From 3ea08f65289157c4707134c4ccc234726c4f4db7 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 18 Aug 2026 21:09:32 -0700 Subject: [PATCH 1/7] feat: add dynamic apps packages --- .github/workflows/ci.yml | 36 + .github/workflows/publish.yml | 104 + .gitignore | 7 + benchmarks/dynamic-apps/README.md | 31 + benchmarks/dynamic-apps/package.json | 16 + benchmarks/dynamic-apps/src/load.test.ts | 80 + benchmarks/dynamic-apps/src/load.ts | 362 + benchmarks/dynamic-apps/tsconfig.json | 12 + biome.json | 48 + .../design/dynamic-apps-api-simplification.md | 835 ++ .../design/dynamic-apps-packaging.md | 406 + docs/content/docs/background-work.mdx | 4 +- docs/content/docs/deploy.mdx | 2 +- docs/content/docs/index.mdx | 12 +- docs/content/docs/quickstart.mdx | 4 +- docs/content/docs/realtime.mdx | 2 +- docs/content/docs/reference.mdx | 2 +- docs/content/docs/routing.mdx | 2 +- docs/content/docs/state-and-data.mdx | 2 +- examples/apps-ai-builder/README.md | 20 + .../apps-ai-builder/fixtures/app/package.json | 17 + .../apps-ai-builder/fixtures/app/src/index.ts | 23 + .../fixtures/app/tsconfig.json | 12 + examples/apps-ai-builder/package.json | 23 + examples/apps-ai-builder/src/actors.ts | 10 + examples/apps-ai-builder/src/server.ts | 132 + examples/apps-ai-builder/tsconfig.json | 12 + examples/apps-hello-world/README.md | 21 + .../fixtures/app/package.json | 12 + .../fixtures/app/src/index.ts | 29 + examples/apps-hello-world/package.json | 21 + examples/apps-hello-world/src/actors.ts | 10 + examples/apps-hello-world/src/deploy.ts | 32 + examples/apps-hello-world/src/server.ts | 18 + examples/apps-hello-world/tsconfig.json | 12 + examples/apps-multiplayer/README.md | 19 + .../fixtures/app/package.json | 13 + .../fixtures/app/src/index.ts | 40 + examples/apps-multiplayer/package.json | 22 + examples/apps-multiplayer/src/actors.ts | 10 + examples/apps-multiplayer/src/client.ts | 26 + examples/apps-multiplayer/src/server.ts | 26 + examples/apps-multiplayer/tsconfig.json | 12 + examples/apps-sqlite/README.md | 19 + .../apps-sqlite/fixtures/app/package.json | 13 + .../apps-sqlite/fixtures/app/src/index.ts | 36 + examples/apps-sqlite/package.json | 22 + examples/apps-sqlite/src/actors.ts | 10 + examples/apps-sqlite/src/client.ts | 26 + examples/apps-sqlite/src/server.ts | 26 + examples/apps-sqlite/tsconfig.json | 12 + examples/apps-static-website/README.md | 18 + .../apps-static-website/fixtures/app/app.js | 2 + .../fixtures/app/index.html | 15 + .../apps-static-website/fixtures/app/logo.svg | 4 + .../fixtures/app/styles.css | 10 + examples/apps-static-website/package.json | 21 + examples/apps-static-website/src/actors.ts | 10 + examples/apps-static-website/src/server.ts | 26 + examples/apps-static-website/tsconfig.json | 12 + examples/apps-workflows/README.md | 18 + .../apps-workflows/fixtures/app/package.json | 13 + .../apps-workflows/fixtures/app/src/index.ts | 37 + examples/apps-workflows/package.json | 22 + examples/apps-workflows/src/actors.ts | 10 + examples/apps-workflows/src/client.ts | 25 + examples/apps-workflows/src/server.ts | 26 + examples/apps-workflows/tsconfig.json | 12 + package.json | 23 + .../dynamic-apps-builder/agentos-package.json | 3 + .../dynamic-apps-builder/cli/apps-builder.mjs | 553 ++ packages/dynamic-apps-builder/package.json | 40 + packages/dynamic-apps-builder/src/index.ts | 12 + .../dynamic-apps-builder/test/builder.test.ts | 326 + packages/dynamic-apps-builder/tsconfig.json | 9 + packages/dynamic-apps/README.md | 100 + .../assets/inspector/deployment/index.html | 50 + .../assets/inspector/replica/index.html | 50 + .../assets/inspector/scaler/index.html | 50 + packages/dynamic-apps/package.json | 52 + packages/dynamic-apps/src/actors.ts | 3891 +++++++++ packages/dynamic-apps/src/advanced.ts | 4 + packages/dynamic-apps/src/control-plane.ts | 177 + packages/dynamic-apps/src/control-request.ts | 65 + packages/dynamic-apps/src/deploy.ts | 115 + packages/dynamic-apps/src/engine-proxy.ts | 625 ++ packages/dynamic-apps/src/errors.ts | 18 + packages/dynamic-apps/src/index.ts | 33 + packages/dynamic-apps/src/router.ts | 112 + packages/dynamic-apps/src/runtime.ts | 560 ++ packages/dynamic-apps/src/source.ts | 128 + packages/dynamic-apps/src/types.ts | 60 + packages/dynamic-apps/tests/apps.test.ts | 1454 ++++ .../dynamic-apps/tests/engine-proxy.test.ts | 239 + .../tests/fixtures/legacy-0.2.15.json | 25 + packages/dynamic-apps/tsconfig.json | 15 + pnpm-lock.yaml | 7187 +++++++++++++++++ pnpm-workspace.yaml | 20 + scripts/check-boundaries.mjs | 94 + scripts/resolve-release.mjs | 62 + scripts/set-release-version.mjs | 18 + scripts/test-packed.mjs | 144 + tests/e2e/dynamic-apps/README.md | 36 + tests/e2e/dynamic-apps/package.json | 25 + tests/e2e/dynamic-apps/src/run.ts | 104 + tests/e2e/dynamic-apps/src/verify.ts | 604 ++ tests/e2e/dynamic-apps/tsconfig.json | 12 + tsconfig.base.json | 14 + 108 files changed, 20078 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 benchmarks/dynamic-apps/README.md create mode 100644 benchmarks/dynamic-apps/package.json create mode 100644 benchmarks/dynamic-apps/src/load.test.ts create mode 100644 benchmarks/dynamic-apps/src/load.ts create mode 100644 benchmarks/dynamic-apps/tsconfig.json create mode 100644 biome.json create mode 100644 docs-internal/design/dynamic-apps-api-simplification.md create mode 100644 docs-internal/design/dynamic-apps-packaging.md create mode 100644 examples/apps-ai-builder/README.md create mode 100644 examples/apps-ai-builder/fixtures/app/package.json create mode 100644 examples/apps-ai-builder/fixtures/app/src/index.ts create mode 100644 examples/apps-ai-builder/fixtures/app/tsconfig.json create mode 100644 examples/apps-ai-builder/package.json create mode 100644 examples/apps-ai-builder/src/actors.ts create mode 100644 examples/apps-ai-builder/src/server.ts create mode 100644 examples/apps-ai-builder/tsconfig.json create mode 100644 examples/apps-hello-world/README.md create mode 100644 examples/apps-hello-world/fixtures/app/package.json create mode 100644 examples/apps-hello-world/fixtures/app/src/index.ts create mode 100644 examples/apps-hello-world/package.json create mode 100644 examples/apps-hello-world/src/actors.ts create mode 100644 examples/apps-hello-world/src/deploy.ts create mode 100644 examples/apps-hello-world/src/server.ts create mode 100644 examples/apps-hello-world/tsconfig.json create mode 100644 examples/apps-multiplayer/README.md create mode 100644 examples/apps-multiplayer/fixtures/app/package.json create mode 100644 examples/apps-multiplayer/fixtures/app/src/index.ts create mode 100644 examples/apps-multiplayer/package.json create mode 100644 examples/apps-multiplayer/src/actors.ts create mode 100644 examples/apps-multiplayer/src/client.ts create mode 100644 examples/apps-multiplayer/src/server.ts create mode 100644 examples/apps-multiplayer/tsconfig.json create mode 100644 examples/apps-sqlite/README.md create mode 100644 examples/apps-sqlite/fixtures/app/package.json create mode 100644 examples/apps-sqlite/fixtures/app/src/index.ts create mode 100644 examples/apps-sqlite/package.json create mode 100644 examples/apps-sqlite/src/actors.ts create mode 100644 examples/apps-sqlite/src/client.ts create mode 100644 examples/apps-sqlite/src/server.ts create mode 100644 examples/apps-sqlite/tsconfig.json create mode 100644 examples/apps-static-website/README.md create mode 100644 examples/apps-static-website/fixtures/app/app.js create mode 100644 examples/apps-static-website/fixtures/app/index.html create mode 100644 examples/apps-static-website/fixtures/app/logo.svg create mode 100644 examples/apps-static-website/fixtures/app/styles.css create mode 100644 examples/apps-static-website/package.json create mode 100644 examples/apps-static-website/src/actors.ts create mode 100644 examples/apps-static-website/src/server.ts create mode 100644 examples/apps-static-website/tsconfig.json create mode 100644 examples/apps-workflows/README.md create mode 100644 examples/apps-workflows/fixtures/app/package.json create mode 100644 examples/apps-workflows/fixtures/app/src/index.ts create mode 100644 examples/apps-workflows/package.json create mode 100644 examples/apps-workflows/src/actors.ts create mode 100644 examples/apps-workflows/src/client.ts create mode 100644 examples/apps-workflows/src/server.ts create mode 100644 examples/apps-workflows/tsconfig.json create mode 100644 package.json create mode 100644 packages/dynamic-apps-builder/agentos-package.json create mode 100755 packages/dynamic-apps-builder/cli/apps-builder.mjs create mode 100644 packages/dynamic-apps-builder/package.json create mode 100644 packages/dynamic-apps-builder/src/index.ts create mode 100644 packages/dynamic-apps-builder/test/builder.test.ts create mode 100644 packages/dynamic-apps-builder/tsconfig.json create mode 100644 packages/dynamic-apps/README.md create mode 100644 packages/dynamic-apps/assets/inspector/deployment/index.html create mode 100644 packages/dynamic-apps/assets/inspector/replica/index.html create mode 100644 packages/dynamic-apps/assets/inspector/scaler/index.html create mode 100644 packages/dynamic-apps/package.json create mode 100644 packages/dynamic-apps/src/actors.ts create mode 100644 packages/dynamic-apps/src/advanced.ts create mode 100644 packages/dynamic-apps/src/control-plane.ts create mode 100644 packages/dynamic-apps/src/control-request.ts create mode 100644 packages/dynamic-apps/src/deploy.ts create mode 100644 packages/dynamic-apps/src/engine-proxy.ts create mode 100644 packages/dynamic-apps/src/errors.ts create mode 100644 packages/dynamic-apps/src/index.ts create mode 100644 packages/dynamic-apps/src/router.ts create mode 100644 packages/dynamic-apps/src/runtime.ts create mode 100644 packages/dynamic-apps/src/source.ts create mode 100644 packages/dynamic-apps/src/types.ts create mode 100644 packages/dynamic-apps/tests/apps.test.ts create mode 100644 packages/dynamic-apps/tests/engine-proxy.test.ts create mode 100644 packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json create mode 100644 packages/dynamic-apps/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/check-boundaries.mjs create mode 100644 scripts/resolve-release.mjs create mode 100644 scripts/set-release-version.mjs create mode 100644 scripts/test-packed.mjs create mode 100644 tests/e2e/dynamic-apps/README.md create mode 100644 tests/e2e/dynamic-apps/package.json create mode 100644 tests/e2e/dynamic-apps/src/run.ts create mode 100644 tests/e2e/dynamic-apps/src/verify.ts create mode 100644 tests/e2e/dynamic-apps/tsconfig.json create mode 100644 tsconfig.base.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..57e1f2f57 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm check-types + - run: pnpm test + - run: pnpm check-boundaries + - run: pnpm lint + - run: npm pack --dry-run + working-directory: packages/dynamic-apps-builder + - run: npm pack --dry-run + working-directory: packages/dynamic-apps + - run: pnpm test:packed diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..92d1349a9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,104 @@ +name: Publish + +on: + workflow_dispatch: + inputs: + version: + description: Semver to publish, or "legacy" for the matching legacy baseline + required: true + default: legacy + type: string + dist_tag: + description: npm dist-tag (auto derives latest/rc/next) + required: true + default: auto + type: choice + options: [auto, latest, rc, next, preview] + +permissions: + contents: write + id-token: write + +concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + environment: npm + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + registry-url: https://registry.npmjs.org + - run: pnpm install --frozen-lockfile + - id: release + run: >- + node scripts/resolve-release.mjs + --version=${{ inputs.version }} + --tag=${{ inputs.dist_tag }} + --branch=${{ github.ref_name }} + - name: Verify npm authentication and unused versions + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + npm whoami + for package in @rivet-dev/dynamic-apps-builder @rivet-dev/dynamic-apps; do + if npm view "$package@${{ steps.release.outputs.version }}" version >/dev/null 2>&1; then + echo "$package@${{ steps.release.outputs.version }} already exists" >&2 + exit 1 + fi + done + - run: node scripts/set-release-version.mjs ${{ steps.release.outputs.version }} + - run: pnpm build + - run: pnpm check-types + - run: pnpm test + - run: pnpm check-boundaries + - run: pnpm lint + - run: pnpm test:packed + - name: Publish builder + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: >- + npm publish + .pack/rivet-dev-dynamic-apps-builder-${{ steps.release.outputs.version }}.tgz + --access public + --provenance + --tag ${{ steps.release.outputs.npm_tag }} + - name: Wait for builder registry visibility + run: | + set -euo pipefail + for attempt in $(seq 1 30); do + if npm view "@rivet-dev/dynamic-apps-builder@${{ steps.release.outputs.version }}" version >/dev/null 2>&1; then + exit 0 + fi + sleep 10 + done + echo "builder version did not become visible" >&2 + exit 1 + - name: Publish main package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: >- + npm publish + .pack/rivet-dev-dynamic-apps-${{ steps.release.outputs.version }}.tgz + --access public + --provenance + --tag ${{ steps.release.outputs.npm_tag }} + - name: Create release tag + if: steps.release.outputs.real_release == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git tag "v${{ steps.release.outputs.version }}" + git push origin "v${{ steps.release.outputs.version }}" + gh release create "v${{ steps.release.outputs.version }}" --generate-notes diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..288c03989 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +*.tsbuildinfo +*.aospkg +.pack/ +.agent/ +.DS_Store diff --git a/benchmarks/dynamic-apps/README.md b/benchmarks/dynamic-apps/README.md new file mode 100644 index 000000000..deb1cfd7b --- /dev/null +++ b/benchmarks/dynamic-apps/README.md @@ -0,0 +1,31 @@ +# Dynamic Apps load test + +Start `examples/apps-hello-world`, then run: + +```sh +pnpm --filter @rivet-dev/dynamic-apps-benchmarks load +``` + +The bounded driver reports p50, p89, p95, and p99 latency for all, cold, and +warm requests, plus queue delay, replica distribution, throughput, and status +counts. + +The defaults run 16 concurrent clients for 10 seconds, with hard limits of +100,000 requests, 100,000 latency samples, 1 MiB per response, 1,024 replica +series, and 10 seconds per request. Configure them with: + +| Variable | Default | +| --- | ---: | +| `LOAD_TEST_URL` | `http://127.0.0.1:3000/apps/hello-world` | +| `LOAD_TEST_CONCURRENCY` | `16` | +| `LOAD_TEST_DURATION_SECONDS` | `10` | +| `LOAD_TEST_TIMEOUT_MS` | `10000` | +| `LOAD_TEST_MAX_REQUESTS` | `100000` | +| `LOAD_TEST_MAX_SAMPLES` | `100000` | +| `LOAD_TEST_MAX_RESPONSE_BYTES` | `1048576` | +| `LOAD_TEST_MAX_REPLICA_SERIES` | `1024` | + +Optional `LOAD_TEST_MAX_P95_MS` and `LOAD_TEST_MIN_SUCCESS_RATE` (from `0` to +`1`) turn the run into a failing performance gate. A run can take up to one +request timeout beyond its configured duration while the final in-flight +requests finish. diff --git a/benchmarks/dynamic-apps/package.json b/benchmarks/dynamic-apps/package.json new file mode 100644 index 000000000..804ea8d70 --- /dev/null +++ b/benchmarks/dynamic-apps/package.json @@ -0,0 +1,16 @@ +{ + "name": "@rivet-dev/dynamic-apps-benchmarks", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "load": "node --import tsx src/load.ts", + "check-types": "tsc --noEmit", + "test": "node --import tsx --test src/load.test.ts" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/benchmarks/dynamic-apps/src/load.test.ts b/benchmarks/dynamic-apps/src/load.test.ts new file mode 100644 index 000000000..312461bf9 --- /dev/null +++ b/benchmarks/dynamic-apps/src/load.test.ts @@ -0,0 +1,80 @@ +import { strict as assert } from "node:assert"; +import { createServer } from "node:http"; +import { describe, it } from "node:test"; +import { readLoadConfig, runLoadTest } from "./load.js"; + +describe("Dynamic Apps load driver", () => { + it("rejects an impossible success-rate gate", () => { + assert.throws( + () => readLoadConfig({ LOAD_TEST_MIN_SUCCESS_RATE: "1.1" }), + /LOAD_TEST_MIN_SUCCESS_RATE must be a number between 0 and 1/, + ); + }); + + it("records cold and warm latency with a hard request bound", async () => { + let requestCount = 0; + const server = createServer((_request, response) => { + requestCount += 1; + response.setHeader( + "x-agentos-app-cold-start", + requestCount % 2 === 1 ? "1" : "0", + ); + response.setHeader( + "x-agentos-app-replica", + `replica-${requestCount % 2}`, + ); + response.setHeader("x-agentos-app-replica-count", "2"); + response.setHeader("x-agentos-app-queue-delay-ms", "3"); + response.end("hello"); + }); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + + try { + const address = server.address(); + assert(address && typeof address !== "string"); + const result = await runLoadTest({ + ...readLoadConfig({}), + target: `http://127.0.0.1:${address.port}`, + concurrency: 2, + durationSeconds: 1, + maxRequests: 4, + }); + + assert.equal(result.completed, 4); + assert.equal(result.successRate, 1); + assert.equal(result.coldStarts, 2); + assert.equal(result.warmRequests, 2); + assert.equal(result.unclassifiedRequests, 0); + assert.equal(result.warmHitRate, 0.5); + assert.equal(result.replicaHeaderCoverage, 1); + assert.equal(result.maximumReplicaCount, 2); + assert.equal(result.stoppedBy, "request-limit"); + assert(result.coldLatencyMs.p50 > 0); + assert(result.warmLatencyMs.p50 > 0); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } + }); + + it("fails a request instead of buffering an oversized response", async () => { + const result = await runLoadTest( + { + ...readLoadConfig({}), + target: "http://load.test", + concurrency: 1, + durationSeconds: 1, + maxRequests: 1, + maxResponseBytes: 4, + }, + async () => new Response("too large"), + ); + + assert.equal(result.completed, 1); + assert.equal(result.successRate, 0); + assert.deepEqual(result.statuses, { ResponseBodyLimitError: 1 }); + }); +}); diff --git a/benchmarks/dynamic-apps/src/load.ts b/benchmarks/dynamic-apps/src/load.ts new file mode 100644 index 000000000..3de9c542a --- /dev/null +++ b/benchmarks/dynamic-apps/src/load.ts @@ -0,0 +1,362 @@ +import { performance } from "node:perf_hooks"; +import { pathToFileURL } from "node:url"; + +export interface LoadConfig { + target: string; + concurrency: number; + durationSeconds: number; + timeoutMs: number; + maxRequests: number; + maxSamples: number; + maxResponseBytes: number; + maxReplicaSeries: number; + maxP95Ms?: number; + minSuccessRate?: number; +} + +export interface LatencySummary { + p50: number; + p89: number; + p95: number; + p99: number; + max: number; +} + +export interface LoadResult { + target: string; + concurrency: number; + durationSeconds: number; + elapsedSeconds: number; + completed: number; + requestsPerSecond: number; + successRate: number; + latencyMs: LatencySummary; + coldLatencyMs: LatencySummary; + warmLatencyMs: LatencySummary; + statuses: Record; + replicas: Record; + coldStarts: number; + warmRequests: number; + unclassifiedRequests: number; + warmHitRate: number; + replicaHeaderCoverage: number; + maximumReplicaCount: number; + queueDelayMs: Pick; + sampledRequests: number; + droppedLatencySamples: number; + droppedQueueDelaySamples: number; + droppedReplicaSeries: number; + stoppedBy: "duration" | "request-limit"; +} + +export function readLoadConfig( + env: NodeJS.ProcessEnv = process.env, +): LoadConfig { + return { + target: env.LOAD_TEST_URL ?? "http://127.0.0.1:3000/apps/hello-world", + concurrency: integerEnv(env, "LOAD_TEST_CONCURRENCY", 16, 1, 1_000), + durationSeconds: integerEnv( + env, + "LOAD_TEST_DURATION_SECONDS", + 10, + 1, + 3_600, + ), + timeoutMs: integerEnv(env, "LOAD_TEST_TIMEOUT_MS", 10_000, 1, 60_000), + maxRequests: integerEnv( + env, + "LOAD_TEST_MAX_REQUESTS", + 100_000, + 1, + 10_000_000, + ), + maxSamples: integerEnv(env, "LOAD_TEST_MAX_SAMPLES", 100_000, 1, 1_000_000), + maxResponseBytes: integerEnv( + env, + "LOAD_TEST_MAX_RESPONSE_BYTES", + 1_048_576, + 0, + 134_217_728, + ), + maxReplicaSeries: integerEnv( + env, + "LOAD_TEST_MAX_REPLICA_SERIES", + 1_024, + 1, + 10_000, + ), + maxP95Ms: optionalNumberEnv(env, "LOAD_TEST_MAX_P95_MS"), + minSuccessRate: optionalNumberEnv(env, "LOAD_TEST_MIN_SUCCESS_RATE", 1), + }; +} + +export async function runLoadTest( + config: LoadConfig, + fetchImpl: typeof fetch = fetch, +): Promise { + const loadStartedAt = performance.now(); + const deadline = loadStartedAt + config.durationSeconds * 1_000; + const latencies: number[] = []; + const coldLatencies: number[] = []; + const warmLatencies: number[] = []; + const statuses = new Map(); + const replicas = new Map(); + const queueDelays: number[] = []; + let started = 0; + let completed = 0; + let successful = 0; + let coldStarts = 0; + let warmRequests = 0; + let replicaHeaders = 0; + let maximumReplicaCount = 0; + let droppedLatencySamples = 0; + let droppedQueueDelaySamples = 0; + let droppedReplicaSeries = 0; + + await Promise.all( + Array.from({ length: config.concurrency }, async () => { + while (performance.now() < deadline && started < config.maxRequests) { + started += 1; + const startedAt = performance.now(); + let status = "error"; + let temperature: "cold" | "warm" | undefined; + try { + const response = await fetchImpl(config.target, { + signal: AbortSignal.timeout(config.timeoutMs), + headers: { "user-agent": "agentos-apps-load-test" }, + }); + await consumeResponseBody(response, config.maxResponseBytes); + status = String(response.status); + if (response.ok) successful += 1; + + const replica = response.headers.get("x-agentos-app-replica"); + if (replica) { + replicaHeaders += 1; + if ( + replicas.has(replica) || + replicas.size < config.maxReplicaSeries + ) { + replicas.set(replica, (replicas.get(replica) ?? 0) + 1); + } else { + droppedReplicaSeries += 1; + } + } + + const coldStart = response.headers.get("x-agentos-app-cold-start"); + if (coldStart === "1") { + coldStarts += 1; + temperature = "cold"; + } else if (coldStart === "0") { + warmRequests += 1; + temperature = "warm"; + } + + const queueDelay = headerNumber( + response, + "x-agentos-app-queue-delay-ms", + ); + if (queueDelay !== undefined) { + if (queueDelays.length < config.maxSamples) { + queueDelays.push(queueDelay); + } else { + droppedQueueDelaySamples += 1; + } + } + + const replicaCount = headerNumber( + response, + "x-agentos-app-replica-count", + ); + if (replicaCount !== undefined) { + maximumReplicaCount = Math.max(maximumReplicaCount, replicaCount); + } + } catch (error) { + status = error instanceof Error ? error.name : "error"; + } + + const latency = performance.now() - startedAt; + if (latencies.length < config.maxSamples) { + latencies.push(latency); + } else { + droppedLatencySamples += 1; + } + if ( + temperature === "cold" && + coldLatencies.length < config.maxSamples + ) { + coldLatencies.push(latency); + } + if ( + temperature === "warm" && + warmLatencies.length < config.maxSamples + ) { + warmLatencies.push(latency); + } + statuses.set(status, (statuses.get(status) ?? 0) + 1); + completed += 1; + } + }), + ); + + const elapsedSeconds = (performance.now() - loadStartedAt) / 1_000; + const successRate = completed === 0 ? 0 : successful / completed; + const classifiedRequests = coldStarts + warmRequests; + return { + target: config.target, + concurrency: config.concurrency, + durationSeconds: config.durationSeconds, + elapsedSeconds: round(elapsedSeconds), + completed, + requestsPerSecond: round(completed / elapsedSeconds), + successRate, + latencyMs: latencySummary(latencies), + coldLatencyMs: latencySummary(coldLatencies), + warmLatencyMs: latencySummary(warmLatencies), + statuses: Object.fromEntries([...statuses.entries()].sort()), + replicas: Object.fromEntries([...replicas.entries()].sort()), + coldStarts, + warmRequests, + unclassifiedRequests: completed - classifiedRequests, + warmHitRate: + classifiedRequests === 0 ? 0 : warmRequests / classifiedRequests, + replicaHeaderCoverage: completed === 0 ? 0 : replicaHeaders / completed, + maximumReplicaCount, + queueDelayMs: { + p50: round(percentile(queueDelays, 0.5)), + p95: round(percentile(queueDelays, 0.95)), + max: round(Math.max(...queueDelays, 0)), + }, + sampledRequests: latencies.length, + droppedLatencySamples, + droppedQueueDelaySamples, + droppedReplicaSeries, + stoppedBy: started >= config.maxRequests ? "request-limit" : "duration", + }; +} + +async function main(): Promise { + const config = readLoadConfig(); + const result = await runLoadTest(config); + console.log(JSON.stringify(result, null, 2)); + + if (result.droppedLatencySamples > 0 || result.droppedQueueDelaySamples > 0) { + console.warn( + `sample limit ${config.maxSamples} reached; dropped ${result.droppedLatencySamples} latency and ${result.droppedQueueDelaySamples} queue-delay samples; raise LOAD_TEST_MAX_SAMPLES`, + ); + } + if (result.droppedReplicaSeries > 0) { + console.warn( + `replica series limit ${config.maxReplicaSeries} reached; dropped ${result.droppedReplicaSeries} replica observations; raise LOAD_TEST_MAX_REPLICA_SERIES`, + ); + } + if (config.maxP95Ms !== undefined && result.latencyMs.p95 > config.maxP95Ms) { + console.error( + `p95 ${result.latencyMs.p95}ms exceeded ${config.maxP95Ms}ms`, + ); + process.exitCode = 1; + } + if ( + config.minSuccessRate !== undefined && + result.successRate < config.minSuccessRate + ) { + console.error( + `success rate ${result.successRate} was below ${config.minSuccessRate}`, + ); + process.exitCode = 1; + } +} + +async function consumeResponseBody( + response: Response, + maxResponseBytes: number, +): Promise { + if (!response.body) return; + const reader = response.body.getReader(); + let received = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) return; + received += value.byteLength; + if (received > maxResponseBytes) { + await reader.cancel(); + throw new ResponseBodyLimitError(maxResponseBytes); + } + } + } finally { + reader.releaseLock(); + } +} + +function headerNumber(response: Response, name: string): number | undefined { + const header = response.headers.get(name); + if (header === null) return undefined; + const value = Number(header); + return Number.isFinite(value) ? value : undefined; +} + +class ResponseBodyLimitError extends Error { + override name = "ResponseBodyLimitError"; + + constructor(limit: number) { + super( + `response exceeded LOAD_TEST_MAX_RESPONSE_BYTES (${limit} bytes); raise the limit to read larger responses`, + ); + } +} + +function integerEnv( + env: NodeJS.ProcessEnv, + name: string, + fallback: number, + minimum: number, + maximum: number, +): number { + const value = Number(env[name] ?? fallback); + if (!Number.isInteger(value) || value < minimum || value > maximum) { + throw new Error( + `${name} must be an integer between ${minimum} and ${maximum}`, + ); + } + return value; +} + +function optionalNumberEnv( + env: NodeJS.ProcessEnv, + name: string, + maximum = Number.POSITIVE_INFINITY, +): number | undefined { + if (env[name] === undefined) return undefined; + const value = Number(env[name]); + if (!Number.isFinite(value) || value < 0 || value > maximum) { + throw new Error(`${name} must be a number between 0 and ${maximum}`); + } + return value; +} + +function latencySummary(values: number[]): LatencySummary { + values.sort((a, b) => a - b); + return { + p50: round(percentile(values, 0.5)), + p89: round(percentile(values, 0.89)), + p95: round(percentile(values, 0.95)), + p99: round(percentile(values, 0.99)), + max: round(values.at(-1) ?? 0), + }; +} + +function percentile(sorted: number[], quantile: number): number { + if (sorted.length === 0) return 0; + return sorted[ + Math.min(sorted.length - 1, Math.ceil(sorted.length * quantile) - 1) + ]!; +} + +function round(value: number): number { + return Math.round(value * 100) / 100; +} + +const entrypoint = process.argv[1]; +if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) { + await main(); +} diff --git a/benchmarks/dynamic-apps/tsconfig.json b/benchmarks/dynamic-apps/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/benchmarks/dynamic-apps/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..e2d225692 --- /dev/null +++ b/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "files": { + "includes": [ + "packages/**/*.ts", + "examples/**/*.ts", + "tests/**/*.ts", + "benchmarks/**/*.ts", + "scripts/**/*.mjs", + "!/**/node_modules" + ], + "ignoreUnknown": true + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "main" + }, + "formatter": { + "enabled": true, + "useEditorconfig": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noUselessElse": "off", + "useNodejsImportProtocol": "error" + }, + "correctness": { + "noUnusedImports": "warn" + }, + "suspicious": { + "noExplicitAny": "off", + "noControlCharactersInRegex": "off" + }, + "performance": { + "useTopLevelRegex": "off" + }, + "nursery": { + "noFloatingPromises": "error", + "noMisusedPromises": "error" + } + } + } +} diff --git a/docs-internal/design/dynamic-apps-api-simplification.md b/docs-internal/design/dynamic-apps-api-simplification.md new file mode 100644 index 000000000..3026b5076 --- /dev/null +++ b/docs-internal/design/dynamic-apps-api-simplification.md @@ -0,0 +1,835 @@ +# Dynamic Apps API Simplification + +Status: implemented and end-to-end validated proof of concept; production +credential plumbing and repo-wide release gates remain open. + +This document records the implemented Dynamic Apps public API and tracks the +remaining work required to take the proof of concept to production. The runtime +moves platform plumbing out of the user-facing surface while preserving +ordinary RivetKit clients and DirectActor calls. + +## Goals + +- Keep `setup()` and `createClient()` as ordinary RivetKit APIs. +- Make `setupApps()` responsible only for creating Dynamic Apps actor + definitions. +- Give every infrastructure actor a stable `agentOSApps*` registry name. +- Deploy a directory or generated in-memory file tree with one function. +- Mount all application HTTP routes on a Hono server without manual path + parsing or request forwarding. +- Use the ordinary RivetKit client defaults. Examples must not read or forward + `RIVET_*` environment variables. +- Store submitted files and immutable releases durably in the application + actor's SQLite database. +- Keep local files disposable. No persistent artifact directory may be required + for recovery or replica placement. +- Preserve the direct RivetKit actor path. Dynamic Apps must never proxy or + reinterpret DirectActor calls. + +## Target API + +### Actor setup + +`setupApps()` creates actor definitions and nothing else: + +```ts +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + ...appsActors, + }, +}); + +registry.start(); +``` + +The exported map must use explicit, collision-resistant registry keys: + +```ts +const appsActors = { + agentOSAppsApp, + agentOSAppsScaler, + agentOSAppsReplica, +}; +``` + +`setupApps()` must not: + +- call `setup()` or start a registry; +- create or wrap a RivetKit client; +- construct an HTTP router; +- read Rivet endpoint, token, namespace, or pool environment variables; +- create directories or perform other import-time I/O. + +The initial common API should require no options: + +```ts +const { appsActors } = setupApps(); +``` + +Actor implementation overrides may be added later under an explicitly advanced +surface. They must not make the default example configure VM permissions, +runtime connection details, namespace provisioning, or artifact storage. + +### Deploying an application + +`deployApp()` is independent from the value returned by `setupApps()`: + +```ts +import { deployApp } from "@rivet-dev/dynamic-apps"; + +const deployment = await deployApp({ + appId: "hello-world", + source: new URL("../fixtures/app/", import.meta.url), +}); +``` + +It lazily creates an ordinary `createClient()` when no client is supplied. +RivetKit resolves the ordinary request client's endpoint, token, namespace, and +pool defaults. The proof of concept still duplicates the standard connection +variables for its internal namespace control-plane calls; removing that +duplication requires the RivetKit primitive tracked below. + +An existing ordinary client can be supplied without creating an Dynamic Apps +client or wrapper: + +```ts +await deployApp({ + appId: "hello-world", + source: new URL("../fixtures/app/", import.meta.url), +}, { client }); +``` + +The input supports a local directory for checked examples and an in-memory file +tree for generated applications: + +```ts +type DeployAppInput = + | { + appId: string; + source: URL; + createNamespace?: boolean; + regions?: string[]; + scaling?: AppScaling; + } + | { + appId: string; + files: Record; + createNamespace?: boolean; + regions?: string[]; + scaling?: AppScaling; + }; +``` + +The result should contain only stable application information: + +```ts +interface Deployment { + appId: string; + release: string; + namespace: string; + pool: string; + regions: string[]; +} +``` + +`appId` is always a required property. There is no positional application +identifier and no generated default. Public and internal implementation +identifiers must consistently use `appId`; remove ambiguous identifier names +such as `name`, `app`, and `appKey`. + +The common-path defaults are: + +| Setting | Default | +| --- | --- | +| `regions` | The stable application actor's current Rivet region | +| `scaling.minReplicas` | `0`; active releases scale to zero when idle | +| `scaling.maxReplicas` | `128` replicas per deployed region | +| `scaling.targetConcurrency` | `8` admitted requests per replica | +| Excess replica warm retention | Five minutes | +| RivetKit client | Lazily create the ordinary default client | +| Rivet namespace | Reuse the namespace configured for the ordinary Rivet connection | +| Dependency installation | `npm ci` with a lockfile; otherwise bounded `npm install` | +| Build | Run `npm run build` when the package defines a build script | +| Entrypoint | Infer from `exports`, then `main`, then the documented default | +| Release activation | Boot and verify at least one replica in every requested region before activation, even when `minReplicas` is `0` | + +`source` and `files` are mutually exclusive and exactly one is required. +`warmIdleTimeout` and infrastructure limits remain bounded internal or advanced +settings; callers should not need them for a normal deployment. + +### Hono routing + +HTTP routing is also independent from `setupApps()`: + +```ts +import { appsRouter } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; + +const server = new Hono(); + +server.route("/apps", appsRouter); +``` + +The router uses a lazy ordinary RivetKit client and handles: + +- `/:appId` and `/:appId/*`; +- bounded `appId` parsing; +- removal of the mounted application prefix; +- region selection; +- scaler admission and renewable leases; +- bounded request buffering and response streaming; +- cancellation and backpressure; +- hop-by-hop header removal and repeated response headers; +- typed mapping of expected routing errors to HTTP responses. + +For a custom client, an advanced adapter may construct the same router without +coupling it to `setupApps()`: + +```ts +import { createAppsRouter } from "@rivet-dev/dynamic-apps/advanced"; + +server.route("/apps", createAppsRouter({ client })); +``` + +There is no public `routeAppRequest()` in the target common API. + +### Complete server example + +The intended example server is: + +```ts +import { serve } from "@hono/node-server"; +import { + appsRouter, + deployApp, + setup, + setupApps, +} from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + ...appsActors, + }, +}); + +registry.start(); + +await deployApp({ + appId: "hello-world", + source: new URL("../fixtures/app/", import.meta.url), +}); + +const server = new Hono(); + +server.route("/apps", appsRouter); + +serve({ + fetch: server.fetch, + port: 3000, +}); +``` + +Runner registration races must be handled inside Dynamic Apps with a bounded +retry for known readiness errors. This retry does not belong in examples. + +## Durable Storage + +### SQLite is the source of truth + +The stable application actor owns the submitted source and built release in its +SQLite database. Actor state should retain only small coordination fields; file +content and artifacts belong in explicit tables. + +Proposed logical schema: + +```text +app_releases + release_id + created_at + status + entrypoint + artifact_hash + artifact_bytes + build_error + +app_release_files + release_id + path + content + byte_length + +app_release_artifact_chunks + release_id + chunk_index + content + byte_length +``` + +All tables and operations must be bounded: + +- maximum releases retained per application; +- maximum files and source bytes per release; +- maximum path length; +- maximum individual and aggregate artifact bytes; +- fixed artifact chunk size and maximum chunk count; +- maximum build stdout and stderr retained; +- bounded transactions and batch sizes; +- typed errors naming the violated limit and its configuration. + +Deployment must be transactional from the caller's perspective: + +1. Validate and normalize every submitted path. +2. Compute the canonical release hash. +3. Insert the release and source files as a non-active building release. +4. Build in a short-lived agentOS VM. +5. Persist and verify the immutable artifact chunks. +6. Warm the required regional replicas. +7. Atomically make the release active. +8. Leave the previous release active on any failure. + +Failed release records may retain bounded diagnostics, but partial artifact +chunks must be removed. + +### Local files are disposable + +agentOS currently requires a host path when mounting a packed `.aospkg`. A +replica may therefore materialize an artifact from SQLite into a bounded +temporary file: + +```text +application actor SQLite + | + | bounded, checksummed chunks + v +replica-owned temporary .aospkg + | + v +agentOS VM read-only package mount +``` + +The temporary file: + +- is not the durable source of truth; +- is scoped to a replica or bounded cache entry; +- is recreated after process or host restart; +- remains present for the VM's entire lifetime because package reads may be + lazy; +- is cleaned after the VM and all lazy mount readers are finished; +- must never require a user-configured artifact directory. + +Delete `localArtifacts()` and the `.data/apps-artifacts` example directory. +There should be no `.agentos/apps/artifacts` persistent requirement either. + +### Replica wake, warm retention, and cleanup + +The replica lifecycle is: + +```text +wake + -> stream the active release from application SQLite + -> write a fresh replica-scoped temporary .aospkg + -> verify its size and content hash + -> boot the agentOS VM + -> report ready to the scaler + +retire, sleep, destroy, or startup failure + -> stop accepting new leases + -> drain bounded in-flight requests + -> stop and dispose the VM + -> delete the temporary .aospkg and its directory +``` + +Cleanup must run in `finally` on every terminal path. A failed cleanup must be +logged and retried or returned as a typed error; it must not be swallowed. A +subsequent wake always creates a new temporary path and never trusts a leftover +file from a previous VM. + +Warm retention and actor sleep are separate policies: + +- `warmIdleTimeout` controls how long the scaler keeps an excess replica hot + after its last lease; +- the configured minimum replica count remains hot indefinitely; +- the actor sleep grace period only controls the actor lifecycle and cleanup + window. It is not the warm-pool autoscaling policy. + +The current 30-second scale-down delay is too aggressive for a VM that must +rehydrate an npm application, initialize V8, and boot its HTTP server. Use a +five-minute default `warmIdleTimeout` for excess replicas, while keeping it an +advanced bounded setting rather than common setup configuration. + +`minReplicas` defaults to `0`. A deployment still boots and health-checks one +replica in every requested region before activating the release. That verified +replica remains warm until the normal idle timeout and can then retire, leaving +the active release at zero replicas. The next request rehydrates the artifact +from SQLite and cold-starts a replica. + +Replicas currently opt out of automatic actor sleep, so the scaler must +explicitly retire and destroy excess replicas after the warm idle timeout. +This preserves an accurate distinction between ready, warm replicas and +nonexistent replicas. If replicas later use engine-driven sleep, a sleeping +replica must first be removed from the scaler's ready set and must complete the +full wake-and-readiness sequence before receiving another request. + +### Scaler capacity warning + +Each regional scaler must emit a host-visible warning when its provisioned +replica count transitions from at or below 50% to above 50% of +`scaling.maxReplicas`. Count both ready and warming replicas so concurrent +scale-up cannot hide approaching capacity. + +For the default `maxReplicas: 128`, the warning is emitted when the count first +reaches `65`. It is transition-based rather than request-based: latch the +warning while usage remains above 50%, clear the latch after usage returns to +50% or below, and warn again only after a later upward crossing. + +The structured warning must include `appId`, release, region, ready replicas, +warming replicas, `maxReplicas`, and the utilization percentage. It must name +the limit and explain how to raise it. Reaching the warning threshold does not +reject traffic or force another scale-up by itself. + +### Retention and garbage collection + +When an inactive release exceeds the configured retention count: + +1. Drain its regional scalers and replicas. +2. Confirm no replica still references its artifact. +3. Delete its artifact chunks. +4. Delete its source files. +5. Delete its release metadata. + +Cleanup failures must be logged and retried. They must not be silently ignored. + +## Namespace and Runtime Plumbing + +The common path reuses the namespace configured for the ordinary Rivet +connection and makes no namespace-management request. Callers may set +`createNamespace: true` to idempotently create a stable, isolated namespace +for `appId` within the configured host namespace. + +Dynamic Apps must internally: + +1. Resolve the namespace from the ordinary Rivet connection by default. +2. When opted in, derive and idempotently create a namespace deterministic for + `appId` within the configured host namespace. +3. Configure a stable Dynamic Apps guest runner pool derived from `appId`. +4. Mint or resolve credentials scoped to that namespace and runner connection. +5. Inject only the namespace, endpoint, pool, scoped credential, and monotonic + release version into the guest process. +6. Keep management credentials out of actor state, SQLite, artifacts, logs, and + guest-visible environment variables. + +Remove these common API concepts: + +- `rivetNamespaceProvisioner()`; +- the `provision` callback; +- the `runtime()` callback; +- `AppRuntimeConfig`; +- manual endpoint, token, namespace, and pool configuration. + +If current RivetKit APIs cannot resolve default client configuration or create a +scoped runner credential without duplicating environment parsing, add the +necessary primitive to RivetKit. Do not keep the callback-based public API as a +workaround. + +## Source Build and Package Conventions + +For `{ source: URL }`, recursively load the directory with these rules: + +- accept only a `file:` directory URL; +- reject symlinks, devices, sockets, and paths escaping the root; +- enforce file-count, individual-file, total-byte, and path-length bounds while + reading; +- preserve empty files and binary static assets; +- sort normalized paths before hashing and upload; +- ignore only a documented fixed set of local build artifacts; +- never follow a user `.gitignore` implicitly. + +The in-memory API accepts byte values for static assets: + +```ts +files: Record +``` + +Dependency installation and compilation happen once per immutable release in a +short-lived agentOS build VM. They never run in the trusted host process and +never run independently on every serving replica: + +```text +deployApp() + -> validate and normalize the submitted source + -> persist the source in the application actor's SQLite + -> start an isolated, bounded agentOS build VM + -> materialize the source into the build VM workspace + -> install dependencies + -> run the build, if present + -> resolve and smoke-test the HTTP entrypoint or static output + -> prune build-only dependencies + -> pack source, output, and runtime node_modules into one .aospkg + -> stream checksummed artifact chunks into application SQLite + -> destroy the build VM and its temporary filesystem +``` + +Package behavior is inferred from `package.json`: + +1. Use `npm ci` when `package-lock.json` exists. +2. Otherwise use bounded `npm install` and retain the generated lockfile with + the immutable release. +3. Run lifecycle scripts only inside the untrusted build VM. The VM receives no + host secrets and has bounded CPU, memory, filesystem, process, output, + network, and wall-clock limits. +4. Run `npm run build` when a build script is present. +5. Resolve a server entrypoint from `package.json.exports`, then + `package.json.main`, then the documented source default. +6. If there is no server entrypoint but the build produced `dist/index.html`, + package `dist/` with the Dynamic Apps static HTTP entrypoint. +7. If there is no `package.json` and the submitted root contains `index.html`, + package the submitted tree as a static website without installing modules. +8. Fail with a typed error when the server/static mode or entrypoint is + ambiguous. + +After a server build, remove development-only dependencies while retaining +runtime dependencies. The resulting `.aospkg` contains the application and its +runtime `node_modules`, so ordinary Node package resolution works inside every +replica without another install. This is also how a guest application imports +the published `rivetkit` npm package. + +Native Node addons are not silently accepted when the agentOS JavaScript runtime +cannot load them. Installation or the smoke test must return a typed unsupported +module error naming the package. Pure JavaScript and WebAssembly packages use +normal package resolution. + +The first implementation should not add a shared mutable `node_modules` cache. +An identical immutable release may reuse its verified artifact; otherwise each +release receives a clean build VM. Build logs, artifact size, dependency count, +process count, network destinations, and build duration are all bounded and +reported through typed deployment errors. + +The common deployment API does not require `entrypoint`, `buildCommand`, +artifact paths, install commands, or VM options. Advanced explicit overrides +can be considered only when real applications prove these conventions +insufficient. + +## Actor Changes + +- Rename the stable application actor registry key to `agentOSAppsApp`. +- Rename the regional scaler actor registry key to `agentOSAppsScaler`. +- Rename the execution replica actor registry key to `agentOSAppsReplica`. +- Return those definitions from `setupApps()` as `appsActors`. +- Keep the actors infrastructure-only; users do not call them for guest actor + actions. +- Replace artifact-path metadata with SQLite release and artifact references. +- Add bounded artifact chunk read actions used only by execution replicas. +- Preserve renewable admission leases and scale-to-zero behavior. +- Preserve monotonic serverless runner versions across releases. +- Keep rollout preparation idempotent across actor retries and process restarts. +- Ensure failed new releases retire every partially created scaler and replica. + +## Client and DirectActor Behavior + +Dynamic Apps does not export a client and does not wrap `createClient()`. + +Guest actors are called through ordinary RivetKit: + +```ts +import { createClient } from "rivetkit/client"; + +const deployment = await deployApp({ appId: "hello-world", source }); +const client = createClient({ + namespace: deployment.namespace, + poolName: deployment.pool, +}); +``` + +The guest application namespace returned by `deployApp()` is used with the +ordinary DirectActor API when an explicit namespace is required. Calls travel: + +```text +RivetKit client + -> Rivet Engine + -> serverless callback through agentOSAppsApp + -> regional scaler admission + -> guest RivetKit registry in a warm agentOS VM + -> guest actor +``` + +This remains the ordinary DirectActor protocol: Dynamic Apps does not wrap the +client or reinterpret actions. Rivet's serverless callback travels through +`agentOSAppsApp` and the regional scaler so actor demand can wake a replica +from zero. It does not require the user-facing Hono router. + +## Public API Changes + +Target common exports: + +```ts +export { + AgentOSAppsError, + appsRouter, + deployApp, + setupApps, + type AppScaling, + type DeployAppInput, + type Deployment, +}; +``` + +Remove from the common public surface: + +```text +agentOSApps +localArtifacts +rivetNamespaceProvisioner +routeAppRequest +AgentOSAppsRoutingClient +AppRuntimeConfig +ArtifactStore +LocalArtifactsOptions +``` + +This repository does not guarantee protocol or API backward compatibility. +Update examples and documentation directly rather than carrying two competing +public APIs. + +## Flat Examples + +Replace the combined `examples/apps/` project with standalone directories: + +```text +examples/ + apps-hello-world/ + apps-sqlite/ + apps-workflows/ + apps-multiplayer/ + apps-static-website/ + apps-ai-builder/ +``` + +Each example uses: + +```text +package.json +tsconfig.json +src/ + server.ts +fixtures/ + app/ +``` + +Rules: + +- trusted host and server code lives in `src/`; +- uploaded application code and static assets live in `fixtures/`; +- examples export `const registry = setup(...)` and call `registry.start()` on a + later statement; +- examples spread `{ ...appsActors }` into `use`; +- examples do not read `RIVET_*` variables; +- examples do not contain local runner-readiness retry loops; +- examples do not contain persistent artifact directories; +- directory examples do not need `files.ts`. + +Move comprehensive validation and load tooling out of beginner examples: + +```text +tests/e2e/dynamic-apps/ +benchmarks/dynamic-apps/ +``` + +### Hello World + +Demonstrate only actor setup, directory deployment, Hono mounting, and one +response. + +### SQLite + +Demonstrate application data persisted through RivetKit actor SQLite. Verify +that data survives request routing to a different Dynamic Apps execution +replica. + +### Workflows + +Demonstrate a durable RivetKit workflow defined inside the deployed application, +including starting it over HTTP, observing progress, and resuming after an +execution replica is replaced. + +### Multiplayer + +Demonstrate a RivetKit multiplayer actor with multiple connected clients, +shared state, reconnect behavior, and execution-replica replacement. Keep the +example focused on the application API rather than load-generation machinery. + +### Static Website + +Demonstrate deploying HTML, CSS, JavaScript, and binary assets without a +`package.json`. Also document the `dist/index.html` convention for a built +static site. + +### AI App Builder + +Tie the complete flow together with the Vercel AI SDK: generate a bounded source +tree, deploy it, feed bounded TypeScript/build diagnostics back to the agent, +repair it, and activate only a successful release. + +Use the Vercel AI SDK on the trusted host: + +```text +prompt + -> coding agent edits bounded in-memory files + -> deployApp() runs the real TypeScript build + -> bounded diagnostics return to the agent + -> agent repairs the files + -> successful immutable release activates +``` + +The host, not the model, decides whether the workflow is complete. Require a +successful deployment, cap model steps and repair attempts, limit editable +paths, and retain the previous valid release during failed iterations. + +## Implementation Order + +### 1. Lock the public contract + +- [x] Add type-level tests for the exact `setupApps()` example. +- [x] Add type-level tests for directory and in-memory `deployApp()` calls. +- [x] Change `deployApp()` to one object input with a required `appId`; remove + the positional application identifier. +- [x] Rename every application identifier field, variable, actor input, route + parameter, error, example, and result to `appId`; remove identifier uses + of `name`, `app`, and `appKey`. +- [x] Add tests for all `deployApp()` defaults and partial scaling overrides. +- [x] Change the default `scaling.minReplicas` to `0` and test idle + scale-to-zero followed by a successful cold wake. +- [x] Change the default `scaling.maxReplicas` to `128` per region and replace + the current hard maximum of `64` with a bounded platform limit that + permits at least the default. +- [x] Add a latched structured warning when a regional scaler crosses above + 50% of `scaling.maxReplicas`, counting ready and warming replicas. +- [x] Test that the capacity warning fires once per upward crossing, rearms + after returning to 50% or below, and includes the required metadata. +- [x] Add a Hono mounting test for `appsRouter`. +- [x] Add a test proving the registry keys are exactly `agentOSAppsApp`, + `agentOSAppsScaler`, and `agentOSAppsReplica`. +- [x] Add a test proving `setupApps()` performs no I/O or client creation. + +### 2. Move releases into actor SQLite + +- [x] Design and migrate the release, file, and artifact-chunk tables. +- [x] Persist normalized source files before starting a build. +- [x] Stream build artifacts into bounded SQLite chunks. +- [x] Verify artifact length and content hash before marking a build ready. +- [x] Rehydrate an artifact into a replica-owned temporary file. +- [x] Keep the temporary artifact for the VM lifetime, then delete it after VM + disposal on retire, sleep, destroy, startup failure, and runtime error. +- [ ] Make cleanup retryable and observable; never reuse an unverified leftover + artifact on wake. +- [x] Replace the 30-second scale-down delay with a five-minute + `warmIdleTimeout` default for excess replicas. +- [x] Prove configured minimum replicas remain warm while excess replicas + retire after the configured timeout. +- [x] Prove deployment verifies one replica per region before activation even + when the configured minimum is zero. +- [x] Prove a cold start succeeds after deleting all local temporary data. +- [x] Implement release retention and garbage collection retried by later + deployments. +- [x] Remove the local artifact store implementation and configuration. + +### 3. Internalize namespace and runner setup + +- [x] Replace provisioning callbacks with internal idempotent namespace setup. +- [x] Make namespace creation opt-in and reuse the configured namespace by + default. +- [ ] Reuse RivetKit's default connection configuration. +- [ ] Add a RivetKit primitive if default config is not safely reusable. +- [x] Configure a stable per-app guest runner pool automatically. +- [ ] Create namespace-scoped guest credentials without exposing management + credentials. +- [x] Move runner-readiness retries into bounded internal deployment logic. +- [x] Delete the public runtime and provisioning APIs. + +### 4. Implement the simple deployment facade + +- [x] Add bounded directory loading. +- [x] Support binary in-memory files. +- [x] Run dependency installation, lifecycle scripts, builds, pruning, and + entrypoint smoke tests only inside a bounded short-lived build VM. +- [x] Infer install, build, server entrypoint, and static output behavior from + the submitted tree and `package.json`. +- [x] Pack runtime dependencies into the immutable artifact so replicas never + install modules on wake. +- [x] Return a typed error for unsupported native Node addons. +- [x] Support package-free static trees rooted at `index.html` and built static + output rooted at `dist/index.html`. +- [x] Preserve content-addressed, deterministic release hashing. +- [x] Let callers optionally pass an ordinary RivetKit client. +- [x] Lazily create the default client without import-time side effects. +- [x] Return stable deployment information and typed build errors. + +### 5. Implement the Hono router + +- [x] Add the `/:appId` and `/:appId/*` routes. +- [x] Strip the mount prefix correctly. +- [x] Preserve response streaming, cancellation, backpressure, and repeated + headers. Request bodies remain bounded and buffered. +- [x] Select regions without maintaining edge-local placement state. +- [x] Use a lazy default client. +- [x] Provide custom-client construction only in the advanced surface. +- [x] Remove public `routeAppRequest()`. + +### 6. Rewrite examples and documentation + +- [x] Replace `examples/apps/` with the flat examples. +- [x] Add focused Hello World, SQLite, Workflows, Multiplayer, and Static + Website examples and corresponding documentation. +- [x] Move E2E verification to `tests/e2e/dynamic-apps/`. +- [x] Move the load driver to `benchmarks/dynamic-apps/`. +- [x] Add the AI SDK generate, type-check, repair, and deploy example. +- [x] Rewrite the package README around the target API. +- [x] Rewrite the website Apps page in this order: product overview, checked + Hello World quick start, application structure, deployment, builds and + dependencies, HTTP routing, SQLite and RivetKit persistence, scaling and + cold starts, regions and isolation, examples, API reference, and current + limitations. +- [x] Lead the website page with the deployable user API; keep scaler, + namespace, artifact, and runner internals after the quick start. +- [x] Source every runnable website snippet from the checked flat examples + through the docs theme `` mechanism. +- [x] Include the deployment defaults table, build pipeline, disposable-replica + versus durable-SQLite diagram, request routing diagram, scale-to-zero + lifecycle, and 50% scaler-capacity warning. +- [x] Link and briefly describe the Hello World, SQLite, Workflows, Multiplayer, + Static Website, and AI App Builder examples without duplicating their + complete READMEs. +- [x] Document the ordinary DirectActor path without an Dynamic Apps client + proxy. +- [x] Update the main Dynamic Apps design wherever the old artifact-store and + routing APIs appear. +- [x] Remove obsolete environment-variable and artifact-directory guidance. + +### 7. Validate the complete behavior + +- [x] Run package unit tests and type checks. +- [x] Run `cargo check --workspace`. +- [ ] Run `pnpm build` and `pnpm check-types`. +- [x] Run fixed-version and publish-helper checks. +- [x] Build the website. +- [x] Run a real RivetKit guest from a packed npm dependency tree. +- [x] Verify DirectActor state survives replica replacement. +- [x] Verify deployment recovery with an empty local filesystem. +- [x] Verify failed TypeScript builds return bounded diagnostics and do not + replace the active release. +- [x] Verify abandoned HTTP requests recover through admission lease expiry. +- [x] Run the bounded load test and record cold-start and warm-request latency. + +## Completion Criteria + +The simplification is complete when a new user can understand the hello-world +server without learning about artifacts, runtime callbacks, namespace +provisioners, route forwarding, or Rivet environment variables; the same +implementation must still recover every deployed release from actor SQLite and +run real RivetKit actors through the ordinary DirectActor API. diff --git a/docs-internal/design/dynamic-apps-packaging.md b/docs-internal/design/dynamic-apps-packaging.md new file mode 100644 index 000000000..f5f15e50c --- /dev/null +++ b/docs-internal/design/dynamic-apps-packaging.md @@ -0,0 +1,406 @@ +# Dynamic Apps Packaging + +Status: implemented and validated on 2026-07-24. + +The production builder is `@rivet-dev/dynamic-apps-builder`. Turbo builds its +generated `dist/package.aospkg` before `@rivet-dev/dynamic-apps`; the artifact +is gitignored but included in the published builder npm package. The current +builder package is 14.2 MB uncompressed and 3.6 MB inside its npm tarball. +The focused shell package is 2.9 MB uncompressed and 1.0 MiB inside its npm +tarball; the build VM no longer mounts the 67.3 MB coreutils package. + +Validation reduced the real RivetKit 2.3.9 fixture from 40,967,331 bytes to +4,889,457 bytes. End-to-end tests cover a package-free app, RivetKit HTTP, +DirectActor state, two-replica autoscaling and routing, and bounded load. The +builder tests also inspect the isolated release rather than resolving +dependencies from the repository. + +This document records the agreed packaging model for Dynamic Apps. The central +rule is: + +> Tenant dependencies exist only in a disposable build VM. Execution replicas +> receive a minimal, immutable App Bundle and never install or build anything. + +The public `deployApp()` API does not expose the bundler, artifact format, or +build-VM configuration. + +## Public API + +Packaging remains an implementation detail behind the existing API: + +```ts +await deployApp({ + appId: "hello", + source: new URL("../fixtures/app", import.meta.url), +}); +``` + +Generated applications continue to use the in-memory file form: + +```ts +await deployApp({ + appId: "hello", + files, +}); +``` + +The common API must not add `bundler`, `runtime`, `artifact`, `minify`, or +builder-package options. Dynamic Apps owns the build conventions and their sane +defaults. + +## Packaging Flow + +```text +deployApp({ appId, source/files }) + | + v +source stored in the stable app actor's SQLite + | + v +temporary agentOS build VM + - mounts platform-owned apps-builder.aospkg + - mounts the platform POSIX shell for package build scripts + - writes tenant source into /workspace + - runs npm ci/install for tenant dependencies + - runs npm run build when defined + - generates the agentOS HTTP runner + - bundles runner + server code + JavaScript dependencies + - emits imported WASM/binary modules separately + - collects static assets + | + v +minimal /release directory + main.mjs + modules/* + public/* + manifest.json + | + v +pack /release as an immutable .aospkg + | + v +store checksummed .aospkg chunks in app actor SQLite + | + v +replica rehydrates and mounts .aospkg at /app + | + v +node /app/main.mjs +``` + +This is equivalent to a multi-stage Docker build: the agentOS build VM is the +builder stage, and the release `.aospkg` is the minimal final image. + +## Platform Build Package + +The build tool must be platform-owned and automatically available. Tenants must +not install it through their own `package.json`. + +Add a software package with a name such as: + +```text +@rivet-dev/dynamic-apps-builder +``` + +`@rivet-dev/dynamic-apps` depends on that package in the same way it currently +depends on `@agentos-software/tar`. Package scripts use the focused +`@agentos-software/sh` package instead of mounting the full coreutils command +set. Each package exports a `SoftwarePackageRef`, and only the Dynamic Apps +build VM includes them in `software`: + +```ts +const buildVmOptions = { + defaultSoftware: false, + software: [sh, tar, appsBuilder], + // Existing bounded permissions and limits. +}; +``` + +The packed software payload contains the pinned build program and everything it +needs: + +```text +apps-builder.aospkg + build-app.mjs + esbuild-wasm JavaScript support + esbuild.wasm + package metadata +``` + +The exact projected package path should come from package resolution rather than +being duplicated as an arbitrary versioned string. The VM invokes the JavaScript +entrypoint with its existing Node runtime. + +Important lifecycle properties: + +- The host installs the Apps builder transitively with Dynamic Apps. +- Every build VM mounts the same immutable, read-only software package. +- No deployment downloads the platform bundler into the tenant workspace. +- Serving replicas do not mount the builder package. +- The release `.aospkg` never contains the builder. +- The builder version and configuration participate in the release hash. +- Updating the builder invalidates build-cache keys. +- Do not add the builder to the global agentOS base layer; ordinary VMs and + serving replicas do not need its relatively large WASM compiler. + +## Build Tool + +Use the mainstream esbuild build model rather than creating a new compiler or +framework build system: + +- Native isolated Linux builders may use ordinary native `esbuild`. +- The current agentOS build VM should use `esbuild-wasm`, because the normal + `esbuild` npm package launches a platform-specific native executable. +- Both implementations must produce the same App Bundle contract. +- The choice between native and WASM esbuild remains internal and can change + without changing `deployApp()` or the execution replicas. + +The direct `esbuild-wasm` API is validated in a real agentOS build VM. The +builder uses esbuild's in-process browser service with `worker: false`; its +Node entrypoint launches a child-process service that is unnecessary inside +the VM and previously made failed builds stall. + +The platform-owned build program should roughly: + +1. Accept a generated runner entrypoint, workspace root, release output + directory, and bounded build settings. +2. Bundle for the agentOS Node runtime as ESM. +3. Set `NODE_ENV` to `production` for dead-code elimination. +4. Enable tree shaking and minification. +5. Emit an external source map for diagnostics, but keep it outside the runtime + package. +6. Emit recognized WASM and binary imports as separate files. +7. Return a metafile describing every generated input and output. +8. Hash the output files and write the App Bundle manifest. + +The generated agentOS runner, rather than the tenant entrypoint alone, is the +bundle entrypoint. This ensures the HTTP adapter and the tenant's imports share +one module graph and one RivetKit module identity. + +## App Bundle + +The logical runtime output is: + +```text +/release + main.mjs + modules/ + -.wasm + -.bin + public/ + index.html + assets/* + manifest.json +``` + +Not every release needs every directory. A server-only release may contain only +`main.mjs`, while a static site may contain a small generated runner and +`public/`. + +The internal manifest should be versioned and simple: + +```ts +interface AppBundleManifest { + version: 1; + mainModule: string; + modules: Array<{ + path: string; + type: "esm" | "wasm" | "text" | "data"; + size: number; + hash: string; + }>; + assets: Array<{ + path: string; + size: number; + hash: string; + }>; +} +``` + +This manifest is internal. Users do not construct or upload it directly in the +initial API. + +The release package must not contain: + +```text +node_modules/ +src/ +package-lock.json +tsconfig.json +platform build tools +unused package files +``` + +SQLite continues to retain the submitted source separately for release history +and rebuilding. The execution artifact contains only runtime outputs. + +## Module And Asset Discovery + +Do not recursively scan `node_modules` for files with interesting extensions. +Packages often ship browser, debug, test, and architecture-specific payloads +that are not used at runtime. + +Use three bounded rules instead. + +### Imported modules + +The bundler follows statically analyzable imports: + +```ts +import dependency from "dependency"; +import schema from "./schema.json"; +import query from "./query.sql"; +import wasmPath from "./engine.wasm"; +``` + +JavaScript, TypeScript, and JSON join the bundle. Recognized WASM and binary +imports become hashed files under `modules/`. Small text-like module types may +be inlined. + +Literal dynamic imports are supported. Computed imports and opaque filesystem +paths are not generally discoverable by any bundler. + +### Static assets + +Use conventional static output directories rather than guessing arbitrary +files: + +- a package-free root static site; +- `dist/` when a frontend build produces `dist/index.html`; +- `public/` for explicitly public application assets. + +Static paths, sizes, and hashes are recorded in the manifest. Initially, include +the required bytes in the release `.aospkg`. The manifest permits a future +content-addressed asset store without changing the user API. + +Do not implement a multi-step asset upload session or JWT protocol for the +proof of concept. The stable app actor already receives and durably owns the +complete file tree. + +### Non-analyzable runtime files + +Computed imports and arbitrary `fs.readFile(runtimeValue)` cannot be packaged +reliably without an explicit convention. The initial behavior should fail with +a bounded, typed build error that identifies the unresolved dependency. + +An advanced module-rule escape hatch may be added when a concrete application +requires it. It is not part of the initial common API. + +## RivetKit + +RivetKit receives a built-in packaging adapter because it is a first-class +Dynamic Apps use case. + +The target is: + +```text +main.mjs bundled app + RivetKit JavaScript +modules/rivetkit-.wasm RivetKit runtime +``` + +Do not retain the RivetKit npm package tree, NAPI bindings, Engine CLI, or +agentOS host integrations in the release. + +Prefer an upstream RivetKit surface that makes its WASM import statically +analyzable or accepts preloaded WASM bindings. Until that is available, the +Apps builder may explicitly resolve and emit the one known RivetKit WASM module. +This is a narrow platform adapter, not a generic `node_modules` scan. + +The generated runner initializes the emitted WASM bytes before importing or +starting the guest registry. `RIVETKIT_RUNTIME=wasm` and serverless runtime mode +remain enforced by the replica. + +## Storage And Replica Lifecycle + +The stable app actor's SQLite database remains the durable source of truth: + +```text +submitted source BLOBs +release metadata +immutable checksummed .aospkg chunks +``` + +A replica: + +1. Downloads the immutable artifact chunks. +2. Validates total bytes and SHA-256. +3. Writes a replica-scoped temporary `.aospkg`. +4. Mounts it read-only at `/app`. +5. Starts `node /app/main.mjs`. +6. Keeps the temporary package while lazy mount readers may exist. +7. Removes it after the VM is disposed. + +Replicas never run npm, a framework build, or the platform bundler. + +## Release Identity + +The release hash must cover: + +- normalized submitted source; +- selected tenant entrypoint and static root; +- tenant build configuration already used by the platform; +- generated runner semantics; +- App Bundle manifest version; +- Apps builder package version; +- bundler version and material options; +- RivetKit packaging-adapter version. + +Changing packaging semantics must not reuse an artifact built under older +semantics. + +## Security And Limits + +Tenant source, dependencies, build scripts, and bundler inputs remain untrusted. +They execute inside the bounded build VM, not in the trusted app actor process. + +Keep or add explicit limits for: + +- source files and bytes; +- dependency count; +- build duration; +- process count and open file descriptors; +- V8 heap; +- build filesystem bytes; +- bundler input and output bytes; +- emitted module and asset counts; +- individual emitted file size; +- total App Bundle size; +- captured diagnostics and source-map size. + +Threshold warnings and typed errors must identify the configured limit and how +to raise it. Build failures must leave the previous active release unchanged. + +## Acceptance Criteria + +The packaging change is complete when tests prove: + +1. A plain JavaScript HTTP app bundles and serves without runtime + `node_modules`. +2. A TypeScript app runs its build, bundles the output, and reports bounded + compiler diagnostics on failure. +3. A real RivetKit app serves HTTP and DirectActor calls using its emitted WASM + module. +4. RivetKit actor state survives replica replacement because state remains in + Rivet, not the release filesystem. +5. A static website serves HTML, JavaScript, CSS, and binary assets. +6. An imported WASM fixture is emitted as a separate hashed runtime module. +7. The release archive contains only the manifest, bundle outputs, and required + assets. +8. The release archive contains no tenant `node_modules`, source tree, lockfile, + or Apps builder. +9. A cold replica rehydrates the minimal artifact from SQLite and becomes + healthy without npm or network access. +10. Repeating a deployment with the same source and builder version reuses the + same release identity. +11. Changing the builder or manifest version invalidates the release identity. +12. Unsupported computed imports or opaque runtime files fail with a clear + typed error. +13. Artifact size is recorded in tests so the RivetKit fixture cannot silently + regress back to shipping its production dependency tree. + +## References + +- [Cloudflare Wrangler bundling](https://developers.cloudflare.com/workers/wrangler/bundling/) +- [Cloudflare multipart Worker upload metadata](https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/) +- [Cloudflare Workers for Platforms static assets](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/static-assets/) +- [Cloudflare Workers versions and deployments](https://developers.cloudflare.com/workers/versions-and-deployments/) diff --git a/docs/content/docs/background-work.mdx b/docs/content/docs/background-work.mdx index ddb109bc1..8e5b54c63 100644 --- a/docs/content/docs/background-work.mdx +++ b/docs/content/docs/background-work.mdx @@ -6,7 +6,7 @@ description: "Durable jobs and recurring schedules inside an app." ## Workflows Example AI-generated app code that runs durable multi-step jobs that can sleep -and resume. [View the complete workflows example](https://github.com/rivet-dev/agentos/tree/main/examples/apps-workflows). +and resume. [View the complete workflows example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-workflows). @@ -18,5 +18,5 @@ and resume. [View the complete workflows example](https://github.com/rivet-dev/a AI-generated apps can schedule recurring work from an actor. See [Cron Jobs](/agentos/docs/cron). -These capabilities use RivetKit and its ordinary DirectActor client. agentOS +These capabilities use RivetKit and its ordinary DirectActor client. Dynamic Apps does not wrap the client. diff --git a/docs/content/docs/deploy.mdx b/docs/content/docs/deploy.mdx index 8ec99bbc5..91e901a88 100644 --- a/docs/content/docs/deploy.mdx +++ b/docs/content/docs/deploy.mdx @@ -43,7 +43,7 @@ for (let attempt = 0; attempt < 3; attempt++) { ``` A failed build does not replace the currently active release. See the -[AI App Builder example](https://github.com/rivet-dev/agentos/tree/main/examples/apps-ai-builder). +[AI App Builder example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-ai-builder). `appId` must contain 1–63 lowercase letters, numbers, or hyphens. Pass exactly one of `source` or `files`. diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index e744e3604..dccdb163f 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -1,28 +1,28 @@ --- title: "Dynamic Apps" -description: "Deploy user-generated applications in agentOS VMs." +description: "Deploy user-generated applications in isolated VMs." skill: true --- -agentOS Apps runs user-generated HTTP applications on Rivet. Apps can add +Dynamic Apps runs user-generated HTTP applications on Rivet. Apps can add durable SQLite state, workflows, multiplayer and realtime state, queues, and cron jobs. -agentOS Apps is in preview and its API is subject to change. +Dynamic Apps is in preview and its API is subject to change. ## Architecture -**agentOS Apps is a library, not a hosted AI-generated app deployment +**Dynamic Apps is a library, not a hosted AI-generated app deployment platform.** Unlike managed platforms, you can deploy it anywhere and customize the server, routing, authentication, and deployment flow. Requests reach your Hono server, where `appsRouter` routes them to a prewarmed -agentOS VM serving the generated application. Rivet handles request routing and +isolated VM serving the generated application. Rivet handles request routing and orchestrates the pool of prewarmed VMs. - + diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx index fedb30c14..4f1a76ccd 100644 --- a/docs/content/docs/quickstart.mdx +++ b/docs/content/docs/quickstart.mdx @@ -7,14 +7,14 @@ skill: true import { Hosting } from "@/components/docs/Hosting"; -[View the complete Quickstart example on GitHub](https://github.com/rivet-dev/agentos/tree/main/examples/apps-hello-world). +[View the complete Quickstart example on GitHub](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-hello-world). ```sh -npm add @rivet-dev/agentos @rivet-dev/agentos-apps +npm add @rivet-dev/dynamic-apps npm add @hono/node-server hono npm add --save-dev tsx npm pkg set type=module diff --git a/docs/content/docs/realtime.mdx b/docs/content/docs/realtime.mdx index c7cc26b05..cba6ff350 100644 --- a/docs/content/docs/realtime.mdx +++ b/docs/content/docs/realtime.mdx @@ -6,7 +6,7 @@ description: "Share realtime state between every user connected to an app." ## Multiplayer Example AI-generated app code that shares realtime state between clients. -[View the complete multiplayer example](https://github.com/rivet-dev/agentos/tree/main/examples/apps-multiplayer). +[View the complete multiplayer example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-multiplayer). diff --git a/docs/content/docs/reference.mdx b/docs/content/docs/reference.mdx index e504e8209..209662851 100644 --- a/docs/content/docs/reference.mdx +++ b/docs/content/docs/reference.mdx @@ -9,7 +9,7 @@ Give an agent the app requirements, let it generate the project files, and pass those files to `deployApp()`. If the build returns TypeScript diagnostics, give them back to the agent and deploy its repaired files again. -[View the complete AI App Builder example](https://github.com/rivet-dev/agentos/tree/main/examples/apps-ai-builder). +[View the complete AI App Builder example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-ai-builder). ## Planned Improvements diff --git a/docs/content/docs/routing.mdx b/docs/content/docs/routing.mdx index 2b63560b4..15d7852e1 100644 --- a/docs/content/docs/routing.mdx +++ b/docs/content/docs/routing.mdx @@ -14,7 +14,7 @@ This routes `/apps/:appId` and `/apps/:appId/*`. To use an explicit RivetKit client: ```ts -import { createAppsRouter } from "@rivet-dev/agentos-apps/advanced"; +import { createAppsRouter } from "@rivet-dev/dynamic-apps"; server.route("/apps", createAppsRouter({ client })); ``` diff --git a/docs/content/docs/state-and-data.mdx b/docs/content/docs/state-and-data.mdx index 718572ec9..b9886ea93 100644 --- a/docs/content/docs/state-and-data.mdx +++ b/docs/content/docs/state-and-data.mdx @@ -11,7 +11,7 @@ show how another part of your system connects to it. ## SQLite Example AI-generated app code that stores durable data in an actor-owned SQLite -database. [View the complete SQLite example](https://github.com/rivet-dev/agentos/tree/main/examples/apps-sqlite). +database. [View the complete SQLite example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-sqlite). diff --git a/examples/apps-ai-builder/README.md b/examples/apps-ai-builder/README.md new file mode 100644 index 000000000..232a636ba --- /dev/null +++ b/examples/apps-ai-builder/README.md @@ -0,0 +1,20 @@ +# Dynamic Apps: AI App Builder + +This trusted host uses the Vercel AI SDK to generate a bounded three-file +RivetKit application. `deployApp()` runs the real TypeScript build in an +isolated VM. Bounded diagnostics are fed back to the model for at most three +repairs, and a failed build never replaces the previous active release. + +Run the checked workspace example with Node.js 22 or newer: + +```sh +ANTHROPIC_API_KEY=... pnpm --dir examples/apps-ai-builder start +# In another terminal: +curl -X POST http://localhost:3000/deploy/ai-generated-app \ + -H 'content-type: application/json' \ + -d '{"prompt":"Build a collaborative counter"}' +``` + +RivetKit starts its local Engine automatically. Against an existing Rivet +deployment, use the standard Rivet connection variables instead. The successful app is mounted at +`http://localhost:3000/apps/ai-generated-app`. diff --git a/examples/apps-ai-builder/fixtures/app/package.json b/examples/apps-ai-builder/fixtures/app/package.json new file mode 100644 index 000000000..47a0ef06d --- /dev/null +++ b/examples/apps-ai-builder/fixtures/app/package.json @@ -0,0 +1,17 @@ +{ + "name": "generated-rivetkit-app", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "typescript": "5.7.3" + } +} diff --git a/examples/apps-ai-builder/fixtures/app/src/index.ts b/examples/apps-ai-builder/fixtures/app/src/index.ts new file mode 100644 index 000000000..94562d628 --- /dev/null +++ b/examples/apps-ai-builder/fixtures/app/src/index.ts @@ -0,0 +1,23 @@ +import { actor, setup } from "rivetkit"; + +const generatedState = actor({ + state: { requests: 0 }, + actions: { + record(c) { + c.state.requests += 1; + return c.state.requests; + }, + }, +}); + +export const registry = setup({ + use: { generatedState }, +}); + +registry.start(); + +export default function fetch() { + return Response.json({ + message: "Replace this seed with the generated application.", + }); +} diff --git a/examples/apps-ai-builder/fixtures/app/tsconfig.json b/examples/apps-ai-builder/fixtures/app/tsconfig.json new file mode 100644 index 000000000..eb0ab515a --- /dev/null +++ b/examples/apps-ai-builder/fixtures/app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/apps-ai-builder/package.json b/examples/apps-ai-builder/package.json new file mode 100644 index 000000000..d79225f8f --- /dev/null +++ b/examples/apps-ai-builder/package.json @@ -0,0 +1,23 @@ +{ + "name": "@rivet-dev/dynamic-apps-example-ai-builder", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --import tsx src/server.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@ai-sdk/anthropic": "^4.0.19", + "@hono/node-server": "^2.0.11", + "@rivet-dev/dynamic-apps": "workspace:*", + "ai": "^7.0.37", + "hono": "^4.12.9", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/examples/apps-ai-builder/src/actors.ts b/examples/apps-ai-builder/src/actors.ts new file mode 100644 index 000000000..32df578bc --- /dev/null +++ b/examples/apps-ai-builder/src/actors.ts @@ -0,0 +1,10 @@ +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + // These actors manage app deployments and scaling. + ...appsActors, + }, +}); diff --git a/examples/apps-ai-builder/src/server.ts b/examples/apps-ai-builder/src/server.ts new file mode 100644 index 000000000..d0839986c --- /dev/null +++ b/examples/apps-ai-builder/src/server.ts @@ -0,0 +1,132 @@ +import { readFile } from "node:fs/promises"; +import { anthropic } from "@ai-sdk/anthropic"; +import { serve } from "@hono/node-server"; +import { + appsRouter, + DynamicAppsError, + deployApp, +} from "@rivet-dev/dynamic-apps"; +import { generateText } from "ai"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +const editablePaths = [ + "package.json", + "tsconfig.json", + "src/index.ts", +] as const; +const maxRepairs = 3; +const maxFileBytes = 64 * 1024; + +registry.start(); + +async function loadSeed(): Promise> { + const files: Record = {}; + for (const path of editablePaths) { + files[path] = await readFile( + new URL(`../fixtures/app/${path}`, import.meta.url), + "utf8", + ); + } + return files; +} + +function parseFiles(text: string): Record { + const json = text.match(/```json\s*([\s\S]*?)```/)?.[1] ?? text; + const value = JSON.parse(json) as { files?: Record }; + if (!value.files || typeof value.files !== "object") { + throw new TypeError("model response must contain a files object"); + } + const files: Record = {}; + for (const path of editablePaths) { + const content = value.files[path]; + if (typeof content !== "string") { + throw new TypeError(`model response is missing ${path}`); + } + if (Buffer.byteLength(content) > maxFileBytes) { + throw new RangeError(`${path} exceeds ${maxFileBytes} bytes`); + } + files[path] = content; + } + return files; +} + +async function revise( + prompt: string, + files: Record, + diagnostics?: string, +): Promise> { + const result = await generateText({ + model: anthropic(process.env.AI_MODEL ?? "claude-sonnet-4-5"), + maxOutputTokens: 8_000, + prompt: [ + 'Return JSON only as {"files":{"path":"content"}}.', + `You may edit only: ${editablePaths.join(", ")}.`, + "The app must compile, start a RivetKit registry, and export a Web fetch handler.", + `User request: ${prompt}`, + diagnostics ? `Previous build diagnostics:\n${diagnostics}` : "", + `Current files:\n${JSON.stringify(files)}`, + ] + .filter(Boolean) + .join("\n\n"), + }); + return parseFiles(result.text); +} + +async function generateApp(appId: string, prompt: string) { + let files = await revise(prompt, await loadSeed()); + for (let attempt = 0; attempt <= maxRepairs; attempt += 1) { + try { + return await deployApp({ + appId, + files, + }); + } catch (error) { + const appsError = + error instanceof DynamicAppsError || + (typeof error === "object" && + error !== null && + "code" in error && + typeof error.code === "string" && + error.code.startsWith("agentos_apps_")); + if (!appsError || attempt === maxRepairs) { + throw error; + } + const details = error as { + code: string; + message?: string; + metadata?: unknown; + }; + const diagnostics = JSON.stringify({ + code: details.code, + message: details.message ?? String(error), + metadata: details.metadata, + }).slice(0, 16 * 1024); + files = await revise(prompt, files, diagnostics); + } + } + throw new Error("unreachable"); +} + +const server = new Hono(); +// An agent or any other part of the system can call this route. A generic +// deployment endpoint could accept multipart files; this example generates the +// files from a prompt instead. +server.post("/deploy/:name", async (context) => { + const body = await context.req.json<{ prompt?: unknown }>(); + if (typeof body.prompt !== "string" || body.prompt.length > 4_000) { + return context.json( + { error: "prompt must be at most 4,000 characters" }, + 400, + ); + } + return context.json( + await generateApp(context.req.param("name"), body.prompt), + ); +}); +server.route("/apps", appsRouter); + +serve({ + fetch: server.fetch, + port: 3000, +}); diff --git a/examples/apps-ai-builder/tsconfig.json b/examples/apps-ai-builder/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/examples/apps-ai-builder/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/apps-hello-world/README.md b/examples/apps-hello-world/README.md new file mode 100644 index 000000000..ed2376cfc --- /dev/null +++ b/examples/apps-hello-world/README.md @@ -0,0 +1,21 @@ +# Dynamic Apps: Hello World + +This smallest example registers the three infrastructure actors, mounts the +Apps router, and deploys generated files from a separate script. The app runs at +`http://localhost:3000/apps/hello-world/`. + +The uploaded fixture serves an HTML page at `/` and a JSON endpoint at +`/api/hello`. It does not use RivetKit because it has no durable or coordinated +state; the SQLite, workflows, and multiplayer examples add RivetKit while still +serving ordinary HTTP requests. + +Run the checked workspace example with Node.js 22 or newer: + +```sh +pnpm --dir examples/apps-hello-world start +# In another terminal: +pnpm --dir examples/apps-hello-world deploy +``` + +Dynamic Apps starts its local Rivet Engine automatically. Against an existing +Rivet deployment, use the standard Rivet connection variables instead. diff --git a/examples/apps-hello-world/fixtures/app/package.json b/examples/apps-hello-world/fixtures/app/package.json new file mode 100644 index 000000000..29af844f2 --- /dev/null +++ b/examples/apps-hello-world/fixtures/app/package.json @@ -0,0 +1,12 @@ +{ + "name": "hello-world-app", + "version": "0.0.0", + "private": true, + "main": "src/index.ts", + "scripts": { + "check-types": "node --check src/index.mjs" + }, + "dependencies": { + "hono": "^4.12.9" + } +} diff --git a/examples/apps-hello-world/fixtures/app/src/index.ts b/examples/apps-hello-world/fixtures/app/src/index.ts new file mode 100644 index 000000000..142f81096 --- /dev/null +++ b/examples/apps-hello-world/fixtures/app/src/index.ts @@ -0,0 +1,29 @@ +import { Hono } from "hono"; + +const app = new Hono(); + +// Serve the application's frontend. +app.get("/", (c) => { + return c.html(` + + + + + Hello from Dynamic Apps + + +
+

Hello from Dynamic Apps

+

This HTML is served by an HTTP app running inside an isolated VM.

+

Call the JSON API

+
+ +`); +}); + +// Serve a REST API request from the same application. +app.get("/api/hello", (c) => { + return c.json({ message: "Hello from Dynamic Apps" }); +}); + +export default app; diff --git a/examples/apps-hello-world/package.json b/examples/apps-hello-world/package.json new file mode 100644 index 000000000..70e05afa7 --- /dev/null +++ b/examples/apps-hello-world/package.json @@ -0,0 +1,21 @@ +{ + "name": "@rivet-dev/dynamic-apps-example-hello-world", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --import tsx src/server.ts", + "deploy": "node --import tsx src/deploy.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.11", + "@rivet-dev/dynamic-apps": "workspace:*", + "hono": "^4.12.9" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/examples/apps-hello-world/src/actors.ts b/examples/apps-hello-world/src/actors.ts new file mode 100644 index 000000000..32df578bc --- /dev/null +++ b/examples/apps-hello-world/src/actors.ts @@ -0,0 +1,10 @@ +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + // These actors manage app deployments and scaling. + ...appsActors, + }, +}); diff --git a/examples/apps-hello-world/src/deploy.ts b/examples/apps-hello-world/src/deploy.ts new file mode 100644 index 000000000..0798c4d69 --- /dev/null +++ b/examples/apps-hello-world/src/deploy.ts @@ -0,0 +1,32 @@ +import { deployApp } from "@rivet-dev/dynamic-apps"; + +// An agent, upload endpoint, or any other part of the system can call +// deployApp() with the files it generated. +await deployApp({ + appId: "hello-world", + files: { + "package.json": JSON.stringify({ + name: "hello-world-app", + version: "0.0.0", + private: true, + type: "module", + main: "src/index.ts", + dependencies: { + hono: "^4.12.9", + }, + }), + "src/index.ts": ` +import { Hono } from "hono"; + +const app = new Hono(); + +// Serve the application's frontend. +app.get("/", (c) => c.html("

Hello from Dynamic Apps

")); + +// Serve a REST API request from the same application. +app.get("/api/hello", (c) => c.json({ message: "Hello from Dynamic Apps" })); + +export default app; +`, + }, +}); diff --git a/examples/apps-hello-world/src/server.ts b/examples/apps-hello-world/src/server.ts new file mode 100644 index 000000000..1baf6af07 --- /dev/null +++ b/examples/apps-hello-world/src/server.ts @@ -0,0 +1,18 @@ +import { serve } from "@hono/node-server"; +import { appsRouter } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +// Start the actor registry before routing applications. +registry.start(); + +const server = new Hono(); + +// Mount every deployed application at /apps/:appId. +server.route("/apps", appsRouter); + +// Serve the host router over HTTP. +serve({ + fetch: server.fetch, + port: 3000, +}); diff --git a/examples/apps-hello-world/tsconfig.json b/examples/apps-hello-world/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/examples/apps-hello-world/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/apps-multiplayer/README.md b/examples/apps-multiplayer/README.md new file mode 100644 index 000000000..e85b98055 --- /dev/null +++ b/examples/apps-multiplayer/README.md @@ -0,0 +1,19 @@ +# Dynamic Apps: Multiplayer + +The deployed server defines keyed room actors and an HTTP handler. The separate +`src/client.ts` deploys it, connects with the returned namespace and pool, then +joins and moves through DirectActor. + +Run the checked workspace example with Node.js 22 or newer: + +```sh +pnpm --dir examples/apps-multiplayer start +# In another terminal: +pnpm --dir examples/apps-multiplayer client +``` + +RivetKit starts its local Engine automatically. Against an existing Rivet +deployment, use the standard Rivet connection variables instead. + +The deployed HTTP handler is available at +`http://localhost:3000/apps/multiplayer-room`. diff --git a/examples/apps-multiplayer/fixtures/app/package.json b/examples/apps-multiplayer/fixtures/app/package.json new file mode 100644 index 000000000..60f797452 --- /dev/null +++ b/examples/apps-multiplayer/fixtures/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "multiplayer-room-app", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "src/index.ts", + "scripts": { + "check-types": "node --check src/index.mjs" + }, + "dependencies": { + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + } +} diff --git a/examples/apps-multiplayer/fixtures/app/src/index.ts b/examples/apps-multiplayer/fixtures/app/src/index.ts new file mode 100644 index 000000000..213a218b2 --- /dev/null +++ b/examples/apps-multiplayer/fixtures/app/src/index.ts @@ -0,0 +1,40 @@ +import { actor, event, setup } from "rivetkit"; + +type Position = { x: number; y: number }; + +const room = actor({ + state: { + players: {} as Record, + }, + events: { + changed: event(), + }, + actions: { + join(c, player: string) { + c.state.players[player] ??= { x: 0, y: 0 }; + c.broadcast("changed", c.state.players); + return c.state.players; + }, + move(c, player: string, x: number, y: number) { + c.state.players[player] = { x, y }; + c.broadcast("changed", c.state.players); + return c.state.players; + }, + inspect(c) { + return c.state.players; + }, + }, +}); + +export const registry = setup({ + use: { room }, +}); + +registry.start(); + +export default function fetch() { + return Response.json({ + app: "multiplayer-room", + message: "Use the RivetKit client to join and move in a room.", + }); +} diff --git a/examples/apps-multiplayer/package.json b/examples/apps-multiplayer/package.json new file mode 100644 index 000000000..a5d07a09b --- /dev/null +++ b/examples/apps-multiplayer/package.json @@ -0,0 +1,22 @@ +{ + "name": "@rivet-dev/dynamic-apps-example-multiplayer", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --import tsx src/server.ts", + "client": "node --import tsx src/client.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.11", + "@rivet-dev/dynamic-apps": "workspace:*", + "hono": "^4.12.9", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/examples/apps-multiplayer/src/actors.ts b/examples/apps-multiplayer/src/actors.ts new file mode 100644 index 000000000..32df578bc --- /dev/null +++ b/examples/apps-multiplayer/src/actors.ts @@ -0,0 +1,10 @@ +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + // These actors manage app deployments and scaling. + ...appsActors, + }, +}); diff --git a/examples/apps-multiplayer/src/client.ts b/examples/apps-multiplayer/src/client.ts new file mode 100644 index 000000000..250bb8e1e --- /dev/null +++ b/examples/apps-multiplayer/src/client.ts @@ -0,0 +1,26 @@ +import type { Deployment } from "@rivet-dev/dynamic-apps"; +import { createClient } from "rivetkit/client"; +import type { registry as appRegistry } from "../fixtures/app/src/index.js"; + +const response = await fetch("http://localhost:3000/deploy/multiplayer-room", { + method: "POST", +}); +if (!response.ok) { + throw new Error( + `deployment failed: ${response.status} ${await response.text()}`, + ); +} +const deployment = (await response.json()) as Deployment; + +const client = createClient({ + namespace: deployment.namespace, + poolName: deployment.pool, +}); + +try { + const room = client.room.getOrCreate(["lobby"]); + await room.join("alice"); + console.log(await room.move("alice", 4, 8)); +} finally { + await client.dispose(); +} diff --git a/examples/apps-multiplayer/src/server.ts b/examples/apps-multiplayer/src/server.ts new file mode 100644 index 000000000..57b50429f --- /dev/null +++ b/examples/apps-multiplayer/src/server.ts @@ -0,0 +1,26 @@ +import { serve } from "@hono/node-server"; +import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +registry.start(); + +const server = new Hono(); + +// In production, an agent or any other part of the system can upload app files +// here as multipart form data. This static example deploys its checked fixture. +server.post("/deploy/:name", async (context) => { + return context.json( + await deployApp({ + appId: context.req.param("name"), + source: new URL("../fixtures/app/", import.meta.url), + }), + ); +}); + +server.route("/apps", appsRouter); + +serve({ + fetch: server.fetch, + port: 3000, +}); diff --git a/examples/apps-multiplayer/tsconfig.json b/examples/apps-multiplayer/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/examples/apps-multiplayer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/apps-sqlite/README.md b/examples/apps-sqlite/README.md new file mode 100644 index 000000000..2b7f41fca --- /dev/null +++ b/examples/apps-sqlite/README.md @@ -0,0 +1,19 @@ +# Dynamic Apps: SQLite + +The deployed server defines a Rivet Actor backed by SQLite and still serves +HTTP. The separate `src/client.ts` deploys it, connects with the returned +namespace and pool, and adds and lists notes through DirectActor. + +Run the checked workspace example with Node.js 22 or newer: + +```sh +pnpm --dir examples/apps-sqlite start +# In another terminal: +pnpm --dir examples/apps-sqlite client +``` + +RivetKit starts its local Engine automatically. Against an existing Rivet +deployment, use the standard Rivet connection variables instead. + +The deployed HTTP handler is available at +`http://localhost:3000/apps/sqlite-notes`. diff --git a/examples/apps-sqlite/fixtures/app/package.json b/examples/apps-sqlite/fixtures/app/package.json new file mode 100644 index 000000000..6073f589f --- /dev/null +++ b/examples/apps-sqlite/fixtures/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "sqlite-notes-app", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "src/index.ts", + "scripts": { + "check-types": "node --check src/index.mjs" + }, + "dependencies": { + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + } +} diff --git a/examples/apps-sqlite/fixtures/app/src/index.ts b/examples/apps-sqlite/fixtures/app/src/index.ts new file mode 100644 index 000000000..7970a300e --- /dev/null +++ b/examples/apps-sqlite/fixtures/app/src/index.ts @@ -0,0 +1,36 @@ +import { actor, setup } from "rivetkit"; +import { db } from "rivetkit/db"; + +const notes = actor({ + db: db({ + async onMigrate(database) { + await database.execute(` + CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + body TEXT NOT NULL + ) + `); + }, + }), + actions: { + async add(c, body: string) { + await c.db.execute("INSERT INTO notes (body) VALUES (?)", body); + }, + async list(c) { + return c.db.execute("SELECT id, body FROM notes ORDER BY id"); + }, + }, +}); + +export const registry = setup({ + use: { notes }, +}); + +registry.start(); + +export default function fetch() { + return Response.json({ + app: "sqlite-notes", + message: "Use the RivetKit client to add and list notes.", + }); +} diff --git a/examples/apps-sqlite/package.json b/examples/apps-sqlite/package.json new file mode 100644 index 000000000..ab9e822aa --- /dev/null +++ b/examples/apps-sqlite/package.json @@ -0,0 +1,22 @@ +{ + "name": "@rivet-dev/dynamic-apps-example-sqlite", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --import tsx src/server.ts", + "client": "node --import tsx src/client.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.11", + "@rivet-dev/dynamic-apps": "workspace:*", + "hono": "^4.12.9", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/examples/apps-sqlite/src/actors.ts b/examples/apps-sqlite/src/actors.ts new file mode 100644 index 000000000..32df578bc --- /dev/null +++ b/examples/apps-sqlite/src/actors.ts @@ -0,0 +1,10 @@ +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + // These actors manage app deployments and scaling. + ...appsActors, + }, +}); diff --git a/examples/apps-sqlite/src/client.ts b/examples/apps-sqlite/src/client.ts new file mode 100644 index 000000000..aa0474e8a --- /dev/null +++ b/examples/apps-sqlite/src/client.ts @@ -0,0 +1,26 @@ +import type { Deployment } from "@rivet-dev/dynamic-apps"; +import { createClient } from "rivetkit/client"; +import type { registry as appRegistry } from "../fixtures/app/src/index.js"; + +const response = await fetch("http://localhost:3000/deploy/sqlite-notes", { + method: "POST", +}); +if (!response.ok) { + throw new Error( + `deployment failed: ${response.status} ${await response.text()}`, + ); +} +const deployment = (await response.json()) as Deployment; + +const client = createClient({ + namespace: deployment.namespace, + poolName: deployment.pool, +}); + +try { + const notes = client.notes.getOrCreate(["shared"]); + await notes.add("Hello from the RivetKit client"); + console.log(await notes.list()); +} finally { + await client.dispose(); +} diff --git a/examples/apps-sqlite/src/server.ts b/examples/apps-sqlite/src/server.ts new file mode 100644 index 000000000..57b50429f --- /dev/null +++ b/examples/apps-sqlite/src/server.ts @@ -0,0 +1,26 @@ +import { serve } from "@hono/node-server"; +import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +registry.start(); + +const server = new Hono(); + +// In production, an agent or any other part of the system can upload app files +// here as multipart form data. This static example deploys its checked fixture. +server.post("/deploy/:name", async (context) => { + return context.json( + await deployApp({ + appId: context.req.param("name"), + source: new URL("../fixtures/app/", import.meta.url), + }), + ); +}); + +server.route("/apps", appsRouter); + +serve({ + fetch: server.fetch, + port: 3000, +}); diff --git a/examples/apps-sqlite/tsconfig.json b/examples/apps-sqlite/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/examples/apps-sqlite/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/apps-static-website/README.md b/examples/apps-static-website/README.md new file mode 100644 index 000000000..12b9c9bb2 --- /dev/null +++ b/examples/apps-static-website/README.md @@ -0,0 +1,18 @@ +# Dynamic Apps: Static Website + +A directory with `index.html` and no `package.json` is served directly. CSS, +JavaScript, SVG, and other byte assets are included in the immutable release. +A package with a build script is treated as a built static site when it emits +`dist/index.html`. + +Run the checked workspace example with Node.js 22 or newer: + +```sh +pnpm --dir examples/apps-static-website start +# In another terminal: +curl -X POST http://localhost:3000/deploy/static-website +``` + +RivetKit starts its local Engine automatically. Against an existing Rivet +deployment, use the standard Rivet connection variables instead. Open +`http://localhost:3000/apps/static-website/`. diff --git a/examples/apps-static-website/fixtures/app/app.js b/examples/apps-static-website/fixtures/app/app.js new file mode 100644 index 000000000..c811d41d2 --- /dev/null +++ b/examples/apps-static-website/fixtures/app/app.js @@ -0,0 +1,2 @@ +document.querySelector("#status").textContent = + "Served from a warm Dynamic Apps execution replica."; diff --git a/examples/apps-static-website/fixtures/app/index.html b/examples/apps-static-website/fixtures/app/index.html new file mode 100644 index 000000000..811e8b0c2 --- /dev/null +++ b/examples/apps-static-website/fixtures/app/index.html @@ -0,0 +1,15 @@ + + + + + + Dynamic Apps + + + + +

Static sites scale to zero too.

+

Loading JavaScript…

+ + + diff --git a/examples/apps-static-website/fixtures/app/logo.svg b/examples/apps-static-website/fixtures/app/logo.svg new file mode 100644 index 000000000..0cef99460 --- /dev/null +++ b/examples/apps-static-website/fixtures/app/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/apps-static-website/fixtures/app/styles.css b/examples/apps-static-website/fixtures/app/styles.css new file mode 100644 index 000000000..c9acfaa9e --- /dev/null +++ b/examples/apps-static-website/fixtures/app/styles.css @@ -0,0 +1,10 @@ +:root { + color-scheme: dark; + font: 18px/1.5 system-ui, sans-serif; +} + +body { + max-width: 42rem; + margin: 6rem auto; + padding: 0 1.5rem; +} diff --git a/examples/apps-static-website/package.json b/examples/apps-static-website/package.json new file mode 100644 index 000000000..c41f0f3df --- /dev/null +++ b/examples/apps-static-website/package.json @@ -0,0 +1,21 @@ +{ + "name": "@rivet-dev/dynamic-apps-example-static-website", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --import tsx src/server.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.11", + "@rivet-dev/dynamic-apps": "workspace:*", + "hono": "^4.12.9", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/examples/apps-static-website/src/actors.ts b/examples/apps-static-website/src/actors.ts new file mode 100644 index 000000000..32df578bc --- /dev/null +++ b/examples/apps-static-website/src/actors.ts @@ -0,0 +1,10 @@ +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + // These actors manage app deployments and scaling. + ...appsActors, + }, +}); diff --git a/examples/apps-static-website/src/server.ts b/examples/apps-static-website/src/server.ts new file mode 100644 index 000000000..57b50429f --- /dev/null +++ b/examples/apps-static-website/src/server.ts @@ -0,0 +1,26 @@ +import { serve } from "@hono/node-server"; +import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +registry.start(); + +const server = new Hono(); + +// In production, an agent or any other part of the system can upload app files +// here as multipart form data. This static example deploys its checked fixture. +server.post("/deploy/:name", async (context) => { + return context.json( + await deployApp({ + appId: context.req.param("name"), + source: new URL("../fixtures/app/", import.meta.url), + }), + ); +}); + +server.route("/apps", appsRouter); + +serve({ + fetch: server.fetch, + port: 3000, +}); diff --git a/examples/apps-static-website/tsconfig.json b/examples/apps-static-website/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/examples/apps-static-website/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/apps-workflows/README.md b/examples/apps-workflows/README.md new file mode 100644 index 000000000..ac2134903 --- /dev/null +++ b/examples/apps-workflows/README.md @@ -0,0 +1,18 @@ +# Dynamic Apps: Workflows + +The deployed server defines a keyed `job` actor and an HTTP handler. The +separate `src/client.ts` deploys it, connects with the returned namespace and +pool, and creates a durable workflow through DirectActor. + +Run the checked workspace example with Node.js 22 or newer: + +```sh +pnpm --dir examples/apps-workflows start +# In another terminal: +pnpm --dir examples/apps-workflows client +``` + +RivetKit starts its local Engine automatically. Against an existing Rivet +deployment, use the standard Rivet connection variables instead. The deployed +HTTP handler is available at +`http://localhost:3000/apps/durable-workflow`. diff --git a/examples/apps-workflows/fixtures/app/package.json b/examples/apps-workflows/fixtures/app/package.json new file mode 100644 index 000000000..5f6f3019d --- /dev/null +++ b/examples/apps-workflows/fixtures/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "durable-workflow-app", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "src/index.ts", + "scripts": { + "check-types": "node --check src/index.mjs" + }, + "dependencies": { + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + } +} diff --git a/examples/apps-workflows/fixtures/app/src/index.ts b/examples/apps-workflows/fixtures/app/src/index.ts new file mode 100644 index 000000000..28553b141 --- /dev/null +++ b/examples/apps-workflows/fixtures/app/src/index.ts @@ -0,0 +1,37 @@ +import { actor, setup } from "rivetkit"; +import { workflow } from "rivetkit/workflow"; + +const job = actor({ + state: { + id: "", + status: "queued" as "queued" | "running" | "complete", + }, + onCreate(c) { + c.state.id = c.key[0] ?? ""; + }, + actions: { + inspect: (c) => c.state, + }, + run: workflow(async (workflowContext) => { + await workflowContext.step("start", async (c) => { + c.state.status = "running"; + }); + await workflowContext.sleep("work", 1_000); + await workflowContext.step("finish", async (c) => { + c.state.status = "complete"; + }); + }), +}); + +export const registry = setup({ + use: { job }, +}); + +registry.start(); + +export default function fetch() { + return Response.json({ + app: "durable-workflow", + message: "Use the RivetKit client to create and inspect jobs.", + }); +} diff --git a/examples/apps-workflows/package.json b/examples/apps-workflows/package.json new file mode 100644 index 000000000..e6e4b81a2 --- /dev/null +++ b/examples/apps-workflows/package.json @@ -0,0 +1,22 @@ +{ + "name": "@rivet-dev/dynamic-apps-example-workflows", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --import tsx src/server.ts", + "client": "node --import tsx src/client.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.11", + "@rivet-dev/dynamic-apps": "workspace:*", + "hono": "^4.12.9", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/examples/apps-workflows/src/actors.ts b/examples/apps-workflows/src/actors.ts new file mode 100644 index 000000000..32df578bc --- /dev/null +++ b/examples/apps-workflows/src/actors.ts @@ -0,0 +1,10 @@ +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + // These actors manage app deployments and scaling. + ...appsActors, + }, +}); diff --git a/examples/apps-workflows/src/client.ts b/examples/apps-workflows/src/client.ts new file mode 100644 index 000000000..23ccedeab --- /dev/null +++ b/examples/apps-workflows/src/client.ts @@ -0,0 +1,25 @@ +import type { Deployment } from "@rivet-dev/dynamic-apps"; +import { createClient } from "rivetkit/client"; +import type { registry as appRegistry } from "../fixtures/app/src/index.js"; + +const response = await fetch("http://localhost:3000/deploy/durable-workflow", { + method: "POST", +}); +if (!response.ok) { + throw new Error( + `deployment failed: ${response.status} ${await response.text()}`, + ); +} +const deployment = (await response.json()) as Deployment; + +const client = createClient({ + namespace: deployment.namespace, + poolName: deployment.pool, +}); + +try { + const job = client.job.getOrCreate(["example-job"]); + console.log(await job.inspect()); +} finally { + await client.dispose(); +} diff --git a/examples/apps-workflows/src/server.ts b/examples/apps-workflows/src/server.ts new file mode 100644 index 000000000..57b50429f --- /dev/null +++ b/examples/apps-workflows/src/server.ts @@ -0,0 +1,26 @@ +import { serve } from "@hono/node-server"; +import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +registry.start(); + +const server = new Hono(); + +// In production, an agent or any other part of the system can upload app files +// here as multipart form data. This static example deploys its checked fixture. +server.post("/deploy/:name", async (context) => { + return context.json( + await deployApp({ + appId: context.req.param("name"), + source: new URL("../fixtures/app/", import.meta.url), + }), + ); +}); + +server.route("/apps", appsRouter); + +serve({ + fetch: server.fetch, + port: 3000, +}); diff --git a/examples/apps-workflows/tsconfig.json b/examples/apps-workflows/tsconfig.json new file mode 100644 index 000000000..05028f5c8 --- /dev/null +++ b/examples/apps-workflows/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..49f47e532 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@rivet-dev/dynamic-apps-workspace", + "private": true, + "packageManager": "pnpm@10.13.1", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "build": "pnpm --filter @rivet-dev/dynamic-apps-builder build && pnpm --filter @rivet-dev/dynamic-apps build", + "check-types": "pnpm -r --if-present check-types", + "test": "pnpm --filter @rivet-dev/dynamic-apps-builder test && pnpm --filter @rivet-dev/dynamic-apps test && pnpm --filter @rivet-dev/dynamic-apps-benchmarks test", + "lint": "pnpm biome check .", + "fmt": "pnpm biome check --write --diagnostic-level=error .", + "check-boundaries": "node scripts/check-boundaries.mjs", + "test:packed": "node scripts/test-packed.mjs" + }, + "devDependencies": { + "@biomejs/biome": "2.4.10", + "@types/node": "^22.19.15", + "tsx": "^4.21.0", + "typescript": "^5.9.2" + } +} diff --git a/packages/dynamic-apps-builder/agentos-package.json b/packages/dynamic-apps-builder/agentos-package.json new file mode 100644 index 000000000..5401181b9 --- /dev/null +++ b/packages/dynamic-apps-builder/agentos-package.json @@ -0,0 +1,3 @@ +{ + "name": "apps-builder" +} diff --git a/packages/dynamic-apps-builder/cli/apps-builder.mjs b/packages/dynamic-apps-builder/cli/apps-builder.mjs new file mode 100755 index 000000000..dee0ae57b --- /dev/null +++ b/packages/dynamic-apps-builder/cli/apps-builder.mjs @@ -0,0 +1,553 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { + copyFile, + cp, + mkdir, + readFile, + readdir, + rename, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import { builtinModules, createRequire } from "node:module"; +import { dirname, extname, join, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import * as esbuild from "esbuild-wasm/esm/browser.js"; + +const MANIFEST_VERSION = 1; +const MAX_DIAGNOSTIC_BYTES = 2 * 1024 * 1024; +const OPTIONAL_RUNTIME_MODULES = new Set([ + "bufferutil", + "cbor-extract", + "utf-8-validate", + "ws", +]); + +const configPath = process.argv[2]; +if (!configPath) { + throw new Error("usage: apps-builder "); +} + +const config = JSON.parse(await readFile(configPath, "utf8")); +const workspace = resolve(config.workspace); +const release = resolve(config.release); +const entrypoint = resolve(workspace, config.entrypoint); +const maxOutputBytes = positiveInteger(config.maxOutputBytes, "maxOutputBytes"); +const maxOutputFiles = positiveInteger(config.maxOutputFiles, "maxOutputFiles"); +const maxFileBytes = positiveInteger(config.maxFileBytes, "maxFileBytes"); + +await rm(release, { recursive: true, force: true }); +await mkdir(join(release, "modules"), { recursive: true }); + +const define = { + "process.env.NODE_ENV": JSON.stringify("production"), +}; +const require = createRequire(pathToFileURL(entrypoint)); +const builderRequire = createRequire(import.meta.url); +if (config.usesRivetKit) { + const wasmSource = require.resolve( + "@rivetkit/rivetkit-wasm/rivetkit_wasm_bg.wasm", + ); + const wasmBytes = await readFile(wasmSource); + const hash = sha256(wasmBytes); + const wasmName = `rivetkit-${hash.slice(0, 16)}.wasm`; + await copyFile(wasmSource, join(release, "modules", wasmName)); + define.__AGENTOS_RIVETKIT_WASM_PATH__ = JSON.stringify( + `./modules/${wasmName}`, + ); +} + +// The Node entrypoint for esbuild-wasm starts a child process. agentOS supports +// child processes, but the bundler does not need that extra transport layer. +// Run the browser service in-process and resolve files through this bounded +// platform-owned plugin instead. +globalThis.self ??= globalThis; +const esbuildWasm = await readFile( + builderRequire.resolve("esbuild-wasm/esbuild.wasm"), +); +await esbuild.initialize({ + wasmModule: new WebAssembly.Module(esbuildWasm), + worker: false, +}); +const build = await esbuild.build({ + entryPoints: [entrypoint], + outfile: join(release, "main.mjs"), + bundle: true, + format: "esm", + platform: "node", + target: "node22", + banner: { + js: 'import { createRequire as __agentOSCreateRequire } from "node:module"; const require = __agentOSCreateRequire(import.meta.url);', + }, + treeShaking: true, + minify: true, + sourcemap: "external", + // esbuild-wasm's in-process browser service attempts to JSON-decode an empty + // metafile before reporting build errors. The release manifest below carries + // the production provenance and hashes we need. + metafile: false, + write: false, + logLevel: "silent", + define, + assetNames: "modules/[name]-[hash]", + loader: { + ".wasm": "file", + ".bin": "file", + ".sql": "text", + ".txt": "text", + }, + plugins: [nodeFileSystemPlugin()], +}); +await esbuild.stop(); + +for (const output of build.outputFiles ?? []) { + const outputPath = resolve(output.path); + if ( + outputPath !== join(release, "main.mjs") && + !outputPath.startsWith(`${release}/`) + ) { + throw new Error(`App Bundle output escapes release root: ${output.path}`); + } + await mkdir(dirname(outputPath), { recursive: true }); + await writeFile(outputPath, output.contents); +} + +const unsupported = build.warnings.filter((warning) => + /dynamic import|not be bundled|unsupported/i.test(warning.text), +); +if (unsupported.length > 0) { + throw new Error( + `unsupported non-analyzable import: ${formatMessages(unsupported)}`, + ); +} + +const sourceMap = join(release, "main.mjs.map"); +await rename(sourceMap, `${configPath}.map`).catch(async (error) => { + if (error?.code !== "ENOENT") throw error; +}); + +const staticRoot = + config.staticRoot ?? + ((await stat(join(workspace, "public")).catch(() => null))?.isDirectory() + ? "public" + : undefined); +if (staticRoot === ".") { + for (const sourcePath of config.sourceFiles ?? []) { + const source = resolve(workspace, sourcePath); + const target = resolve(join(release, "public"), sourcePath); + if ( + !source.startsWith(`${workspace}/`) || + !target.startsWith(`${join(release, "public")}/`) + ) { + throw new Error(`static source path escapes its root: ${sourcePath}`); + } + const info = await stat(source); + await mkdir(dirname(target), { recursive: true }); + if (info.isDirectory()) { + await cp(source, target, { recursive: true, force: false }); + } else if (info.isFile()) { + await copyFile(source, target); + } + } +} else if (staticRoot) { + const staticSource = resolve(workspace, staticRoot); + const staticInfo = await stat(staticSource); + if (!staticInfo.isDirectory()) { + throw new Error(`static root is not a directory: ${staticRoot}`); + } + await cp(staticSource, join(release, "public"), { + recursive: true, + force: false, + }); +} + +const entries = await walkRelease(release); +if (entries.length > maxOutputFiles) { + throw new Error( + `App Bundle emitted ${entries.length} files, limit is maxOutputFiles ${maxOutputFiles}`, + ); +} +let totalBytes = 0; +for (const entry of entries) { + if (entry.size > maxFileBytes) { + throw new Error( + `App Bundle file ${entry.path} is ${entry.size} bytes, limit is maxFileBytes ${maxFileBytes}`, + ); + } + totalBytes += entry.size; +} +if (totalBytes > maxOutputBytes) { + throw new Error( + `App Bundle is ${totalBytes} bytes, limit is maxOutputBytes ${maxOutputBytes}`, + ); +} + +const manifest = { + version: MANIFEST_VERSION, + mainModule: "main.mjs", + modules: entries + .filter((entry) => !entry.path.startsWith("public/")) + .map((entry) => ({ + path: entry.path, + type: entry.path.endsWith(".wasm") + ? "wasm" + : entry.path.endsWith(".mjs") || entry.path.endsWith(".js") + ? "esm" + : "data", + size: entry.size, + hash: entry.hash, + })), + assets: entries + .filter((entry) => entry.path.startsWith("public/")) + .map((entry) => ({ + path: entry.path, + size: entry.size, + hash: entry.hash, + })), +}; +await writeFile( + join(release, "manifest.json"), + `${JSON.stringify(manifest, null, 2)}\n`, +); +await writeFile( + join(release, "agentos-package.json"), + `${JSON.stringify({ + name: "agentos-app", + version: String(config.version), + })}\n`, +); + +function positiveInteger(value, name) { + if (!Number.isSafeInteger(value) || value < 1) { + throw new TypeError(`${name} must be a positive safe integer`); + } + return value; +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex"); +} + +async function walkRelease(root) { + const files = []; + const walk = async (directory) => { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await walk(path); + continue; + } + if (!entry.isFile()) { + throw new Error(`App Bundle contains unsupported entry: ${path}`); + } + const bytes = await readFile(path); + files.push({ + path: relative(root, path).split("\\").join("/"), + size: bytes.byteLength, + hash: sha256(bytes), + }); + } + }; + await walk(root); + return files.sort((a, b) => a.path.localeCompare(b.path)); +} + +function formatMessages(messages) { + return messages + .map((message) => message.text) + .join("\n") + .slice(0, MAX_DIAGNOSTIC_BYTES); +} + +function nodeFileSystemPlugin() { + const builtins = new Set([ + ...builtinModules, + ...builtinModules.map((name) => `node:${name}`), + ]); + return { + name: "agentos-node-filesystem", + setup(build) { + build.onResolve({ filter: /.*/ }, async (args) => { + if (builtins.has(args.path)) { + return { path: args.path, external: true }; + } + const importer = + args.kind === "entry-point" || !args.importer + ? entrypoint + : args.importer; + try { + if ( + args.kind === "import-statement" || + args.kind === "dynamic-import" + ) { + return { + path: await resolveEsmImport(args.path, importer), + }; + } + const resolver = createRequire(pathToFileURL(importer)); + return { path: resolver.resolve(args.path) }; + } catch (error) { + if (OPTIONAL_RUNTIME_MODULES.has(args.path)) { + return { path: args.path, external: true }; + } + return { + errors: [ + { + text: `could not resolve ${JSON.stringify(args.path)} from ${importer}: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + }); + build.onLoad({ filter: /.*/ }, async (args) => { + const extension = extname(args.path).toLowerCase(); + if (extension === ".node") { + return { + errors: [ + { + text: `native Node addon is unsupported in Dynamic Apps: ${args.path}`, + }, + ], + }; + } + let contents = await readFile(args.path); + if ( + config.usesRivetKit && + (extension === ".js" || + extension === ".mjs" || + extension === ".ts") + ) { + // RivetKit deliberately hides this optional dependency from + // ordinary bundlers. Apps always select the WASM runtime, so + // make that one runtime edge statically analyzable while + // leaving the native and engine-CLI fallbacks unreachable. + const source = contents.toString("utf8"); + contents = Buffer.from( + source.replaceAll( + 'import(["@rivetkit", "rivetkit-wasm"].join("/"))', + 'import("@rivetkit/rivetkit-wasm")', + ), + ); + } + return { + contents, + loader: + extension === ".json" + ? "json" + : extension === ".ts" || extension === ".cts" + ? "ts" + : extension === ".tsx" + ? "tsx" + : extension === ".jsx" + ? "jsx" + : extension === ".css" + ? "css" + : extension === ".sql" || extension === ".txt" + ? "text" + : extension === ".wasm" || extension === ".bin" + ? "file" + : "js", + }; + }); + }, + }; +} + +async function resolveEsmImport(specifier, importer) { + if (specifier.startsWith("file:")) return fileURLToPath(specifier); + if ( + specifier.startsWith("./") || + specifier.startsWith("../") || + specifier.startsWith("/") + ) { + return resolveModuleFile( + specifier.startsWith("/") + ? specifier + : resolve(dirname(importer), specifier), + ); + } + if (specifier.startsWith("#")) { + return resolvePackageImport(specifier, importer); + } + + const parts = specifier.split("/"); + const packageName = specifier.startsWith("@") + ? parts.slice(0, 2).join("/") + : parts[0]; + const packageSubpath = parts.slice(packageName.startsWith("@") ? 2 : 1); + let directory = dirname(importer); + for (;;) { + const packageRoot = join(directory, "node_modules", packageName); + const packageJsonPath = join(packageRoot, "package.json"); + const packageJsonText = await readFile(packageJsonPath, "utf8").catch( + (error) => { + if (error?.code === "ENOENT") return undefined; + throw error; + }, + ); + if (packageJsonText !== undefined) { + const packageJson = JSON.parse(packageJsonText); + const subpath = packageSubpath.length + ? `./${packageSubpath.join("/")}` + : "."; + const exported = selectPackageExport(packageJson.exports, subpath); + if (exported) return resolveModuleFile(resolve(packageRoot, exported)); + const fallback = packageSubpath.length + ? join(packageRoot, ...packageSubpath) + : resolve( + packageRoot, + typeof packageJson.module === "string" + ? packageJson.module + : typeof packageJson.main === "string" + ? packageJson.main + : "index.js", + ); + return resolveModuleFile(fallback); + } + const parent = dirname(directory); + if (parent === directory) break; + directory = parent; + } + throw new Error( + `Cannot find package ${JSON.stringify(packageName)} imported from ${importer}`, + ); +} + +async function resolvePackageImport(specifier, importer) { + let directory = dirname(importer); + for (;;) { + const packageJsonPath = join(directory, "package.json"); + const packageJsonText = await readFile(packageJsonPath, "utf8").catch( + (error) => { + if (error?.code === "ENOENT") return undefined; + throw error; + }, + ); + if (packageJsonText !== undefined) { + const packageJson = JSON.parse(packageJsonText); + const imports = packageJson.imports; + if (imports && typeof imports === "object" && !Array.isArray(imports)) { + const exact = imports[specifier]; + if (exact !== undefined) { + const selected = selectConditionalExport(exact); + if (selected) return resolveModuleFile(resolve(directory, selected)); + } + for (const [key, value] of Object.entries(imports)) { + const star = key.indexOf("*"); + if ( + star < 0 || + !specifier.startsWith(key.slice(0, star)) || + !specifier.endsWith(key.slice(star + 1)) + ) { + continue; + } + const matched = specifier.slice( + star, + specifier.length - (key.length - star - 1), + ); + const selected = selectConditionalExport(value); + if (selected) { + return resolveModuleFile( + resolve(directory, selected.replaceAll("*", matched)), + ); + } + } + } + } + const parent = dirname(directory); + if (parent === directory) break; + directory = parent; + } + throw new Error( + `Cannot resolve package import ${JSON.stringify(specifier)} from ${importer}`, + ); +} + +function selectPackageExport(exportsValue, subpath) { + if (typeof exportsValue === "string") { + return subpath === "." ? exportsValue : undefined; + } + if (Array.isArray(exportsValue)) { + for (const value of exportsValue) { + const selected = selectPackageExport(value, subpath); + if (selected) return selected; + } + return undefined; + } + if ( + !exportsValue || + typeof exportsValue !== "object" + ) { + return undefined; + } + const entries = Object.entries(exportsValue); + if (entries.some(([key]) => key.startsWith("."))) { + const exact = exportsValue[subpath]; + if (exact !== undefined) return selectConditionalExport(exact); + for (const [key, value] of entries) { + const star = key.indexOf("*"); + if ( + star < 0 || + !subpath.startsWith(key.slice(0, star)) || + !subpath.endsWith(key.slice(star + 1)) + ) { + continue; + } + const matched = subpath.slice(star, subpath.length - (key.length - star - 1)); + const selected = selectConditionalExport(value); + return selected?.replaceAll("*", matched); + } + return undefined; + } + return subpath === "." ? selectConditionalExport(exportsValue) : undefined; +} + +function selectConditionalExport(value) { + if (typeof value === "string") return value; + if (Array.isArray(value)) { + for (const entry of value) { + const selected = selectConditionalExport(entry); + if (selected) return selected; + } + return undefined; + } + if (!value || typeof value !== "object") return undefined; + for (const [condition, target] of Object.entries(value)) { + if ( + condition === "import" || + condition === "node" || + condition === "production" || + condition === "default" + ) { + const selected = selectConditionalExport(target); + if (selected) return selected; + } + } + return undefined; +} + +async function resolveModuleFile(candidate) { + const candidates = extname(candidate) + ? [candidate] + : [ + candidate, + `${candidate}.mjs`, + `${candidate}.js`, + `${candidate}.ts`, + `${candidate}.tsx`, + join(candidate, "index.mjs"), + join(candidate, "index.js"), + join(candidate, "index.ts"), + ]; + for (const path of candidates) { + const info = await stat(path).catch((error) => { + if (error?.code === "ENOENT") return undefined; + throw error; + }); + if (info?.isFile()) return path; + } + throw new Error(`Cannot resolve module file ${candidate}`); +} diff --git a/packages/dynamic-apps-builder/package.json b/packages/dynamic-apps-builder/package.json new file mode 100644 index 000000000..8396ea922 --- /dev/null +++ b/packages/dynamic-apps-builder/package.json @@ -0,0 +1,40 @@ +{ + "name": "@rivet-dev/dynamic-apps-builder", + "version": "0.2.15", + "type": "module", + "license": "Apache-2.0", + "description": "Platform-owned Dynamic Apps release bundler", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "apps-builder": "./cli/apps-builder.mjs" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "cli", + "dist", + "agentos-package.json", + "!dist/package", + "!dist/package.tar" + ], + "scripts": { + "build": "tsc && rm -f dist/package.aospkg && agentos-toolchain pack . --out dist/package --prune-native", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "dependencies": { + "esbuild-wasm": "0.27.4" + }, + "devDependencies": { + "@rivet-dev/agentos-toolchain": "0.2.15", + "@types/node": "^22.19.15", + "typescript": "^5.7.3", + "vitest": "^2.1.9" + } +} diff --git a/packages/dynamic-apps-builder/src/index.ts b/packages/dynamic-apps-builder/src/index.ts new file mode 100644 index 000000000..16ac3ec03 --- /dev/null +++ b/packages/dynamic-apps-builder/src/index.ts @@ -0,0 +1,12 @@ +import packageJson from "../package.json" with { type: "json" }; + +export interface DynamicAppsBuilderPackageRef { + packagePath: string; +} + +const packagePath = new URL("./package.aospkg", import.meta.url).pathname; + +export const appsBuilderVersion = packageJson.version; +export const appBundleManifestVersion = 1; + +export default { packagePath } satisfies DynamicAppsBuilderPackageRef; diff --git a/packages/dynamic-apps-builder/test/builder.test.ts b/packages/dynamic-apps-builder/test/builder.test.ts new file mode 100644 index 000000000..b1f93bee4 --- /dev/null +++ b/packages/dynamic-apps-builder/test/builder.test.ts @@ -0,0 +1,326 @@ +import { execFile, spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + copyFile, + mkdir, + mkdtemp, + readdir, + readFile, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { runnerSource } from "../../dynamic-apps/src/runtime.js"; + +const execFileAsync = promisify(execFile); +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(packageRoot, "../.."); +const builder = join(packageRoot, "cli", "apps-builder.mjs"); +const rivetKitTarball = process.env.AGENTOS_APPS_RIVETKIT_TARBALL; + +describe("apps-builder", () => { + test("emits a minimal executable TypeScript release with static assets", async () => { + const root = await mkdtemp(join(tmpdir(), "agentos-apps-builder-")); + const workspace = join(root, "workspace"); + const release = join(root, "release"); + await mkdir(join(workspace, "src"), { recursive: true }); + await mkdir(join(workspace, "public"), { recursive: true }); + await writeFile( + join(workspace, "entry.ts"), + [ + 'import { greeting } from "./src/app.ts";', + 'import query from "#query";', + 'import wasmPath from "./src/module.wasm";', + "export default { greeting, query, wasmPath };", + ].join("\n"), + ); + await writeFile( + join(workspace, "package.json"), + JSON.stringify({ + type: "module", + imports: { + "#query": { + node: "./src/query.sql", + default: "./src/missing.sql", + }, + }, + }), + ); + await writeFile( + join(workspace, "src", "app.ts"), + 'export const greeting: string = "hello from Dynamic Apps";\n', + ); + await writeFile( + join(workspace, "src", "query.sql"), + "select 'hello from sqlite';\n", + ); + await writeFile( + join(workspace, "src", "module.wasm"), + new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]), + ); + await writeFile( + join(workspace, "public", "index.html"), + "

Hello

\n", + ); + await writeFile( + join(workspace, "package-lock.json"), + '{"must":"not ship"}\n', + ); + const configPath = join(root, "config.json"); + await writeFile( + configPath, + JSON.stringify({ + workspace, + release, + entrypoint: "entry.ts", + version: "release-test", + staticRoot: "public", + sourceFiles: [ + "src/app.ts", + "src/query.sql", + "src/module.wasm", + "public/index.html", + ], + usesRivetKit: false, + maxOutputBytes: 1024 * 1024, + maxOutputFiles: 32, + maxFileBytes: 512 * 1024, + }), + ); + + await execFileAsync(process.execPath, [builder, configPath]); + + const paths = await listFiles(release); + const wasmPath = paths.find( + (path) => path.startsWith("modules/module-") && path.endsWith(".wasm"), + ); + expect(paths).toEqual([ + "agentos-package.json", + "main.mjs", + "manifest.json", + wasmPath, + "public/index.html", + ]); + expect(paths).not.toContain("package-lock.json"); + expect(paths.some((path) => path.startsWith("src/"))).toBe(false); + expect(paths.some((path) => path.startsWith("node_modules/"))).toBe(false); + + const loaded = await import( + `${pathToFileURL(join(release, "main.mjs")).href}?test=${Date.now()}` + ); + expect(loaded.default).toEqual({ + greeting: "hello from Dynamic Apps", + query: "select 'hello from sqlite';\n", + wasmPath: expect.stringMatching(/^\.\/modules\/module-[A-Z0-9]+\.wasm$/), + }); + + const manifest = JSON.parse( + await readFile(join(release, "manifest.json"), "utf8"), + ); + expect(manifest.version).toBe(1); + expect(manifest.mainModule).toBe("main.mjs"); + expect(manifest.modules).toHaveLength(2); + expect(manifest.assets).toHaveLength(1); + const main = await readFile(join(release, "main.mjs")); + expect(manifest.modules[0]).toMatchObject({ + path: "main.mjs", + size: main.byteLength, + hash: createHash("sha256").update(main).digest("hex"), + }); + }); + + test.skipIf(!rivetKitTarball)( + "bundles a real RivetKit application without native runtime packages", + async () => { + const root = await mkdtemp( + join(tmpdir(), "agentos-apps-rivetkit-builder-"), + ); + const workspace = join(root, "workspace"); + const release = join(root, "release"); + await mkdir(join(workspace, "src"), { recursive: true }); + await mkdir(join(workspace, "vendor"), { recursive: true }); + await copyFile( + rivetKitTarball!, + join(workspace, "vendor", "rivetkit.tgz"), + ); + await writeFile( + join(workspace, "package.json"), + JSON.stringify({ + private: true, + type: "module", + dependencies: { + rivetkit: "file:./vendor/rivetkit.tgz", + "@rivetkit/rivetkit-wasm": + "0.0.0-feat-workflows-public-host-apis.0ff6164", + }, + overrides: { + "@rivet-dev/agent-os-core": "npm:empty-npm-package@1.0.0", + "@rivetkit/engine-cli": "npm:empty-npm-package@1.0.0", + "@rivetkit/rivetkit-napi": "npm:empty-npm-package@1.0.0", + }, + }), + ); + await writeFile( + join(workspace, "runner.mjs"), + runnerSource({ + entrypoint: "src/index.mjs", + release: "rivetkit-test", + port: 3080, + maxRequestBytes: 1024 * 1024, + maxResponseBytes: 1024 * 1024, + usesRivetKit: true, + }), + ); + await writeFile( + join(workspace, "src", "index.mjs"), + [ + 'import { actor, setup } from "rivetkit";', + "export const counter = actor({", + " state: { count: 0 },", + " actions: { increment: (c) => ++c.state.count },", + "});", + "export const registry = setup({ use: { counter } });", + "registry.start();", + 'export default () => new Response("hello");', + ].join("\n"), + ); + const configPath = join(root, "config.json"); + await writeFile( + configPath, + JSON.stringify({ + workspace, + release, + entrypoint: "runner.mjs", + version: "rivetkit-test", + sourceFiles: ["src/index.mjs"], + usesRivetKit: true, + maxOutputBytes: 16 * 1024 * 1024, + maxOutputFiles: 64, + maxFileBytes: 8 * 1024 * 1024, + }), + ); + + await execFileAsync( + "npm", + [ + "install", + "--install-strategy=shallow", + "--omit=optional", + "--omit=peer", + "--legacy-peer-deps", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--loglevel=error", + ], + { cwd: workspace }, + ); + await execFileAsync(process.execPath, [builder, configPath], { + cwd: repositoryRoot, + }); + + const paths = await listFiles(release); + const wasmPath = paths.find( + (path) => + path.startsWith("modules/rivetkit-") && path.endsWith(".wasm"), + ); + expect(paths).toEqual([ + "agentos-package.json", + "main.mjs", + "manifest.json", + wasmPath, + ]); + const totalBytes = ( + await Promise.all( + paths.map( + async (path) => (await readFile(join(release, path))).byteLength, + ), + ) + ).reduce((sum, bytes) => sum + bytes, 0); + expect(totalBytes).toBeLessThan(8 * 1024 * 1024); + + const guest = spawn(process.execPath, [join(release, "main.mjs")], { + env: { + ...process.env, + RIVETKIT_RUNTIME: "wasm", + RIVETKIT_RUNTIME_MODE: "serverless", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + try { + await waitForHttp("http://127.0.0.1:3080/.agentos/ready", guest); + const response = await fetch("http://127.0.0.1:3080/"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("hello"); + const metadata = await fetch( + "http://127.0.0.1:3080/api/rivet/metadata", + { headers: { "user-agent": "RivetEngine/test" } }, + ); + expect(metadata.status).toBe(200); + } finally { + guest.kill("SIGTERM"); + const exited = await Promise.race([ + new Promise((resolve) => + guest.once("exit", () => resolve(true)), + ), + new Promise((resolve) => + setTimeout(() => resolve(false), 1_000), + ), + ]); + if (!exited && guest.exitCode === null) { + guest.kill("SIGKILL"); + if (guest.exitCode === null) { + await new Promise((resolve) => + guest.once("exit", () => resolve()), + ); + } + } + } + }, + 20_000, + ); +}); + +async function listFiles(root: string): Promise { + const paths: string[] = []; + const walk = async (directory: string) => { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await walk(path); + } else { + paths.push(path.slice(root.length + 1).replaceAll("\\", "/")); + } + } + }; + await walk(root); + return paths.sort(); +} + +async function waitForHttp( + url: string, + process: ReturnType, +): Promise { + const deadline = Date.now() + 10_000; + let stderr = ""; + process.stderr?.on("data", (chunk) => { + stderr = `${stderr}${chunk}`.slice(-16_384); + }); + while (Date.now() < deadline) { + if (process.exitCode !== null) { + throw new Error( + `bundled application exited with ${process.exitCode}: ${stderr}`, + ); + } + try { + if ((await fetch(url)).ok) return; + } catch { + // The application is still starting. + } + await new Promise((resolve) => setTimeout(resolve, 25)); + } + throw new Error(`bundled application did not become ready: ${stderr}`); +} diff --git a/packages/dynamic-apps-builder/tsconfig.json b/packages/dynamic-apps-builder/tsconfig.json new file mode 100644 index 000000000..c050f15d0 --- /dev/null +++ b/packages/dynamic-apps-builder/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/dynamic-apps/README.md b/packages/dynamic-apps/README.md new file mode 100644 index 000000000..19a4dd4e8 --- /dev/null +++ b/packages/dynamic-apps/README.md @@ -0,0 +1,100 @@ +# Dynamic Apps + +Dynamic Apps deploys user-generated JavaScript and static sites into isolated +VMs and routes HTTP through Rivet Actors. + +Install Dynamic Apps in a Node.js 22 or newer project: + +```sh +npm add @rivet-dev/dynamic-apps +npm add @hono/node-server hono +npm add --save-dev tsx +npm pkg set type=module +``` + +RivetKit starts a local Engine automatically. For an existing Rivet deployment, +use its standard Rivet connection variables and credentials. + +`src/actors.ts`: + +```ts +import { setup, setupApps } from "@rivet-dev/dynamic-apps"; + +const { appsActors } = setupApps(); + +export const registry = setup({ + use: { + ...appsActors, + }, +}); +``` + +`src/server.ts`: + +```ts +import { serve } from "@hono/node-server"; +import { appsRouter } from "@rivet-dev/dynamic-apps"; +import { Hono } from "hono"; +import { registry } from "./actors.js"; + +registry.start(); + +const server = new Hono(); +server.route("/apps", appsRouter); + +serve({ fetch: server.fetch, port: 3000 }); +``` + +`src/deploy.ts`: + +```ts +import { deployApp } from "@rivet-dev/dynamic-apps"; + +await deployApp({ + appId: "hello-world", + files: { + "index.html": "

Hello from Dynamic Apps

", + }, +}); +``` + +The common API has three primary entry points: + +- `setupApps()` returns the three stable internal actor definitions used for + deployments, scaling, and replicas. +- `deployApp({ appId, source | files })` builds and activates an immutable + release. It lazily uses an ordinary RivetKit client. +- `appsRouter` routes `/:appId` and `/:appId/*` to deployed applications. + +It also exports the deployment input, result, scaling, and typed error types. +Supplying a custom ordinary client remains an option on `deployApp()` rather +than a separate Apps client abstraction. + +Submitted source and packed release chunks are stored in the stable app actor's +SQLite database. Serving replicas materialize a verified temporary `.aospkg` +for the VM lifetime and delete it after VM disposal. No durable local artifact +directory is required. + +Dependencies and build tools exist only inside a disposable build VM. The +platform-owned Apps builder emits a minimal release containing bundled +JavaScript, imported WASM modules, and static assets; serving replicas never +install packages and releases do not contain tenant `node_modules`. + +Deployments use the ordinary Rivet connection's configured namespace by +default and do not require namespace-management permission. Set +`createNamespace: true` on `deployApp()` to idempotently create a stable, +isolated namespace for that `appId` within the configured host namespace. Every +deployment returns its stable `pool` along with its `namespace` for ordinary +DirectActor clients. + +The `scaling` options default to `minReplicas: 0`, `maxReplicas: 128`, and +`targetConcurrency: 8`. + +Guest Rivet Actors use the ordinary DirectActor API from `rivetkit/client`. +Dynamic Apps does not export or wrap a RivetKit client. Host management tokens +are never exposed inside guest VMs. Each VM receives an opaque, app-scoped +Engine capability that fixes the namespace and runner pool and rejects +management routes. Rivet Engine callbacks use a random per-app credential that +the trusted app actor validates and strips before forwarding. + +See `examples/apps-hello-world` for the smallest runnable server. diff --git a/packages/dynamic-apps/assets/inspector/deployment/index.html b/packages/dynamic-apps/assets/inspector/deployment/index.html new file mode 100644 index 000000000..eb2e2580b --- /dev/null +++ b/packages/dynamic-apps/assets/inspector/deployment/index.html @@ -0,0 +1,50 @@ + + + + + + Dynamic App deployment + + + + +

Dynamic App deployment

+
Waiting for inspector…
+ + + diff --git a/packages/dynamic-apps/assets/inspector/replica/index.html b/packages/dynamic-apps/assets/inspector/replica/index.html new file mode 100644 index 000000000..d7be08067 --- /dev/null +++ b/packages/dynamic-apps/assets/inspector/replica/index.html @@ -0,0 +1,50 @@ + + + + + + Dynamic App execution replica + + + + +

Dynamic App execution replica

+
Waiting for inspector…
+ + + diff --git a/packages/dynamic-apps/assets/inspector/scaler/index.html b/packages/dynamic-apps/assets/inspector/scaler/index.html new file mode 100644 index 000000000..dc63e4b3a --- /dev/null +++ b/packages/dynamic-apps/assets/inspector/scaler/index.html @@ -0,0 +1,50 @@ + + + + + + Dynamic App regional scaler + + + + +

Dynamic App regional scaler

+
Waiting for inspector…
+ + + diff --git a/packages/dynamic-apps/package.json b/packages/dynamic-apps/package.json new file mode 100644 index 000000000..fe69a0125 --- /dev/null +++ b/packages/dynamic-apps/package.json @@ -0,0 +1,52 @@ +{ + "name": "@rivet-dev/dynamic-apps", + "version": "0.2.15", + "description": "Run and scale user-generated HTTP applications with Rivet Actors.", + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "files": [ + "dist", + "assets", + "README.md", + "package.json" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./advanced": { + "import": { + "types": "./dist/advanced.d.ts", + "default": "./dist/advanced.js" + } + } + }, + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "build": "tsup src/index.ts src/advanced.ts --format esm --dts --sourcemap --clean --external rivetkit --external @rivet-dev/agentos --external @rivet-dev/agentos-core --external @rivet-dev/agentos-toolchain --external @rivet-dev/dynamic-apps-builder --external @agentos-software/sh --external @agentos-software/tar", + "check-types": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@agentos-software/sh": "0.2.15", + "@agentos-software/tar": "0.3.5", + "@rivet-dev/agentos": "0.2.15", + "@rivet-dev/agentos-core": "0.2.15", + "@rivet-dev/agentos-toolchain": "0.2.15", + "@rivet-dev/dynamic-apps-builder": "workspace:0.2.15", + "hono": "^4.7.0", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@types/node": "^22.19.15", + "tsup": "^8.4.0", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + } +} diff --git a/packages/dynamic-apps/src/actors.ts b/packages/dynamic-apps/src/actors.ts new file mode 100644 index 000000000..c1f834674 --- /dev/null +++ b/packages/dynamic-apps/src/actors.ts @@ -0,0 +1,3891 @@ +import { createHash, randomUUID, timingSafeEqual } from "node:crypto"; +import { mkdtemp, open, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import sh from "@agentos-software/sh"; +import tar from "@agentos-software/tar"; +import { + agentOS, + type VmFetchResponse, + type VmFetchStreamChunk, + type VmFetchStreamHead, +} from "@rivet-dev/agentos"; +import { + AgentOs, + type AgentOsOptions, + createHostDirBackend, +} from "@rivet-dev/agentos-core"; +import { packAospkgFromTarBytes } from "@rivet-dev/agentos-toolchain"; +import appsBuilder, { + appBundleManifestVersion, + appsBuilderVersion, +} from "@rivet-dev/dynamic-apps-builder"; +import { type AnyActorDefinition, actor, UserError } from "rivetkit"; +import { db, type RawAccess } from "rivetkit/db"; +import { + configureAppNamespaceRunner, + resolveDefaultRivetConnection, +} from "./control-plane.js"; +import { + type GuestEngineProxyRegistration, + registerGuestEngineProxy, + unregisterGuestEngineProxy, +} from "./engine-proxy.js"; +import { AgentOSAppsError } from "./errors.js"; +import { + APP_CALLBACK_SECRET_HEADER, + canonicalDeploymentHash, + normalizeAppPath, + releaseEnvoyVersion, + runnerSource, + staticRunnerSource, +} from "./runtime.js"; +import type { + AppReleaseInfo, + AppScaling, + Deployment, + PreparedDeployAppInput, +} from "./types.js"; + +const APP_PORT = 3_080; +const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024; +const DEFAULT_MAX_FILES = 2_000; +const DEFAULT_MAX_VERSIONS = 20; +const DEFAULT_MAX_REGIONS = 8; +const DEFAULT_BUILD_TIMEOUT_MS = 15 * 60_000; +const DEFAULT_WARM_TIMEOUT_MS = 30_000; +const DEFAULT_WARM_IDLE_TIMEOUT_MS = 5 * 60_000; +const DEFAULT_ADMISSION_LEASE_MS = 60_000; +const DEFAULT_MAX_ADMISSIONS = 4_096; +const DEFAULT_MAX_REQUEST_BYTES = 1024 * 1024; +const DEFAULT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024; +const DEFAULT_MAX_SERVERLESS_METADATA_BYTES = 256 * 1024; +const GUEST_SHUTDOWN_TIMEOUT_MS = 15_000; +const GUEST_RPC_TIMEOUT_MS = 30_000; +const MAX_PENDING_GUEST_RPCS = 1_024; +const GUEST_RPC_PREFIX = "AGENTOS_APPS_RPC "; +const DEFAULT_MAX_DEPENDENCIES = 256; +const DEFAULT_MAX_BUILD_OUTPUT_BYTES = 2 * 1024 * 1024; +const DEFAULT_MAX_BUILD_ARTIFACT_BYTES = 64 * 1024 * 1024; +const DEFAULT_MAX_BUILD_ARTIFACT_FILES = 4_096; +const DEFAULT_MAX_BUILD_ARTIFACT_FILE_BYTES = 32 * 1024 * 1024; +const DEFAULT_MAX_BUILD_FILESYSTEM_BYTES = 2 * 1024 * 1024 * 1024; +const MAX_REPLICAS = 128; +const ARTIFACT_CHUNK_BYTES = 512 * 1024; +const ARTIFACT_CHUNKS_PER_TRANSACTION = 1; +const MAX_ARTIFACT_CHUNKS = Math.ceil( + DEFAULT_MAX_BUILD_ARTIFACT_BYTES / ARTIFACT_CHUNK_BYTES, +); +const SOURCE_CHUNK_BYTES = 512 * 1024; +const MAX_SOURCE_CHUNKS = + Math.ceil(DEFAULT_MAX_SOURCE_BYTES / SOURCE_CHUNK_BYTES) + DEFAULT_MAX_FILES; + +const APP_ACTOR_NAME = "agentOSAppsApp"; +const SCALER_ACTOR_NAME = "agentOSAppsScaler"; +const REPLICA_ACTOR_NAME = "agentOSAppsReplica"; +const INSPECTOR_ROOT = fileURLToPath( + new URL("../assets/inspector", import.meta.url), +); + +export interface StoredAppRelease extends AppReleaseInfo { + entrypoint: string; + namespace: string; + envoyVersion: number; + runtimeEndpoint: string; + runtimePool: string; + usesRivetKit: boolean; + callbackSecret: string; +} + +export interface AppState { + activeRelease: string | null; + namespace: string | null; + revision: number; + nextEnvoyVersion?: number; + serverlessMetadata?: { + release: string; + status: number; + statusText: string; + headers: Record; + bodyBase64: string; + }; +} + +export interface ReplicaRecord { + key: string[]; + readyAt: number; + activeRequests: number; + lastUsedAt: number; + draining: boolean; +} + +interface AdmissionLease { + id: string; + replicaKey: string[]; + expiresAt: number; +} + +export interface ScalerState { + appId: string | null; + release: string | null; + region: string | null; + scaling: Required | null; + replicas: ReplicaRecord[]; + warmingReplicas: number; + warmingReplicaKeys?: string[][]; + capacityWarningLatched: boolean; + retired: boolean; + revision: number; + selectionCursor: number; + nextReplicaIndex: number; + reconcileScheduledAt: number | null; + admissions?: Record; +} + +export interface ReplicaState { + configuration: { + appId: string; + release: string; + artifactHash: string; + artifactBytes: number; + namespace: string; + envoyVersion: number; + runtime: AppRuntimeConfig; + /** Absent on replicas persisted before conditional Engine access. */ + usesRivetKit?: boolean; + } | null; + startedAt: number | null; + guestPid: number | null; +} + +interface ReplicaAdmission { + admissionId: string; + leaseMs: number; + key: string[]; + release: string; + region: string; + replicaCount: number; + queueDelayMs: number; + coldStart: boolean; +} + +export interface AppRuntimeConfig { + endpoint: string; + namespace: string; + pool: string; +} + +type ReplicaConfiguration = NonNullable; + +/** @internal Exported for focused security tests. */ +export function replicaGuestEnvironment( + configuration: ReplicaConfiguration, + engineEndpoint = configuration.runtime.endpoint, +): Record { + const env: Record = { NODE_ENV: "production" }; + if (!configuration.usesRivetKit) return env; + return { + ...env, + RIVETKIT_RUNTIME: "wasm", + RIVETKIT_RUNTIME_MODE: "serverless", + RIVET_ENVOY_VERSION: String( + configuration.envoyVersion ?? releaseEnvoyVersion(configuration.release), + ), + RIVET_ENDPOINT: engineEndpoint, + RIVET_NAMESPACE: configuration.runtime.namespace, + RIVET_POOL: configuration.runtime.pool, + RIVET_RUNNER: configuration.runtime.pool, + RIVET_RUNNER_POOL: configuration.runtime.pool, + }; +} + +/** @internal Exported for focused security tests. */ +export function replicaLoopbackExemptPorts( + configuration: ReplicaConfiguration, + proxyPort?: number, +): number[] { + return configuration.usesRivetKit && proxyPort !== undefined + ? [proxyPort] + : []; +} + +type AnyActorContext = { + actorId: string; + key: string[]; + region: string; + state: any; + db: RawAccess; + client(): any; + keepAwake(promise: Promise): Promise; + destroy(): void; + schedule: { + after( + delayMs: number, + action: string, + ...args: unknown[] + ): Promise; + }; + log: { + info(value: unknown): void; + warn(value: unknown): void; + error(value: unknown): void; + }; +}; + +interface ExecResult { + exitCode: number; + stdout: string; + stderr: string; +} + +type BuildHandle = { + artifactGuestPath: string; + writeFiles( + entries: Array<{ path: string; content: string | Uint8Array }>, + ): Promise>; + execArgv( + command: string, + args: string[], + options?: { + cwd?: string; + env?: Record; + timeout?: number; + captureStdio?: boolean; + }, + ): Promise; + artifactSize(): Promise; + readArtifact(): Promise; + dispose(): Promise; +}; + +type ReplicaHandle = { + destroy(): Promise; + configure(input: ReplicaState["configuration"]): Promise; + markStarted(): Promise; + inspect(): Promise<{ + release: string | null; + startedAt: number | null; + }>; + fetch( + input: string | URL | Request, + init?: RequestInit & { skipReadyWait?: boolean }, + ): Promise; + spawn( + command: string, + args: string[], + options?: { cwd?: string; env?: Record }, + ): Promise<{ pid: number }>; + vmFetch( + port: number, + url: string, + options?: { + method?: string; + headers?: Record; + body?: string | Uint8Array; + }, + ): Promise<{ + status: number; + statusText: string; + headers: Record; + body: Uint8Array; + }>; + vmFetchStreamStart( + port: number, + url: string, + options?: { + method?: string; + headers?: Record; + body?: string | Uint8Array; + }, + ): Promise; + vmFetchStreamRead( + streamId: string, + maxBytes?: number, + ): Promise; + vmFetchStreamCancel(streamId: string): Promise; +}; + +export interface AppRouteResolution { + appId: string; + release: string; + region: string; + scalerKey: string[]; + revision: number; + maxRequestBytes: number; + maxResponseBytes: number; +} + +async function readBoundedRequestBody( + request: Request, + maxBytes: number, +): Promise { + if (request.method === "GET" || request.method === "HEAD") return undefined; + if (!request.body) return new Uint8Array(0); + const reader = request.body.getReader(); + const chunks: Uint8Array[] = []; + let bytes = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + bytes += value.byteLength; + if (bytes > maxBytes) { + await reader.cancel("Dynamic Apps request body limit exceeded"); + return null; + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + return new Uint8Array(Buffer.concat(chunks, bytes)); +} + +async function readBoundedResponseBody( + response: Response, + maxBytes: number, +): Promise { + if (!response.body) return new Uint8Array(0); + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let bytes = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + bytes += value.byteLength; + if (bytes > maxBytes) { + await reader.cancel("Dynamic Apps response body limit exceeded"); + fail( + "agentos_apps_response_limit", + `response exceeds ${maxBytes} bytes`, + { limit: maxBytes }, + ); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + return new Uint8Array(Buffer.concat(chunks, bytes)); +} + +const locks = new Map>(); + +async function serialized(key: string, run: () => Promise): Promise { + const previous = locks.get(key) ?? Promise.resolve(); + let release = () => {}; + const gate = new Promise((resolve) => { + release = resolve; + }); + const current = previous.then(() => gate); + locks.set(key, current); + await previous; + try { + return await run(); + } finally { + release(); + if (locks.get(key) === current) locks.delete(key); + } +} + +function fail( + code: string, + message: string, + metadata?: Record, +): never { + throw new UserError(message, { code, metadata }); +} + +function positiveInteger(value: number, name: string, maximum: number): number { + if (!Number.isInteger(value) || value < 1 || value > maximum) { + fail( + "agentos_apps_invalid_config", + `${name} must be an integer between 1 and ${maximum}`, + { name, maximum }, + ); + } + return value; +} + +export function normalizeScaling( + input: AppScaling | undefined, +): Required { + const minReplicas = input?.minReplicas ?? 0; + const maxReplicas = input?.maxReplicas ?? 128; + const targetConcurrency = input?.targetConcurrency ?? 8; + if ( + !Number.isInteger(minReplicas) || + minReplicas < 0 || + minReplicas > MAX_REPLICAS + ) { + fail( + "agentos_apps_invalid_scaling", + `scaling.minReplicas must be an integer between 0 and ${MAX_REPLICAS}`, + ); + } + positiveInteger(maxReplicas, "scaling.maxReplicas", MAX_REPLICAS); + positiveInteger(targetConcurrency, "scaling.targetConcurrency", 1_024); + if (minReplicas > maxReplicas) { + fail( + "agentos_apps_invalid_scaling", + "scaling.minReplicas cannot exceed scaling.maxReplicas", + ); + } + return { minReplicas, maxReplicas, targetConcurrency }; +} + +/** @internal Exported for focused migration tests. */ +export async function migrateAppsTables(database: RawAccess): Promise { + await database.execute(` + CREATE TABLE IF NOT EXISTS agentos_apps_releases ( + release_id TEXT PRIMARY KEY, + created_at INTEGER NOT NULL, + status TEXT NOT NULL, + entrypoint TEXT NOT NULL, + artifact_hash TEXT NOT NULL DEFAULT '', + artifact_bytes INTEGER NOT NULL DEFAULT 0, + build_error TEXT, + regions_json TEXT NOT NULL, + scaling_json TEXT NOT NULL, + namespace TEXT NOT NULL, + envoy_version INTEGER NOT NULL, + runtime_endpoint TEXT NOT NULL, + runtime_pool TEXT NOT NULL, + callback_secret TEXT NOT NULL DEFAULT '', + uses_rivetkit INTEGER NOT NULL DEFAULT 0 + CHECK (uses_rivetkit IN (0, 1)) + ); + CREATE TABLE IF NOT EXISTS agentos_apps_release_files ( + release_id TEXT NOT NULL, + path TEXT NOT NULL, + chunk_index INTEGER NOT NULL, + content BLOB NOT NULL, + byte_length INTEGER NOT NULL, + PRIMARY KEY (release_id, path, chunk_index) + ); + CREATE TABLE IF NOT EXISTS agentos_apps_artifact_chunks ( + release_id TEXT NOT NULL, + chunk_index INTEGER NOT NULL, + content BLOB NOT NULL, + byte_length INTEGER NOT NULL, + PRIMARY KEY (release_id, chunk_index) + ); + CREATE INDEX IF NOT EXISTS idx_agentos_apps_releases_created_at + ON agentos_apps_releases(created_at); + `); + const columns = await database.execute<{ name: string }>( + "PRAGMA table_info(agentos_apps_releases)", + ); + if (!columns.some((column) => column.name === "callback_secret")) { + await database.execute( + `ALTER TABLE agentos_apps_releases + ADD COLUMN callback_secret TEXT NOT NULL DEFAULT ''`, + ); + } + if (!columns.some((column) => column.name === "uses_rivetkit")) { + await database.execute( + `ALTER TABLE agentos_apps_releases + ADD COLUMN uses_rivetkit INTEGER NOT NULL DEFAULT 0 + CHECK (uses_rivetkit IN (0, 1))`, + ); + } +} + +async function deleteReleaseFilesBatched( + database: RawAccess, + releaseId: string, +): Promise { + for (let batch = 0; batch < MAX_SOURCE_CHUNKS; batch += 1) { + const rows = await database.execute<{ chunks: number }>( + `SELECT COUNT(*) AS chunks + FROM agentos_apps_release_files + WHERE release_id = ?`, + releaseId, + ); + if (Number(rows[0]?.chunks ?? 0) === 0) return; + await database.execute( + `DELETE FROM agentos_apps_release_files + WHERE rowid IN ( + SELECT rowid FROM agentos_apps_release_files + WHERE release_id = ? + ORDER BY path, chunk_index + LIMIT 1 + )`, + releaseId, + ); + } + fail( + "agentos_apps_source_cleanup_limit", + `source cleanup exceeded ${MAX_SOURCE_CHUNKS} bounded batches`, + { releaseId, limit: MAX_SOURCE_CHUNKS }, + ); +} + +async function persistReleaseFilesBatched( + database: RawAccess, + releaseId: string, + files: Record, +): Promise { + for (const [path, content] of Object.entries(files)) { + const chunkCount = Math.max( + 1, + Math.ceil(content.byteLength / SOURCE_CHUNK_BYTES), + ); + for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex += 1) { + const offset = chunkIndex * SOURCE_CHUNK_BYTES; + const chunk = content.slice(offset, offset + SOURCE_CHUNK_BYTES); + await database.execute( + `INSERT INTO agentos_apps_release_files + (release_id, path, chunk_index, content, byte_length) + VALUES (?, ?, ?, ?, ?)`, + releaseId, + path, + chunkIndex, + chunk, + chunk.byteLength, + ); + } + } +} + +async function deleteArtifactChunksBatched( + database: RawAccess, + releaseId: string, +): Promise { + for (let batch = 0; batch < MAX_ARTIFACT_CHUNKS; batch += 1) { + const rows = await database.execute<{ chunks: number }>( + `SELECT COUNT(*) AS chunks + FROM agentos_apps_artifact_chunks + WHERE release_id = ?`, + releaseId, + ); + if (Number(rows[0]?.chunks ?? 0) === 0) return; + await database.execute( + `DELETE FROM agentos_apps_artifact_chunks + WHERE rowid IN ( + SELECT rowid FROM agentos_apps_artifact_chunks + WHERE release_id = ? + ORDER BY chunk_index + LIMIT ${ARTIFACT_CHUNKS_PER_TRANSACTION} + )`, + releaseId, + ); + } + fail( + "agentos_apps_artifact_cleanup_limit", + `artifact cleanup exceeded ${MAX_ARTIFACT_CHUNKS} bounded batches`, + { releaseId, limit: MAX_ARTIFACT_CHUNKS }, + ); +} + +interface ReleaseRow extends Record { + release_id: string; + created_at: number; + status: StoredAppRelease["status"]; + entrypoint: string; + artifact_hash: string; + artifact_bytes: number; + build_error: string | null; + regions_json: string; + scaling_json: string; + namespace: string; + envoy_version: number; + runtime_endpoint: string; + runtime_pool: string; + callback_secret?: string; + uses_rivetkit?: number; +} + +function releaseFromRow(row: ReleaseRow): StoredAppRelease { + return { + release: row.release_id, + createdAt: Number(row.created_at), + status: row.status, + entrypoint: row.entrypoint, + artifactHash: row.artifact_hash, + artifactBytes: Number(row.artifact_bytes), + error: row.build_error ?? undefined, + regions: JSON.parse(row.regions_json) as string[], + scaling: JSON.parse(row.scaling_json) as Required, + namespace: row.namespace, + envoyVersion: Number(row.envoy_version), + runtimeEndpoint: row.runtime_endpoint, + runtimePool: row.runtime_pool, + callbackSecret: row.callback_secret ?? "", + usesRivetKit: Number(row.uses_rivetkit ?? 0) === 1, + }; +} + +async function getStoredRelease( + database: RawAccess, + releaseId: string, +): Promise { + const rows = await database.execute( + "SELECT * FROM agentos_apps_releases WHERE release_id = ?", + releaseId, + ); + return rows[0] ? releaseFromRow(rows[0]) : undefined; +} + +async function listStoredReleases( + database: RawAccess, +): Promise { + const rows = await database.execute( + "SELECT * FROM agentos_apps_releases ORDER BY created_at ASC", + ); + return rows.map(releaseFromRow); +} + +interface BuildPlan { + entrypoint: string; + build: boolean; + staticRoot?: string; + dependencyCount: number; + hasLockfile: boolean; + usesRivetKit: boolean; +} + +function textFile( + files: Record, + path: string, +): string | undefined { + const content = files[path]; + return content ? new TextDecoder().decode(content) : undefined; +} + +function packageExport(value: unknown): string | undefined { + if (typeof value === "string") return value; + if (!value || typeof value !== "object" || Array.isArray(value)) + return undefined; + const object = value as Record; + return ( + packageExport(object["."]) ?? + packageExport(object.import) ?? + packageExport(object.default) + ); +} + +function validateDeployment( + input: PreparedDeployAppInput, + limits: { maxSourceBytes: number; maxFiles: number; maxDependencies: number }, +): BuildPlan { + if (!input || typeof input !== "object" || !input.files) { + fail( + "agentos_apps_invalid_files", + "deployApp files must contain the complete application tree", + ); + } + const files = Object.entries(input.files); + if (files.length === 0 || files.length > limits.maxFiles) { + fail( + "agentos_apps_file_count_limit", + `deployment must contain between 1 and ${limits.maxFiles} files; reduce the source tree`, + { observed: files.length, limit: limits.maxFiles }, + ); + } + let sourceBytes = 0; + const normalizedFiles: Record = {}; + for (const [path, content] of files) { + const normalizedPath = normalizeAppPath(path); + if (normalizedFiles[normalizedPath]) { + fail( + "agentos_apps_duplicate_file_path", + `multiple deployment paths normalize to ${normalizedPath}`, + ); + } + if (!(content instanceof Uint8Array)) { + fail( + "agentos_apps_invalid_file", + `deployment file ${path} must be a string or Uint8Array`, + ); + } + normalizedFiles[normalizedPath] = content; + sourceBytes += content.byteLength; + } + if (sourceBytes > limits.maxSourceBytes) { + fail( + "agentos_apps_source_limit", + `deployment source is ${sourceBytes} bytes, exceeding maxSourceBytes ${limits.maxSourceBytes}; reduce the source tree`, + { observed: sourceBytes, limit: limits.maxSourceBytes }, + ); + } + input.files = normalizedFiles; + + const packageJsonSource = textFile(normalizedFiles, "package.json"); + if (!packageJsonSource) { + if (!normalizedFiles["index.html"]) { + fail( + "agentos_apps_entrypoint_not_found", + "application without package.json must contain index.html", + ); + } + return { + entrypoint: "runner.mjs", + build: false, + staticRoot: ".", + dependencyCount: 0, + hasLockfile: false, + usesRivetKit: false, + }; + } + + let packageJson: { + dependencies?: unknown; + devDependencies?: unknown; + scripts?: { build?: unknown }; + exports?: unknown; + main?: unknown; + }; + try { + packageJson = JSON.parse(packageJsonSource); + } catch (error) { + fail( + "agentos_apps_invalid_package_json", + "package.json is not valid JSON", + { + error: String(error), + }, + ); + } + const dependencyCount = [ + packageJson.dependencies, + packageJson.devDependencies, + ] + .filter( + (value): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value), + ) + .reduce( + (count, dependencies) => count + Object.keys(dependencies).length, + 0, + ); + const usesRivetKit = [packageJson.dependencies, packageJson.devDependencies] + .filter( + (value): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value), + ) + .some((dependencies) => typeof dependencies.rivetkit === "string"); + if (dependencyCount > limits.maxDependencies) { + fail( + "agentos_apps_dependency_limit", + `deployment has ${dependencyCount} dependencies, exceeding maxDependencies ${limits.maxDependencies}; reduce dependencies`, + { observed: dependencyCount, limit: limits.maxDependencies }, + ); + } + const build = typeof packageJson.scripts?.build === "string"; + const declaredEntrypoint = + packageExport(packageJson.exports) ?? + (typeof packageJson.main === "string" ? packageJson.main : undefined); + if (declaredEntrypoint) { + return { + entrypoint: normalizeAppPath(declaredEntrypoint), + build, + dependencyCount, + hasLockfile: Boolean(normalizedFiles["package-lock.json"]), + usesRivetKit, + }; + } + for (const candidate of [ + "src/index.mjs", + "src/index.js", + "index.mjs", + "index.js", + ]) { + if (normalizedFiles[candidate]) { + return { + entrypoint: candidate, + build, + dependencyCount, + hasLockfile: Boolean(normalizedFiles["package-lock.json"]), + usesRivetKit, + }; + } + } + if (build) { + return { + entrypoint: "runner.mjs", + build: true, + staticRoot: "dist", + dependencyCount, + hasLockfile: Boolean(normalizedFiles["package-lock.json"]), + usesRivetKit, + }; + } + if (normalizedFiles["index.html"]) { + return { + entrypoint: "runner.mjs", + build: false, + staticRoot: ".", + dependencyCount, + hasLockfile: Boolean(normalizedFiles["package-lock.json"]), + usesRivetKit, + }; + } + fail( + "agentos_apps_entrypoint_not_found", + "could not infer a server entrypoint or static index.html", + ); +} + +function normalizeRegions( + regions: string[] | undefined, + fallbackRegion: string, + maxRegions: number, +): string[] { + const unique = [...new Set(regions ?? [fallbackRegion || "default"])]; + if (unique.length === 0 || unique.length > maxRegions) { + fail( + "agentos_apps_invalid_regions", + `an app must have between 1 and ${maxRegions} regions; raise maxRegions to allow more`, + { maxRegions }, + ); + } + for (const region of unique) { + if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(region)) { + fail( + "agentos_apps_invalid_region", + `invalid region ${JSON.stringify(region)}; use a lowercase Rivet region slug`, + ); + } + } + return unique; +} + +function scalerKey(appId: string, release: string, region: string): string[] { + return [appId, release, region]; +} + +function replicaKey( + appId: string, + release: string, + region: string, + index: number, +): string[] { + return [appId, release, region, String(index)]; +} + +function boundedOutput(value: string, maximum: number): string { + const bytes = Buffer.from(value); + if (bytes.byteLength <= maximum) return value; + return `${bytes.subarray(0, maximum).toString("utf8")}\n[truncated at ${maximum} bytes]`; +} + +const HOP_BY_HOP_HEADERS = [ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +] as const; + +function stripHopByHopHeaders(headers: Headers): void { + const connectionTokens = (headers.get("connection") ?? "") + .split(",") + .map((value) => value.trim().toLowerCase()) + .filter(Boolean); + for (const name of [...HOP_BY_HOP_HEADERS, ...connectionTokens]) { + headers.delete(name); + } +} + +function throwCommandFailure( + kind: "install" | "build" | "pack", + command: string, + result: ExecResult, + maxOutputBytes: number, +): never { + fail( + `agentos_apps_${kind}_failed`, + `${command} failed with exit code ${result.exitCode}`, + { + exitCode: result.exitCode, + stdout: boundedOutput(result.stdout, maxOutputBytes), + stderr: boundedOutput(result.stderr, maxOutputBytes), + }, + ); +} + +async function buildRelease( + c: AnyActorContext, + input: PreparedDeployAppInput, + plan: BuildPlan, + release: string, + config: { + createBuildVm: () => Promise; + buildTimeoutMs: number; + maxRequestBytes: number; + maxResponseBytes: number; + maxBuildOutputBytes: number; + maxBuildArtifactBytes: number; + artifactCache?: { + get(release: string): Promise; + put(release: string, artifact: Uint8Array): Promise; + }; + }, +): Promise<{ hash: string; size: number; bytes: Uint8Array }> { + const cached = await config.artifactCache?.get(release); + if (cached) { + if (cached.byteLength > config.maxBuildArtifactBytes) { + fail( + "agentos_apps_build_artifact_size_limit", + `cached application artifact is ${cached.byteLength} bytes, limit is maxBuildArtifactBytes ${config.maxBuildArtifactBytes}`, + { + artifactBytes: cached.byteLength, + maxBuildArtifactBytes: config.maxBuildArtifactBytes, + }, + ); + } + return { + hash: createHash("sha256").update(cached).digest("hex"), + size: cached.byteLength, + bytes: cached, + }; + } + const build = await config.createBuildVm(); + const buildStartedAt = Date.now(); + const logBuildPhase = (phase: string): void => { + c.log.info({ + msg: "Dynamic Apps build phase completed", + release, + phase, + elapsedMs: Date.now() - buildStartedAt, + }); + }; + let buildError: unknown; + try { + const files: Array<{ + path: string; + content: string | Uint8Array; + }> = Object.entries(input.files).map(([path, content]) => ({ + path: `/workspace/${normalizeAppPath(path)}`, + content, + })); + files.push({ + path: "/workspace/runner.mjs", + content: plan.staticRoot + ? staticRunnerSource({ + root: "public", + release, + port: APP_PORT, + }) + : runnerSource({ + entrypoint: plan.entrypoint, + release, + port: APP_PORT, + maxRequestBytes: config.maxRequestBytes, + maxResponseBytes: config.maxResponseBytes, + usesRivetKit: plan.usesRivetKit, + }), + }); + const writes = await build.writeFiles(files); + const failedWrite = writes.find((entry) => !entry.success); + if (failedWrite) { + fail( + "agentos_apps_build_write_failed", + `failed to write build input ${failedWrite.path}: ${failedWrite.error ?? "unknown error"}`, + { path: failedWrite.path, error: failedWrite.error }, + ); + } + + if (input.files["package.json"]) { + if (plan.usesRivetKit) { + const packageJson = JSON.parse( + new TextDecoder().decode(input.files["package.json"]), + ) as Record; + const overrides = + typeof packageJson.overrides === "object" && + packageJson.overrides !== null && + !Array.isArray(packageJson.overrides) + ? (packageJson.overrides as Record) + : {}; + const dependencies = + typeof packageJson.dependencies === "object" && + packageJson.dependencies !== null && + !Array.isArray(packageJson.dependencies) + ? (packageJson.dependencies as Record) + : {}; + const devDependencies = + typeof packageJson.devDependencies === "object" && + packageJson.devDependencies !== null && + !Array.isArray(packageJson.devDependencies) + ? (packageJson.devDependencies as Record) + : {}; + const rivetKitVersion = + typeof dependencies.rivetkit === "string" + ? dependencies.rivetkit + : typeof devDependencies.rivetkit === "string" + ? devDependencies.rivetkit + : undefined; + if (!rivetKitVersion) { + fail( + "agentos_apps_invalid_rivetkit_dependency", + "RivetKit applications must declare a string rivetkit dependency", + ); + } + const rivetKitBuildOnlyPackages = [ + "@rivet-dev/agent-os-core", + "@rivetkit/engine-cli", + "@rivetkit/rivetkit-napi", + ] as const; + const needsCompatibilityOverrides = rivetKitBuildOnlyPackages.some( + (name) => overrides[name] === undefined, + ); + const needsRuntimeDependencies = + dependencies.rivetkit === undefined || + dependencies["@rivetkit/rivetkit-wasm"] === undefined; + if (needsCompatibilityOverrides || needsRuntimeDependencies) { + // RivetKit publishes host integrations as hard dependencies even + // though its normal serverless/WASM entrypoint does not load them. + // Avoid recursively packaging AgentOS, the engine binary, and a + // native addon that cannot execute inside the guest VM. + for (const name of rivetKitBuildOnlyPackages) { + overrides[name] ??= "npm:empty-npm-package@1.0.0"; + } + dependencies.rivetkit ??= rivetKitVersion; + dependencies["@rivetkit/rivetkit-wasm"] ??= rivetKitVersion; + packageJson.overrides = overrides; + packageJson.dependencies = dependencies; + const compatibilityWrites = await build.writeFiles([ + { + path: "/workspace/package.json", + content: JSON.stringify(packageJson), + }, + ]); + const failedCompatibilityWrite = compatibilityWrites.find( + (entry) => !entry.success, + ); + if (failedCompatibilityWrite) { + fail( + "agentos_apps_build_write_failed", + `failed to prepare RivetKit dependency ${failedCompatibilityWrite.path}: ${failedCompatibilityWrite.error ?? "unknown error"}`, + { + path: failedCompatibilityWrite.path, + error: failedCompatibilityWrite.error, + }, + ); + } + + if (plan.hasLockfile) { + const reconcileArgs = [ + "install", + "--package-lock-only", + "--ignore-scripts", + "--include=dev", + "--omit=optional", + "--omit=peer", + "--legacy-peer-deps", + "--no-audit", + "--no-fund", + "--maxsockets=16", + "--loglevel=error", + ]; + const reconcile = await build.execArgv("npm", reconcileArgs, { + cwd: "/workspace", + env: { + NODE_ENV: "development", + NPM_CONFIG_PRODUCTION: "false", + }, + timeout: config.buildTimeoutMs, + captureStdio: true, + }); + if (reconcile.exitCode !== 0) { + throwCommandFailure( + "install", + "npm install --package-lock-only", + reconcile, + config.maxBuildOutputBytes, + ); + } + } + } + } + const installArgs = [ + plan.hasLockfile ? "ci" : "install", + "--install-strategy=shallow", + "--include=dev", + "--omit=optional", + "--omit=peer", + "--legacy-peer-deps", + "--no-audit", + "--no-fund", + "--maxsockets=16", + "--loglevel=error", + ]; + const install = await build.execArgv("npm", installArgs, { + cwd: "/workspace", + env: { + NODE_ENV: "development", + NPM_CONFIG_PRODUCTION: "false", + }, + timeout: config.buildTimeoutMs, + captureStdio: true, + }); + if (install.exitCode !== 0) { + const debugLog = await build.execArgv( + "node", + [ + "-e", + 'const fs=require("node:fs"); const path=require("node:path"); const cache=process.env.npm_config_cache || path.join(process.env.HOME || "/root", ".npm"); const logs=path.join(cache, "_logs"); if(fs.existsSync(logs)){const files=fs.readdirSync(logs).filter((name)=>name.endsWith("-debug-0.log")).sort(); const latest=files[files.length-1]; if(latest) process.stdout.write(fs.readFileSync(path.join(logs, latest), "utf8").slice(-65536));}', + ], + { + cwd: "/workspace", + timeout: 5_000, + captureStdio: true, + }, + ); + if (debugLog.exitCode === 0 && debugLog.stdout) { + install.stderr = `${install.stderr}\n--- npm debug log ---\n${debugLog.stdout}`; + } else if (debugLog.exitCode !== 0) { + c.log.error({ + msg: "failed to collect npm debug log", + exitCode: debugLog.exitCode, + stderr: boundedOutput(debugLog.stderr, config.maxBuildOutputBytes), + }); + } + throwCommandFailure( + "install", + `npm ${installArgs[0]}`, + install, + config.maxBuildOutputBytes, + ); + } + logBuildPhase("dependencies_installed"); + + if (plan.build) { + const result = await build.execArgv("npm", ["run", "build"], { + cwd: "/workspace", + timeout: config.buildTimeoutMs, + captureStdio: true, + }); + if (result.exitCode !== 0) { + throwCommandFailure( + "build", + "npm run build", + result, + config.maxBuildOutputBytes, + ); + } + logBuildPhase("application_built"); + } + + const prune = await build.execArgv( + "npm", + [ + "prune", + "--omit=dev", + "--omit=optional", + "--omit=peer", + "--legacy-peer-deps", + ], + { + cwd: "/workspace", + timeout: config.buildTimeoutMs, + captureStdio: true, + }, + ); + if (prune.exitCode !== 0) { + throwCommandFailure( + "install", + "npm prune --omit=dev --omit=optional", + prune, + config.maxBuildOutputBytes, + ); + } + + const nativeAddonCheck = await build.execArgv( + "node", + [ + "-e", + 'const fs=require("node:fs"); const path=require("node:path"); const found=[]; const walk=(p)=>{if(!fs.existsSync(p))return; for(const e of fs.readdirSync(p,{withFileTypes:true})){const q=path.join(p,e.name); if(e.isDirectory())walk(q); else if(e.name.endsWith(".node"))found.push(q)}}; walk("node_modules"); if(found.length){console.error(found.slice(0,32).join("\\n")); process.exit(42)}', + ], + { + cwd: "/workspace", + timeout: config.buildTimeoutMs, + captureStdio: true, + }, + ); + if (nativeAddonCheck.exitCode === 42) { + fail( + "agentos_apps_native_addon_unsupported", + "application contains native Node addons that the agentOS JavaScript runtime cannot load", + { + files: boundedOutput( + nativeAddonCheck.stderr, + config.maxBuildOutputBytes, + ), + }, + ); + } + if (nativeAddonCheck.exitCode !== 0) { + throwCommandFailure( + "build", + "native addon scan", + nativeAddonCheck, + config.maxBuildOutputBytes, + ); + } + } + + const bundleConfigPath = "/workspace/.agentos-app-build.json"; + const bundleConfigWrites = await build.writeFiles([ + { + path: bundleConfigPath, + content: JSON.stringify({ + version: release, + workspace: "/workspace", + release: "/release", + entrypoint: "runner.mjs", + staticRoot: plan.staticRoot, + sourceFiles: Object.keys(input.files), + usesRivetKit: plan.usesRivetKit, + maxOutputBytes: config.maxBuildArtifactBytes, + maxOutputFiles: DEFAULT_MAX_BUILD_ARTIFACT_FILES, + maxFileBytes: DEFAULT_MAX_BUILD_ARTIFACT_FILE_BYTES, + }), + }, + ]); + const failedBundleConfigWrite = bundleConfigWrites.find( + (entry) => !entry.success, + ); + if (failedBundleConfigWrite) { + fail( + "agentos_apps_build_write_failed", + `failed to write Apps builder input ${failedBundleConfigWrite.path}: ${failedBundleConfigWrite.error ?? "unknown error"}`, + { + path: failedBundleConfigWrite.path, + error: failedBundleConfigWrite.error, + }, + ); + } + const bundle = await build.execArgv( + "node", + ["/opt/agentos/bin/apps-builder", bundleConfigPath], + { + cwd: "/workspace", + timeout: config.buildTimeoutMs, + captureStdio: true, + }, + ); + if (bundle.exitCode !== 0) { + throwCommandFailure( + "build", + "apps-builder", + bundle, + config.maxBuildOutputBytes, + ); + } + logBuildPhase("release_bundled"); + + const packArgs = [ + "--sort=name", + "--mtime=@0", + "--owner=0", + "--group=0", + "--numeric-owner", + "-cf", + build.artifactGuestPath, + ".", + ]; + const pack = await build.execArgv("tar", packArgs, { + cwd: "/release", + timeout: config.buildTimeoutMs, + captureStdio: true, + }); + if (pack.exitCode !== 0) { + throwCommandFailure("pack", "tar", pack, config.maxBuildOutputBytes); + } + logBuildPhase("release_archived"); + + const archiveSize = await build.artifactSize(); + if ( + !Number.isSafeInteger(archiveSize) || + archiveSize < 0 || + archiveSize > config.maxBuildArtifactBytes + ) { + fail( + "agentos_apps_build_artifact_size_limit", + `built application archive is ${archiveSize} bytes, limit is maxBuildArtifactBytes ${config.maxBuildArtifactBytes}; raise maxBuildArtifactBytes or reduce deployment dependencies`, + { + artifactBytes: archiveSize, + maxBuildArtifactBytes: config.maxBuildArtifactBytes, + }, + ); + } + const sourceTar = Buffer.from(await build.readArtifact()); + if (sourceTar.byteLength !== archiveSize) { + fail( + "agentos_apps_build_artifact_truncated", + `build artifact contained ${sourceTar.byteLength} bytes, expected ${archiveSize}`, + { expectedBytes: archiveSize, actualBytes: sourceTar.byteLength }, + ); + } + const packed = packAospkgFromTarBytes(sourceTar).bytes; + const artifactHash = createHash("sha256").update(packed).digest("hex"); + await config.artifactCache?.put(release, new Uint8Array(packed)); + return { + hash: artifactHash, + size: packed.byteLength, + bytes: new Uint8Array(packed), + }; + } catch (error) { + buildError = error; + throw error; + } finally { + await build.dispose().catch((disposeError) => { + if (!buildError) throw disposeError; + c.log.error({ + msg: "failed to dispose Dynamic Apps build VM after build failure", + disposeError, + }); + }); + } +} + +function parseReadyRelease(response: VmFetchResponse): string | null { + if (response.status !== 200) return null; + try { + const value = JSON.parse(new TextDecoder().decode(response.body)) as { + release?: unknown; + }; + return typeof value.release === "string" ? value.release : null; + } catch { + return null; + } +} + +async function probeReplica(handle: ReplicaHandle): Promise { + try { + return parseReadyRelease( + await handle.vmFetch(APP_PORT, "http://agentos-app/.agentos/ready"), + ); + } catch { + return null; + } +} + +export function normalizeServerlessCallbackPath( + request: Request, +): "/api/rivet/metadata" | "/api/rivet/start" | undefined { + if (!request.headers.get("user-agent")?.startsWith("RivetEngine/")) { + return undefined; + } + const pathname = new URL(request.url).pathname; + if (request.method === "GET" && pathname.endsWith("/metadata")) { + return "/api/rivet/metadata"; + } + if ( + (request.method === "GET" || request.method === "POST") && + pathname.endsWith("/start") + ) { + return "/api/rivet/start"; + } + return undefined; +} + +function validCallbackSecret(request: Request, expected: string): boolean { + const received = request.headers.get(APP_CALLBACK_SECRET_HEADER); + if (!received || !expected) return false; + return timingSafeEqual( + createHash("sha256").update(received).digest(), + createHash("sha256").update(expected).digest(), + ); +} + +export function resolveAppCallbackSecret( + releases: ReadonlyArray<{ callbackSecret: string }>, + activeRelease?: { callbackSecret: string }, +): string { + return ( + activeRelease?.callbackSecret || + releases.find((release) => release.callbackSecret)?.callbackSecret || + randomUUID() + ); +} + +export interface DynamicAppsActors { + agentOSAppsApp: AnyActorDefinition; + agentOSAppsScaler: AnyActorDefinition; + agentOSAppsReplica: AnyActorDefinition; +} + +/** + * Defines the stable app, regional scaler, build VM, and execution-replica + * actors. Register the returned definitions in one RivetKit registry. + * + * @internal + */ +export function createAppsActors( + options: { + /** Internal development hook; setupApps() intentionally does not expose this. */ + artifactCache?: { + get(release: string): Promise; + put(release: string, artifact: Uint8Array): Promise; + }; + } = {}, +): DynamicAppsActors { + const maxSourceBytes = DEFAULT_MAX_SOURCE_BYTES; + const maxFiles = DEFAULT_MAX_FILES; + const maxVersions = DEFAULT_MAX_VERSIONS; + const maxRegions = DEFAULT_MAX_REGIONS; + const maxDependencies = DEFAULT_MAX_DEPENDENCIES; + const buildTimeoutMs = DEFAULT_BUILD_TIMEOUT_MS; + const warmTimeoutMs = DEFAULT_WARM_TIMEOUT_MS; + const warmIdleTimeoutMs = DEFAULT_WARM_IDLE_TIMEOUT_MS; + const admissionLeaseMs = DEFAULT_ADMISSION_LEASE_MS; + const maxAdmissions = DEFAULT_MAX_ADMISSIONS; + const maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES; + const maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES; + const maxBuildArtifactBytes = DEFAULT_MAX_BUILD_ARTIFACT_BYTES; + + async function forwardAppRequest( + c: AnyActorContext, + request: Request, + ): Promise { + const state = c.state as AppState; + const release = state.activeRelease + ? await getStoredRelease(c.db, state.activeRelease) + : undefined; + if (!release || release.status !== "ready") { + return new Response("Dynamic App has no active release", { status: 503 }); + } + const serverlessCallbackPath = normalizeServerlessCallbackPath(request); + if ( + serverlessCallbackPath && + !validCallbackSecret(request, release.callbackSecret) + ) { + return new Response("Unauthorized", { status: 401 }); + } + if ( + serverlessCallbackPath === "/api/rivet/metadata" && + state.serverlessMetadata?.release === release.release + ) { + return new Response( + Buffer.from(state.serverlessMetadata.bodyBase64, "base64"), + { + status: state.serverlessMetadata.status, + statusText: state.serverlessMetadata.statusText, + headers: state.serverlessMetadata.headers, + }, + ); + } + + const requestedRegion = request.headers.get("x-agentos-app-region"); + if (requestedRegion && !release.regions.includes(requestedRegion)) { + return new Response( + `Dynamic App is not deployed in requested region ${requestedRegion}`, + { status: 421 }, + ); + } + const region = requestedRegion ?? release.regions[0]; + if (!region) + return new Response("Dynamic App has no configured region", { + status: 503, + }); + + const contentLength = Number(request.headers.get("content-length") ?? 0); + if (Number.isFinite(contentLength) && contentLength > maxRequestBytes) { + return new Response("Request body exceeds Dynamic Apps limit", { + status: 413, + }); + } + const body = await readBoundedRequestBody(request, maxRequestBytes); + if (body === null) { + return new Response("Request body exceeds Dynamic Apps limit", { + status: 413, + }); + } + + const scaler = c + .client() + [SCALER_ACTOR_NAME].getOrCreate( + scalerKey(c.key[0]!, release.release, region), + { createInRegion: region }, + ); + const admission = (await scaler.acquire()) as ReplicaAdmission; + const replica = c + .client() + [REPLICA_ACTOR_NAME].getOrCreate(admission.key) as ReplicaHandle; + let released = false; + let renewalTimer: ReturnType | undefined; + const scheduleRenewal = () => { + renewalTimer = setTimeout( + () => { + void scaler + .renew(admission.admissionId) + .then(scheduleRenewal) + .catch((error: unknown) => { + c.log.error({ + msg: "failed to renew app admission lease", + error, + }); + }); + }, + Math.max(1_000, Math.floor(admission.leaseMs / 3)), + ); + }; + scheduleRenewal(); + const releaseAdmission = async () => { + if (released) return; + released = true; + if (renewalTimer) clearTimeout(renewalTimer); + try { + await scaler.release(admission.admissionId); + } catch (error) { + c.log.error({ + msg: "failed to release app admission; lease expiry will recover it", + admissionId: admission.admissionId, + error, + }); + } + }; + + try { + const forwardedHeaders = new Headers(request.headers); + forwardedHeaders.delete("x-agentos-app-region"); + forwardedHeaders.delete(APP_CALLBACK_SECRET_HEADER); + forwardedHeaders.delete("x-rivet-token"); + if (serverlessCallbackPath) forwardedHeaders.delete("authorization"); + stripHopByHopHeaders(forwardedHeaders); + const headers: Record = {}; + forwardedHeaders.forEach((value, name) => { + headers[name] = value; + }); + const forwardedUrl = new URL(request.url); + forwardedUrl.pathname = + serverlessCallbackPath ?? + `/${forwardedUrl.pathname.replace(/^\/+/, "")}`; + if (serverlessCallbackPath) { + const guestResponse = await replica.fetch(forwardedUrl, { + method: request.method, + headers, + body: body ? Buffer.from(body) : undefined, + }); + const responseHeaders = new Headers(guestResponse.headers); + stripHopByHopHeaders(responseHeaders); + if (serverlessCallbackPath === "/api/rivet/metadata") { + const responseBody = await readBoundedResponseBody( + guestResponse, + DEFAULT_MAX_SERVERLESS_METADATA_BYTES, + ); + await releaseAdmission(); + if (guestResponse.status < 200 || guestResponse.status >= 300) { + c.log.warn({ + msg: "Dynamic Apps guest metadata request failed", + status: guestResponse.status, + path: forwardedUrl.pathname, + body: new TextDecoder().decode(responseBody).slice(0, 2_048), + }); + } else { + state.serverlessMetadata = { + release: release.release, + status: guestResponse.status, + statusText: guestResponse.statusText, + headers: Object.fromEntries(responseHeaders), + bodyBase64: Buffer.from(responseBody).toString("base64"), + }; + } + return new Response(Buffer.from(responseBody), { + status: guestResponse.status, + statusText: guestResponse.statusText, + headers: responseHeaders, + }); + } + const reader = guestResponse.body?.getReader(); + const responseBody = reader + ? new ReadableStream({ + async pull(controller) { + try { + const chunk = await reader.read(); + if (chunk.done) { + controller.close(); + await releaseAdmission(); + } else { + controller.enqueue(chunk.value); + } + } catch (error) { + controller.error(error); + await releaseAdmission(); + } + }, + async cancel(reason) { + await reader.cancel(reason).catch((error) => { + c.log.error({ + msg: "failed to cancel guest RivetKit callback stream", + error, + }); + }); + await releaseAdmission(); + }, + }) + : null; + if (!reader) await releaseAdmission(); + return new Response(responseBody, { + status: guestResponse.status, + statusText: guestResponse.statusText, + headers: responseHeaders, + }); + } + const guestResponse = await replica.vmFetchStreamStart( + APP_PORT, + forwardedUrl.href, + { + method: request.method, + headers, + body, + }, + ); + const responseHeaders = new Headers( + guestResponse.rawHeaders ?? Object.entries(guestResponse.headers), + ); + stripHopByHopHeaders(responseHeaders); + if (serverlessCallbackPath === "/api/rivet/metadata") { + const chunks: Uint8Array[] = []; + let metadataBytes = 0; + try { + for (;;) { + const chunk = await replica.vmFetchStreamRead( + guestResponse.streamId, + ); + metadataBytes += chunk.body.byteLength; + if (metadataBytes > DEFAULT_MAX_SERVERLESS_METADATA_BYTES) { + await replica.vmFetchStreamCancel(guestResponse.streamId); + fail( + "agentos_apps_metadata_limit", + `RivetKit metadata exceeds ${DEFAULT_MAX_SERVERLESS_METADATA_BYTES} bytes`, + { limit: DEFAULT_MAX_SERVERLESS_METADATA_BYTES }, + ); + } + if (chunk.body.byteLength > 0) chunks.push(chunk.body); + if (chunk.done) break; + } + } finally { + await releaseAdmission(); + } + const metadataBody = new Uint8Array( + Buffer.concat(chunks, metadataBytes), + ); + const headers = Object.fromEntries(responseHeaders); + if (guestResponse.status < 200 || guestResponse.status >= 300) { + c.log.warn({ + msg: "Dynamic Apps guest metadata request failed", + status: guestResponse.status, + path: forwardedUrl.pathname, + body: new TextDecoder().decode(metadataBody).slice(0, 2_048), + }); + } + if (guestResponse.status >= 200 && guestResponse.status < 300) { + state.serverlessMetadata = { + release: release.release, + status: guestResponse.status, + statusText: guestResponse.statusText, + headers, + bodyBase64: Buffer.from(metadataBody).toString("base64"), + }; + } + return new Response(metadataBody, { + status: guestResponse.status, + statusText: guestResponse.statusText, + headers, + }); + } + const hasResponseBody = + request.method !== "HEAD" && + ![101, 204, 205, 304].includes(guestResponse.status); + let responseBytes = 0; + const responseBody = hasResponseBody + ? new ReadableStream({ + async pull(controller) { + try { + const chunk = await replica.vmFetchStreamRead( + guestResponse.streamId, + ); + responseBytes += chunk.body.byteLength; + if (responseBytes > maxResponseBytes) { + await replica.vmFetchStreamCancel(guestResponse.streamId); + throw new AgentOSAppsError( + "agentos_apps_response_limit", + `response exceeds maxResponseBytes ${maxResponseBytes}; raise maxResponseBytes to allow a larger response`, + { maxResponseBytes }, + ); + } + if (chunk.body.byteLength) controller.enqueue(chunk.body); + if (chunk.done) { + await releaseAdmission(); + controller.close(); + } + } catch (error) { + await replica + .vmFetchStreamCancel(guestResponse.streamId) + .catch((cancelError) => { + c.log.error({ + msg: "failed to cancel app response stream", + cancelError, + }); + }); + await releaseAdmission().catch((releaseError) => { + c.log.error({ + msg: "failed to release app admission", + releaseError, + }); + }); + controller.error(error); + } + }, + async cancel() { + try { + await replica.vmFetchStreamCancel(guestResponse.streamId); + } finally { + await releaseAdmission(); + } + }, + }) + : null; + if (!hasResponseBody) { + await replica.vmFetchStreamCancel(guestResponse.streamId); + await releaseAdmission(); + } + responseHeaders.set("x-agentos-app-replica", admission.key.join("/")); + responseHeaders.set("x-agentos-app-release", admission.release); + responseHeaders.set( + "x-agentos-app-replica-count", + String(admission.replicaCount), + ); + responseHeaders.set( + "x-agentos-app-queue-delay-ms", + String(admission.queueDelayMs), + ); + responseHeaders.set( + "x-agentos-app-cold-start", + admission.coldStart ? "1" : "0", + ); + return new Response(responseBody, { + status: guestResponse.status, + statusText: guestResponse.statusText, + headers: responseHeaders, + }); + } catch (error) { + await releaseAdmission().catch((releaseError) => { + c.log.error({ + msg: "failed to release app admission", + releaseError, + }); + }); + throw error; + } + } + + const agentOSAppsApp = actor({ + inspector: { + tabs: [ + { + id: "agentos-app", + label: "Dynamic App", + icon: "box", + source: `${INSPECTOR_ROOT}/deployment`, + }, + ], + }, + options: { + actionTimeout: buildTimeoutMs + warmTimeoutMs + 60_000, + }, + db: db({ onMigrate: migrateAppsTables }), + createState: (): AppState => ({ + activeRelease: null, + namespace: null, + revision: 0, + nextEnvoyVersion: 1, + }), + onRequest: forwardAppRequest, + actions: { + deploy: async ( + c: AnyActorContext, + input: PreparedDeployAppInput, + ): Promise => + c.keepAwake( + serialized(`app:${c.actorId}`, async () => { + const appId = c.key[0]; + if (!appId || c.key.length !== 1 || input.appId !== appId) { + fail( + "agentos_apps_app_id_mismatch", + "deployApp appId must match the stable application actor key", + { appId: input.appId, actorKey: c.key }, + ); + } + const plan = validateDeployment(input, { + maxSourceBytes, + maxFiles, + maxDependencies, + }); + const regions = normalizeRegions( + input.regions, + c.region, + maxRegions, + ); + const scaling = normalizeScaling(input.scaling); + const releaseId = canonicalDeploymentHash({ + files: input.files, + entrypoint: plan.entrypoint, + build: plan.build, + staticRoot: plan.staticRoot, + packagingIdentity: [ + `apps-builder@${appsBuilderVersion}`, + `manifest@${appBundleManifestVersion}`, + "bundle@2", + "esbuild-wasm@0.27.4", + "rivetkit-adapter@6", + ].join(";"), + deploymentIdentity: JSON.stringify({ + regions, + scaling, + namespace: input.namespace, + runtime: input.runtime, + usesRivetKit: plan.usesRivetKit, + }), + }); + const state = c.state as AppState; + const releasesBefore = await listStoredReleases(c.db); + state.nextEnvoyVersion ??= + Math.max( + 0, + ...releasesBefore.map((candidate) => candidate.envoyVersion), + ) + 1; + const previousReleaseId = state.activeRelease; + const previousServerlessMetadata = state.serverlessMetadata; + const previousRelease = previousReleaseId + ? await getStoredRelease(c.db, previousReleaseId) + : undefined; + const callbackSecret = resolveAppCallbackSecret( + releasesBefore, + previousRelease, + ); + if (state.namespace && state.namespace !== input.namespace) { + fail( + "agentos_apps_namespace_changed", + "an appId cannot be reassigned to a different Rivet namespace", + { + appId, + expected: state.namespace, + received: input.namespace, + }, + ); + } + state.namespace = input.namespace; + let release = await getStoredRelease(c.db, releaseId); + if (release && release.callbackSecret !== callbackSecret) { + release.callbackSecret = callbackSecret; + await c.db.execute( + `UPDATE agentos_apps_releases + SET callback_secret = ? + WHERE release_id = ?`, + release.callbackSecret, + releaseId, + ); + } + const previousRegions = [...(release?.regions ?? [])]; + const envoyVersion = + previousReleaseId === releaseId && release + ? release.envoyVersion + : state.nextEnvoyVersion++; + + if (!release || release.status !== "ready") { + release = { + release: releaseId, + artifactHash: "", + artifactBytes: 0, + createdAt: release?.createdAt ?? Date.now(), + regions, + scaling, + status: "building", + entrypoint: plan.entrypoint, + namespace: input.namespace, + envoyVersion, + runtimeEndpoint: input.runtime.endpoint, + runtimePool: input.runtime.pool, + usesRivetKit: plan.usesRivetKit, + callbackSecret, + }; + const releaseCreatedAt = release.createdAt; + await deleteArtifactChunksBatched(c.db, releaseId); + await c.db.transaction(async (tx) => { + await tx.execute( + `INSERT INTO agentos_apps_releases ( + release_id, created_at, status, entrypoint, + artifact_hash, artifact_bytes, build_error, + regions_json, scaling_json, namespace, envoy_version, + runtime_endpoint, runtime_pool, callback_secret, + uses_rivetkit + ) VALUES (?, ?, ?, ?, '', 0, NULL, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(release_id) DO UPDATE SET + status = excluded.status, + entrypoint = excluded.entrypoint, + build_error = NULL, + regions_json = excluded.regions_json, + scaling_json = excluded.scaling_json, + namespace = excluded.namespace, + envoy_version = excluded.envoy_version, + runtime_endpoint = excluded.runtime_endpoint, + runtime_pool = excluded.runtime_pool, + callback_secret = excluded.callback_secret, + uses_rivetkit = excluded.uses_rivetkit`, + releaseId, + releaseCreatedAt, + "building", + plan.entrypoint, + JSON.stringify(regions), + JSON.stringify(scaling), + input.namespace, + envoyVersion, + input.runtime.endpoint, + input.runtime.pool, + callbackSecret, + plan.usesRivetKit ? 1 : 0, + ); + }); + await deleteReleaseFilesBatched(c.db, releaseId); + await persistReleaseFilesBatched(c.db, releaseId, input.files); + try { + const artifact = await buildRelease(c, input, plan, releaseId, { + createBuildVm, + buildTimeoutMs, + maxRequestBytes, + maxResponseBytes, + maxBuildOutputBytes: DEFAULT_MAX_BUILD_OUTPUT_BYTES, + maxBuildArtifactBytes, + artifactCache: options.artifactCache, + }); + const chunkCount = Math.ceil( + artifact.size / ARTIFACT_CHUNK_BYTES, + ); + if (chunkCount > MAX_ARTIFACT_CHUNKS) { + fail( + "agentos_apps_artifact_chunk_limit", + `artifact requires ${chunkCount} chunks, exceeding ${MAX_ARTIFACT_CHUNKS}`, + { observed: chunkCount, limit: MAX_ARTIFACT_CHUNKS }, + ); + } + await deleteArtifactChunksBatched(c.db, releaseId); + for ( + let firstChunk = 0; + firstChunk < chunkCount; + firstChunk += ARTIFACT_CHUNKS_PER_TRANSACTION + ) { + await c.db.transaction(async (tx) => { + const endChunk = Math.min( + chunkCount, + firstChunk + ARTIFACT_CHUNKS_PER_TRANSACTION, + ); + for (let index = firstChunk; index < endChunk; index += 1) { + const offset = index * ARTIFACT_CHUNK_BYTES; + const content = artifact.bytes.slice( + offset, + offset + ARTIFACT_CHUNK_BYTES, + ); + await tx.execute( + `INSERT INTO agentos_apps_artifact_chunks + (release_id, chunk_index, content, byte_length) + VALUES (?, ?, ?, ?)`, + releaseId, + index, + content, + content.byteLength, + ); + } + }); + } + const totals = await c.db.execute<{ + bytes: number; + chunks: number; + }>( + `SELECT COALESCE(SUM(byte_length), 0) AS bytes, + COUNT(*) AS chunks + FROM agentos_apps_artifact_chunks + WHERE release_id = ?`, + releaseId, + ); + if ( + Number(totals[0]?.bytes ?? 0) !== artifact.size || + Number(totals[0]?.chunks ?? 0) !== chunkCount + ) { + fail( + "agentos_apps_artifact_persist_mismatch", + "persisted artifact chunks failed length verification", + { + expectedBytes: artifact.size, + actualBytes: Number(totals[0]?.bytes ?? 0), + }, + ); + } + await c.db.execute( + `UPDATE agentos_apps_releases + SET status = 'ready', artifact_hash = ?, + artifact_bytes = ?, build_error = NULL + WHERE release_id = ?`, + artifact.hash, + artifact.size, + releaseId, + ); + release.artifactHash = artifact.hash; + release.artifactBytes = artifact.size; + release.status = "ready"; + delete release.error; + } catch (error) { + release.status = "failed"; + release.error = + error instanceof Error ? error.message : String(error); + await deleteArtifactChunksBatched(c.db, releaseId); + await c.db.execute( + `UPDATE agentos_apps_releases + SET status = 'failed', build_error = ? + WHERE release_id = ?`, + release.error, + releaseId, + ); + c.log.error({ + msg: "Dynamic App build failed", + release: releaseId, + error, + }); + throw error; + } + } else { + release = { + ...release, + regions, + scaling, + envoyVersion, + runtimeEndpoint: input.runtime.endpoint, + runtimePool: input.runtime.pool, + usesRivetKit: plan.usesRivetKit, + }; + await c.db.execute( + `UPDATE agentos_apps_releases + SET regions_json = ?, scaling_json = ?, envoy_version = ?, + runtime_endpoint = ?, runtime_pool = ?, + uses_rivetkit = ? + WHERE release_id = ?`, + JSON.stringify(regions), + JSON.stringify(scaling), + envoyVersion, + input.runtime.endpoint, + input.runtime.pool, + plan.usesRivetKit ? 1 : 0, + releaseId, + ); + } + + const rolloutRelease: StoredAppRelease = { + ...release, + regions, + scaling, + }; + const client = c.client(); + const rolloutResults = await Promise.allSettled( + regions.map((region) => + client[SCALER_ACTOR_NAME] + .getOrCreate(scalerKey(appId, releaseId, region), { + createInRegion: region, + }) + .prepare({ + appId, + release: rolloutRelease, + region, + verifyReplica: true, + }), + ), + ); + const rolloutFailure = rolloutResults.find( + (result) => result.status === "rejected", + ); + if (rolloutFailure?.status === "rejected") { + const cleanupRegions = + previousReleaseId === releaseId + ? regions.filter( + (region) => !previousRegions.includes(region), + ) + : regions; + const cleanup = await Promise.allSettled( + cleanupRegions.map((region) => + client[SCALER_ACTOR_NAME] + .getOrCreate(scalerKey(appId, releaseId, region), { + createInRegion: region, + }) + .retire(), + ), + ); + for (const result of cleanup) { + if (result.status === "rejected") { + c.log.error({ + msg: "failed to clean up an unsuccessful Dynamic Apps rollout", + release: releaseId, + error: result.reason, + }); + } + } + throw rolloutFailure.reason; + } + if (plan.usesRivetKit) { + const connection = resolveDefaultRivetConnection(); + if ( + connection.endpoint.replace(/\/$/, "") !== + input.runtime.endpoint.replace(/\/$/, "") + ) { + fail( + "agentos_apps_runtime_changed", + "the app actor Rivet endpoint does not match the deployment runtime", + { + expected: connection.endpoint, + received: input.runtime.endpoint, + }, + ); + } + // The Engine starts polling metadata as soon as the runner + // config is written. Make this healthy release visible for + // that handshake, then roll back if configuration fails. + state.activeRelease = releaseId; + state.serverlessMetadata = undefined; + try { + await configureAppNamespaceRunner( + c.actorId, + { + endpoint: input.runtime.endpoint, + namespace: input.namespace, + pool: input.runtime.pool, + }, + release.callbackSecret, + connection, + ); + } catch (error) { + state.activeRelease = previousReleaseId; + state.serverlessMetadata = previousServerlessMetadata; + const cleanup = await Promise.allSettled( + regions.map((region) => + client[SCALER_ACTOR_NAME] + .getOrCreate(scalerKey(appId, releaseId, region), { + createInRegion: region, + }) + .retire(), + ), + ); + for (const result of cleanup) { + if (result.status === "rejected") { + c.log.error({ + msg: "failed to clean up a Dynamic Apps rollout after runner configuration failed", + release: releaseId, + error: result.reason, + }); + } + } + throw error; + } + } + state.activeRelease = releaseId; + if (!plan.usesRivetKit) state.serverlessMetadata = undefined; + state.revision += 1; + const retiredRegions = + previousRelease && previousRelease.release !== releaseId + ? previousRelease.regions.map((region) => ({ + release: previousRelease.release, + region, + })) + : previousRegions + .filter((region) => !regions.includes(region)) + .map((region) => ({ release: releaseId, region })); + if (retiredRegions.length > 0) { + const retirements = await Promise.allSettled( + retiredRegions.map((retirement) => + client[SCALER_ACTOR_NAME] + .getOrCreate( + scalerKey(appId, retirement.release, retirement.region), + { createInRegion: retirement.region }, + ) + .retire(), + ), + ); + for (const retirement of retirements) { + if (retirement.status === "rejected") { + c.log.error({ + msg: "failed to retire an inactive Dynamic Apps release", + release: releaseId, + error: retirement.reason, + }); + } + } + } + const releases = await listStoredReleases(c.db); + if (releases.length > maxVersions) { + const removable = releases + .filter((candidate) => candidate.release !== releaseId) + .sort((a, b) => a.createdAt - b.createdAt); + let retained = releases.length; + while (retained > maxVersions) { + const candidate = removable.shift(); + if (!candidate) break; + const retirements = await Promise.allSettled( + candidate.regions.map((region) => + client[SCALER_ACTOR_NAME] + .getOrCreate( + scalerKey(appId, candidate.release, region), + { createInRegion: region }, + ) + .retire(), + ), + ); + const stillReferenced = retirements.some( + (result) => + result.status === "rejected" || + Number(result.value?.drainingReplicas ?? 0) > 0, + ); + if (stillReferenced) { + c.log.warn({ + msg: "deferred Dynamic Apps release garbage collection while replicas are still draining", + appId, + release: candidate.release, + }); + continue; + } + await deleteArtifactChunksBatched(c.db, candidate.release); + await deleteReleaseFilesBatched(c.db, candidate.release); + await c.db.execute( + "DELETE FROM agentos_apps_releases WHERE release_id = ?", + candidate.release, + ); + retained -= 1; + } + } + return { + appId, + release: releaseId, + namespace: input.namespace, + pool: input.runtime.pool, + regions, + appActorId: c.actorId, + usesRivetKit: plan.usesRivetKit, + }; + }), + ), + resolveDeployment: async ( + c: AnyActorContext, + requestedRegion?: string, + ) => { + const state = c.state as AppState; + const release = state.activeRelease + ? await getStoredRelease(c.db, state.activeRelease) + : undefined; + if (!release || release.status !== "ready") { + fail( + "agentos_apps_not_deployed", + "app has no active release; call app.deploy() first", + ); + } + if (requestedRegion && !release.regions.includes(requestedRegion)) { + fail( + "agentos_apps_region_not_deployed", + `app is not deployed in requested region ${requestedRegion}`, + { requestedRegion, regions: release.regions }, + ); + } + const region = requestedRegion ?? release.regions[0]; + if (!region) fail("agentos_apps_no_region", "active app has no region"); + return { + appId: c.key[0], + release: release.release, + region, + scalerKey: scalerKey(c.key[0]!, release.release, region), + revision: state.revision, + maxRequestBytes, + maxResponseBytes, + }; + }, + getRelease: async (c: AnyActorContext, releaseId: string) => { + const release = await getStoredRelease(c.db, releaseId); + if (!release) { + fail( + "agentos_apps_release_not_found", + `app release ${releaseId} was not found`, + ); + } + const { callbackSecret: _callbackSecret, ...publicRelease } = release; + return publicRelease; + }, + getArtifactManifest: async (c: AnyActorContext, releaseId: string) => { + const release = await getStoredRelease(c.db, releaseId); + if (!release || release.status !== "ready") { + fail( + "agentos_apps_artifact_not_ready", + `artifact for release ${releaseId} is not ready`, + ); + } + const rows = await c.db.execute<{ chunks: number; bytes: number }>( + `SELECT COUNT(*) AS chunks, + COALESCE(SUM(byte_length), 0) AS bytes + FROM agentos_apps_artifact_chunks WHERE release_id = ?`, + releaseId, + ); + const chunks = Number(rows[0]?.chunks ?? 0); + const bytes = Number(rows[0]?.bytes ?? 0); + if (chunks > MAX_ARTIFACT_CHUNKS || bytes !== release.artifactBytes) { + fail( + "agentos_apps_artifact_manifest_invalid", + `artifact ${releaseId} failed persisted manifest validation`, + { chunks, bytes, expectedBytes: release.artifactBytes }, + ); + } + return { + hash: release.artifactHash, + bytes, + chunks, + chunkBytes: ARTIFACT_CHUNK_BYTES, + }; + }, + readArtifactChunk: async ( + c: AnyActorContext, + releaseId: string, + index: number, + ) => { + if ( + !Number.isInteger(index) || + index < 0 || + index >= MAX_ARTIFACT_CHUNKS + ) { + fail( + "agentos_apps_invalid_artifact_chunk", + `artifact chunk index must be between 0 and ${MAX_ARTIFACT_CHUNKS - 1}`, + { index }, + ); + } + const rows = await c.db.execute<{ + content: Uint8Array; + byte_length: number; + }>( + `SELECT content, byte_length + FROM agentos_apps_artifact_chunks + WHERE release_id = ? AND chunk_index = ?`, + releaseId, + index, + ); + const row = rows[0]; + if (!row) { + fail( + "agentos_apps_artifact_chunk_not_found", + `artifact chunk ${index} for release ${releaseId} was not found`, + ); + } + const content = new Uint8Array(row.content); + if ( + content.byteLength !== Number(row.byte_length) || + content.byteLength > ARTIFACT_CHUNK_BYTES + ) { + fail( + "agentos_apps_artifact_chunk_invalid", + `artifact chunk ${index} failed length validation`, + ); + } + return content; + }, + inspect: async (c: AnyActorContext) => { + const state = c.state as AppState; + return { + activeRelease: state.activeRelease, + namespace: state.namespace, + revision: state.revision, + releases: (await listStoredReleases(c.db)).map( + ({ + entrypoint: _entrypoint, + namespace: _namespace, + callbackSecret: _callbackSecret, + ...release + }) => release, + ), + }; + }, + }, + }); + + function updateCapacityWarning( + c: AnyActorContext, + state: ReturnType, + ): void { + const provisioned = state.replicas.length + state.warmingReplicas; + const aboveHalf = provisioned > state.scaling.maxReplicas / 2; + if (aboveHalf && !state.capacityWarningLatched) { + state.capacityWarningLatched = true; + c.log.warn({ + msg: `Dynamic Apps scaler is above 50% of maxReplicas ${state.scaling.maxReplicas}; raise scaling.maxReplicas if this app needs more capacity`, + appId: state.appId, + release: state.release, + region: state.region, + readyReplicas: state.replicas.length, + warmingReplicas: state.warmingReplicas, + maxReplicas: state.scaling.maxReplicas, + utilizationPercent: (provisioned / state.scaling.maxReplicas) * 100, + }); + } else if (!aboveHalf && state.capacityWarningLatched) { + state.capacityWarningLatched = false; + } + } + + async function reserveReplica( + c: AnyActorContext, + ): Promise<{ index: number; key: string[] } | null> { + return serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + if ( + state.replicas.length + state.warmingReplicas >= + state.scaling.maxReplicas + ) { + return null; + } + const index = state.nextReplicaIndex++; + const key = replicaKey(state.appId, state.release, state.region, index); + state.warmingReplicaKeys ??= []; + state.warmingReplicaKeys.push(key); + state.warmingReplicas = state.warmingReplicaKeys.length; + state.revision += 1; + updateCapacityWarning(c, state); + return { index, key }; + }); + } + + async function warmReservedReplica( + c: AnyActorContext, + reservation: { index: number; key: string[] }, + ): Promise { + const initial = requireScalerState(c); + const { appId, release: releaseId, region } = initial; + const client = c.client(); + const key = reservation.key; + let handle: ReplicaHandle | undefined; + try { + const release = (await client[APP_ACTOR_NAME] + .getOrCreate([appId]) + .getRelease(releaseId)) as StoredAppRelease; + handle = client[REPLICA_ACTOR_NAME].getOrCreate(key, { + createInRegion: region, + }) as ReplicaHandle; + await handle.configure({ + appId, + release: release.release, + artifactHash: release.artifactHash, + artifactBytes: release.artifactBytes, + namespace: release.namespace, + envoyVersion: release.envoyVersion, + usesRivetKit: release.usesRivetKit, + runtime: { + namespace: release.namespace, + endpoint: release.runtimeEndpoint, + pool: release.runtimePool, + }, + }); + const inspection = await handle.inspect(); + if ((await probeReplica(handle)) !== release.release) { + const deadline = Date.now() + warmTimeoutMs; + await new Promise((resolve) => setTimeout(resolve, 200)); + while (Date.now() < deadline) { + if ((await probeReplica(handle)) === release.release) break; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + if ((await probeReplica(handle)) !== release.release) { + fail( + "agentos_apps_replica_warm_timeout", + `execution replica did not become ready within warmTimeoutMs ${warmTimeoutMs}`, + { release: release.release, warmTimeoutMs }, + ); + } + } + if (inspection.startedAt === null) await handle.markStarted(); + const registered = await serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + state.warmingReplicaKeys = (state.warmingReplicaKeys ?? []).filter( + (candidate) => candidate.join("\0") !== key.join("\0"), + ); + state.warmingReplicas = state.warmingReplicaKeys.length; + if ( + state.retired || + state.appId !== appId || + state.release !== releaseId || + state.region !== region + ) { + state.revision += 1; + updateCapacityWarning(c, state); + return false; + } + const now = Date.now(); + state.replicas.push({ + key, + readyAt: now, + activeRequests: 0, + lastUsedAt: now, + draining: false, + }); + state.revision += 1; + updateCapacityWarning(c, state); + return true; + }); + if (!registered) { + await handle.destroy(); + await serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + if ( + state.retired && + state.replicas.length === 0 && + state.warmingReplicas === 0 + ) { + c.destroy(); + } + }); + return false; + } + return true; + } catch (error) { + await handle?.destroy().catch((destroyError) => { + c.log.error({ + msg: "failed to destroy unsuccessful Dynamic Apps replica", + appId, + release: releaseId, + region, + destroyError, + }); + }); + await serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + state.warmingReplicaKeys = (state.warmingReplicaKeys ?? []).filter( + (candidate) => candidate.join("\0") !== key.join("\0"), + ); + state.warmingReplicas = state.warmingReplicaKeys.length; + state.revision += 1; + updateCapacityWarning(c, state); + if ( + state.retired && + state.replicas.length === 0 && + state.warmingReplicas === 0 + ) { + c.destroy(); + } + }); + throw error; + } + } + + async function addReplica(c: AnyActorContext): Promise { + const reservation = await reserveReplica(c); + return reservation === null ? false : warmReservedReplica(c, reservation); + } + + function requireScalerState(c: AnyActorContext): ScalerState & { + appId: string; + release: string; + region: string; + scaling: Required; + } { + const state = c.state as ScalerState; + if (!state.appId || !state.release || !state.region || !state.scaling) { + fail( + "agentos_apps_scaler_uninitialized", + "regional scaler has not been prepared by an app deployment", + ); + } + state.selectionCursor ??= 0; + state.nextReplicaIndex ??= + Math.max( + -1, + ...state.replicas.map((replica) => Number(replica.key.at(-1) ?? -1)), + ) + 1; + state.reconcileScheduledAt ??= null; + state.admissions ??= {}; + state.warmingReplicaKeys ??= []; + state.warmingReplicas = state.warmingReplicaKeys.length; + state.capacityWarningLatched ??= false; + for (const replica of state.replicas) { + replica.activeRequests ??= 0; + replica.lastUsedAt ??= replica.readyAt; + replica.draining ??= false; + } + return state as ScalerState & { + appId: string; + release: string; + region: string; + scaling: Required; + }; + } + + function expireAdmissions( + c: AnyActorContext, + state: ReturnType, + ): number { + const now = Date.now(); + let expired = 0; + for (const [id, admission] of Object.entries(state.admissions ?? {})) { + if (admission.expiresAt > now) continue; + delete state.admissions?.[id]; + const replica = state.replicas.find( + (candidate) => + candidate.key.join("\0") === admission.replicaKey.join("\0"), + ); + if (replica) { + replica.activeRequests = Math.max(0, replica.activeRequests - 1); + replica.lastUsedAt = now; + } + expired += 1; + } + if (expired > 0) { + state.revision += 1; + c.log.warn({ + msg: "expired abandoned Dynamic Apps admissions", + expired, + admissionLeaseMs, + }); + } + return expired; + } + + async function reconcileIdleReplicas( + c: AnyActorContext, + state: ReturnType, + ): Promise { + expireAdmissions(c, state); + let removed = 0; + const now = Date.now(); + const candidates = state.replicas + .filter( + (replica) => + replica.activeRequests === 0 && + (replica.draining || now - replica.lastUsedAt >= warmIdleTimeoutMs), + ) + .sort((a, b) => a.lastUsedAt - b.lastUsedAt); + const maxRemoval = Math.max(1, Math.ceil(state.replicas.length * 0.25)); + while ( + state.replicas.length > state.scaling.minReplicas && + candidates.length > 0 && + removed < maxRemoval + ) { + const candidate = candidates.shift(); + if (!candidate) break; + const index = state.replicas.findIndex( + (replica) => replica.key.join("\0") === candidate.key.join("\0"), + ); + if (index >= 0) { + const handle = c + .client() + [REPLICA_ACTOR_NAME].getOrCreate(candidate.key) as ReplicaHandle; + await handle.destroy(); + state.replicas.splice(index, 1); + removed += 1; + } + } + if (removed > 0) { + state.revision += 1; + updateCapacityWarning(c, state); + } + return removed; + } + + const agentOSAppsScaler = actor({ + inspector: { + tabs: [ + { + id: "agentos-scaler", + label: "Regional scaler", + icon: "activity", + source: `${INSPECTOR_ROOT}/scaler`, + }, + ], + }, + options: { + actionTimeout: warmTimeoutMs + 30_000, + }, + createState: (): ScalerState => ({ + appId: null, + release: null, + region: null, + scaling: null, + replicas: [], + warmingReplicas: 0, + warmingReplicaKeys: [], + capacityWarningLatched: false, + retired: false, + revision: 0, + selectionCursor: 0, + nextReplicaIndex: 0, + reconcileScheduledAt: null, + admissions: {}, + }), + onWake: async (c: AnyActorContext) => { + const state = c.state as ScalerState; + const strandedKeys = [...(state.warmingReplicaKeys ?? [])]; + const strandedCount = Math.max( + state.warmingReplicas ?? 0, + strandedKeys.length, + ); + state.warmingReplicaKeys = []; + state.warmingReplicas = 0; + if (strandedCount > 0) { + c.log.warn({ + msg: "recovering stranded Dynamic Apps replica warm reservations", + strandedReservations: strandedCount, + }); + const cleanup = await Promise.allSettled( + strandedKeys.map((key) => + ( + c.client()[REPLICA_ACTOR_NAME].getOrCreate(key) as ReplicaHandle + ).destroy(), + ), + ); + for (const result of cleanup) { + if (result.status === "rejected") { + c.log.error({ + msg: "failed to destroy a stranded Dynamic Apps warming replica", + error: result.reason, + }); + } + } + state.revision += 1; + } + if ( + state.appId && + state.release && + state.region && + state.scaling && + !state.retired && + state.replicas.length < state.scaling.minReplicas + ) { + state.reconcileScheduledAt = Date.now() + 1; + await c.schedule.after(1, "reconcile"); + } + if ( + state.retired && + state.replicas.length === 0 && + state.warmingReplicas === 0 + ) { + c.destroy(); + } + }, + actions: { + prepare: async ( + c: AnyActorContext, + input: { + appId: string; + release: StoredAppRelease; + region: string; + verifyReplica?: boolean; + }, + ) => + c.keepAwake( + (async () => { + await serialized(`scaler:${c.actorId}`, async () => { + const state = c.state as ScalerState; + if (state.release && state.release !== input.release.release) { + fail( + "agentos_apps_scaler_key_collision", + "regional scaler key was reused for a different release", + ); + } + state.appId = input.appId; + state.release = input.release.release; + state.region = input.region; + state.scaling = input.release.scaling; + state.retired = false; + state.selectionCursor ??= 0; + state.nextReplicaIndex ??= + Math.max( + -1, + ...state.replicas.map((replica) => + Number(replica.key.at(-1) ?? -1), + ), + ) + 1; + state.reconcileScheduledAt ??= null; + state.admissions ??= {}; + state.warmingReplicaKeys ??= []; + state.warmingReplicas = state.warmingReplicaKeys.length; + state.capacityWarningLatched ??= false; + for (const replica of state.replicas) { + replica.activeRequests ??= 0; + replica.lastUsedAt ??= replica.readyAt; + replica.draining ??= false; + } + }); + const target = Math.max( + input.release.scaling.minReplicas, + input.verifyReplica ? 1 : 0, + ); + while ( + (c.state as ScalerState).replicas.length + + (c.state as ScalerState).warmingReplicas < + target + ) { + if (!(await addReplica(c))) break; + } + const state = requireScalerState(c); + if ( + state.replicas.length > state.scaling.minReplicas && + state.reconcileScheduledAt === null + ) { + state.reconcileScheduledAt = Date.now() + warmIdleTimeoutMs; + await c.schedule.after(warmIdleTimeoutMs, "reconcile"); + } + return { + release: state.release, + region: state.region, + readyReplicas: state.replicas.length, + }; + })(), + ), + acquire: async (c: AnyActorContext) => + c.keepAwake( + (async () => { + const startedAt = Date.now(); + await serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + expireAdmissions(c, state); + if (Object.keys(state.admissions ?? {}).length >= maxAdmissions) { + fail( + "agentos_apps_admission_limit", + `regional scaler reached maxAdmissions ${maxAdmissions}; raise maxAdmissions or investigate abandoned requests`, + { maxAdmissions }, + ); + } + await reconcileIdleReplicas(c, state); + }); + let coldStart = false; + if ((c.state as ScalerState).replicas.length === 0) { + coldStart = await addReplica(c); + if (!coldStart) { + const deadline = Date.now() + warmTimeoutMs; + while ( + (c.state as ScalerState).replicas.length === 0 && + Date.now() < deadline + ) { + await new Promise((resolve) => setTimeout(resolve, 25)); + } + } + } + const shouldWarm = await serialized( + `scaler:${c.actorId}`, + async () => { + const state = requireScalerState(c); + const candidates = state.replicas.filter( + (replica) => !replica.draining, + ); + const minimum = + candidates.length === 0 + ? Number.POSITIVE_INFINITY + : Math.min( + ...candidates.map((replica) => replica.activeRequests), + ); + return ( + minimum + 1 >= state.scaling.targetConcurrency && + state.replicas.length + state.warmingReplicas < + state.scaling.maxReplicas + ); + }, + ); + if (shouldWarm) { + void c.keepAwake(addReplica(c)).catch((error) => { + c.log.error({ + msg: "Dynamic Apps background replica warm failed", + error, + }); + }); + } + return serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + let candidates = state.replicas.filter( + (replica) => !replica.draining, + ); + if (candidates.length === 0) { + const reusable = state.replicas.find( + (replica) => replica.draining && replica.activeRequests === 0, + ); + if (reusable) { + reusable.draining = false; + candidates = [reusable]; + } + } + if (candidates.length === 0) { + fail( + "agentos_apps_no_capacity", + "regional scaler has no ready replicas", + ); + } + const minimum = Math.min( + ...candidates.map((replica) => replica.activeRequests), + ); + const leastLoaded = candidates.filter( + (replica) => replica.activeRequests === minimum, + ); + const selected = + leastLoaded[state.selectionCursor++ % leastLoaded.length]; + if (!selected) + fail( + "agentos_apps_no_capacity", + "regional scaler has no ready replicas", + ); + selected.activeRequests += 1; + selected.lastUsedAt = Date.now(); + const admissionId = randomUUID(); + state.admissions ??= {}; + state.admissions[admissionId] = { + id: admissionId, + replicaKey: selected.key, + expiresAt: Date.now() + admissionLeaseMs, + }; + if (state.reconcileScheduledAt === null) { + state.reconcileScheduledAt = Date.now() + admissionLeaseMs; + await c.schedule.after(admissionLeaseMs, "reconcile"); + } + state.revision += 1; + return { + admissionId, + leaseMs: admissionLeaseMs, + key: selected.key, + release: state.release, + region: state.region, + replicaCount: state.replicas.length, + queueDelayMs: Date.now() - startedAt, + coldStart, + } satisfies ReplicaAdmission; + }); + })(), + ), + renew: async (c: AnyActorContext, admissionId: string) => + c.keepAwake( + serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + expireAdmissions(c, state); + const admission = state.admissions?.[admissionId]; + if (!admission) return { renewed: false }; + admission.expiresAt = Date.now() + admissionLeaseMs; + return { renewed: true, expiresAt: admission.expiresAt }; + }), + ), + release: async (c: AnyActorContext, admissionId: string) => + c.keepAwake( + serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + expireAdmissions(c, state); + const admission = state.admissions?.[admissionId]; + if (!admission) return { released: false }; + delete state.admissions?.[admissionId]; + const replica = state.replicas.find( + (candidate) => + candidate.key.join("\0") === admission.replicaKey.join("\0"), + ); + if (!replica) return { released: false }; + replica.activeRequests = Math.max(0, replica.activeRequests - 1); + replica.lastUsedAt = Date.now(); + state.revision += 1; + if (replica.draining) await reconcileIdleReplicas(c, state); + if ( + state.replicas.length > state.scaling.minReplicas && + state.reconcileScheduledAt === null + ) { + state.reconcileScheduledAt = Date.now() + warmIdleTimeoutMs; + await c.schedule.after(warmIdleTimeoutMs, "reconcile"); + } + if ( + state.retired && + state.replicas.length === 0 && + state.warmingReplicas === 0 + ) { + c.destroy(); + } + return { released: true }; + }), + ), + reconcile: async (c: AnyActorContext) => + c.keepAwake( + serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + state.reconcileScheduledAt = null; + const removed = await reconcileIdleReplicas(c, state); + const missing = Math.max( + 0, + state.scaling.minReplicas - + state.replicas.length - + state.warmingReplicas, + ); + const deadlines = Object.values(state.admissions ?? {}).map( + (admission) => admission.expiresAt, + ); + if (state.replicas.length > state.scaling.minReplicas) { + deadlines.push( + ...state.replicas.map((replica) => + replica.activeRequests === 0 + ? replica.lastUsedAt + warmIdleTimeoutMs + : Date.now() + warmIdleTimeoutMs, + ), + ); + } + if (deadlines.length > 0) { + const nextDelay = Math.max( + 1, + Math.min(...deadlines) - Date.now(), + ); + state.reconcileScheduledAt = Date.now() + nextDelay; + await c.schedule.after(nextDelay, "reconcile"); + } + for (let index = 0; index < missing; index += 1) { + void c.keepAwake(addReplica(c)).catch((error) => { + c.log.error({ + msg: "Dynamic Apps minimum replica warm failed", + error, + }); + }); + } + if ( + state.retired && + state.replicas.length === 0 && + state.warmingReplicas === 0 + ) { + c.destroy(); + } + return { removed, readyReplicas: state.replicas.length }; + }), + ), + drainReplica: async (c: AnyActorContext, key: string[]) => + c.keepAwake( + serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + const replica = state.replicas.find( + (candidate) => candidate.key.join("\0") === key.join("\0"), + ); + if (!replica) { + fail( + "agentos_apps_replica_not_found", + "replica is not in this scaler", + ); + } + if (state.replicas.length <= state.scaling.minReplicas) { + void c.keepAwake(addReplica(c)).catch((error) => { + c.log.error({ + msg: "Dynamic Apps replacement replica warm failed", + error, + }); + }); + } + replica.draining = true; + state.revision += 1; + await reconcileIdleReplicas(c, state); + return { draining: replica.activeRequests > 0 }; + }), + ), + retire: async (c: AnyActorContext) => + c.keepAwake( + serialized(`scaler:${c.actorId}`, async () => { + const state = requireScalerState(c); + state.scaling = { ...state.scaling, minReplicas: 0 }; + state.retired = true; + for (const replica of state.replicas) replica.draining = true; + state.revision += 1; + const removed = await reconcileIdleReplicas(c, state); + if (state.replicas.length === 0 && state.warmingReplicas === 0) + c.destroy(); + return { + removed, + drainingReplicas: state.replicas.length, + }; + }), + ), + inspect: (c: AnyActorContext) => { + const state = c.state as ScalerState; + return { + region: state.region, + release: state.release, + scaling: state.scaling, + revision: state.revision, + readyReplicas: state.replicas, + warmingReplicas: state.warmingReplicas, + }; + }, + }, + }); + + const buildVmOptions: AgentOsOptions = { + defaultSoftware: false, + software: [sh, tar, appsBuilder], + permissions: { + fs: "allow", + childProcess: "allow", + process: "allow", + env: "allow", + network: "allow", + }, + limits: { + tls: { + maxBufferedBytes: 16 * 1024 * 1024, + }, + jsRuntime: { + v8HeapLimitMb: 1_024, + }, + resources: { + maxProcesses: 64, + maxOpenFds: 2_048, + // Leave framing headroom beneath the sidecar's 16 MiB bridge cap. + maxPreadBytes: 15 * 1024 * 1024, + maxFdWriteBytes: 16 * 1024 * 1024, + maxSocketBufferedBytes: 16 * 1024 * 1024, + // Packaging temporarily stores both the installed application tree and + // its uncompressed tar. Keep that workspace bounded, but large enough + // for dependency-heavy packages such as the published RivetKit build. + maxFilesystemBytes: Math.max( + DEFAULT_MAX_BUILD_FILESYSTEM_BYTES, + maxBuildArtifactBytes * 2, + ), + }, + }, + }; + const createBuildVm = async (): Promise => { + const outputDirectory = await mkdtemp( + join(tmpdir(), "agentos-apps-build-output-"), + ); + const artifactGuestPath = "/agentos-app-output/agentos-app.tar"; + const artifactHostPath = join(outputDirectory, "agentos-app.tar"); + let vm: AgentOs; + try { + vm = await AgentOs.create({ + ...buildVmOptions, + mounts: [ + ...(buildVmOptions.mounts ?? []), + { + path: "/agentos-app-output", + readOnly: false, + plugin: createHostDirBackend({ + hostPath: outputDirectory, + readOnly: false, + }), + }, + ], + }); + } catch (error) { + await rm(outputDirectory, { recursive: true, force: true }); + throw error; + } + return { + artifactGuestPath, + writeFiles: (...args) => vm.writeFiles(...args), + execArgv: (...args) => vm.execArgv(...args), + artifactSize: async () => (await stat(artifactHostPath)).size, + readArtifact: async () => + new Uint8Array(await readFile(artifactHostPath)), + dispose: async () => { + const results = await Promise.allSettled([ + vm.dispose(), + rm(outputDirectory, { recursive: true, force: true }), + ]); + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length > 0) { + throw new AggregateError( + failures, + "failed to dispose Dynamic Apps build VM output", + ); + } + }, + }; + }; + + const temporaryArtifacts = new Map< + string, + { directory: string; path: string } + >(); + const guestEngineRegistrations = new Map< + string, + GuestEngineProxyRegistration + >(); + type GuestRpcHead = { + status: number; + statusText: string; + headers: Array<[string, string]>; + }; + type PendingGuestRpc = { + resolveHead(response: GuestRpcHead): void; + reject(error: unknown): void; + timeout: ReturnType; + controller?: ReadableStreamDefaultController; + chunk?: Uint8Array; + ended: boolean; + headResolved: boolean; + }; + type GuestBridge = { + vm: AgentOs; + pid: number; + stdoutBuffer: string; + pending: Map; + }; + const guestBridges = new Map(); + + const rejectGuestBridge = (actorId: string, error: unknown) => { + const bridge = guestBridges.get(actorId); + if (!bridge) return; + guestBridges.delete(actorId); + for (const pending of bridge.pending.values()) { + clearTimeout(pending.timeout); + if (pending.headResolved) pending.controller?.error(error); + else pending.reject(error); + } + bridge.pending.clear(); + }; + + const sendGuestRpcControl = ( + c: AnyActorContext, + bridge: GuestBridge, + id: string, + event: "ack" | "cancel", + ) => { + void bridge.vm.process + .writeStdin(bridge.pid, `${JSON.stringify({ id, event })}\n`) + .catch((error) => { + const pending = bridge.pending.get(id); + bridge.pending.delete(id); + if (pending) { + clearTimeout(pending.timeout); + if (pending.headResolved) pending.controller?.error(error); + else pending.reject(error); + } + c.log.error({ + msg: "failed to send Dynamic Apps guest RPC control", + event, + error, + }); + }); + }; + + const pumpGuestRpc = ( + c: AnyActorContext, + bridge: GuestBridge, + id: string, + pending: PendingGuestRpc, + ) => { + if ( + pending.controller && + pending.chunk && + (pending.controller.desiredSize ?? 1) > 0 + ) { + const chunk = pending.chunk; + pending.chunk = undefined; + pending.controller.enqueue(chunk); + sendGuestRpcControl(c, bridge, id, "ack"); + } + if (pending.controller && pending.ended && !pending.chunk) { + bridge.pending.delete(id); + pending.controller.close(); + } + }; + + const handleGuestStdout = (c: any, bridge: GuestBridge, data: Uint8Array) => { + bridge.stdoutBuffer += new TextDecoder().decode(data); + const maxBufferedCharacters = + Math.ceil((maxResponseBytes * 4) / 3) + 65_536; + if (bridge.stdoutBuffer.length > maxBufferedCharacters) { + const error = new AgentOSAppsError( + "agentos_apps_guest_rpc_output_limit", + `guest RPC output exceeded ${maxBufferedCharacters} buffered characters`, + { limit: maxBufferedCharacters }, + ); + rejectGuestBridge(c.actorId, error); + c.log.error({ msg: error.message, error }); + return; + } + for (;;) { + const newline = bridge.stdoutBuffer.indexOf("\n"); + if (newline < 0) break; + const line = bridge.stdoutBuffer.slice(0, newline); + bridge.stdoutBuffer = bridge.stdoutBuffer.slice(newline + 1); + if (line.startsWith(GUEST_RPC_PREFIX)) { + let response: { + id?: unknown; + event?: unknown; + status?: unknown; + statusText?: unknown; + headers?: unknown; + bodyBase64?: unknown; + message?: unknown; + }; + try { + response = JSON.parse(line.slice(GUEST_RPC_PREFIX.length)); + } catch (error) { + c.log.error({ + msg: "Dynamic Apps guest emitted an invalid RPC response", + error, + }); + continue; + } + if (typeof response.id !== "string") { + c.log.error({ + msg: "Dynamic Apps guest RPC response omitted its request id", + }); + continue; + } + const pending = bridge.pending.get(response.id); + if (!pending) { + c.log.warn({ + msg: "Dynamic Apps guest emitted an RPC response for an unknown request", + requestId: response.id, + }); + continue; + } + if ( + response.event === "head" && + typeof response.status === "number" && + typeof response.statusText === "string" && + Array.isArray(response.headers) + ) { + clearTimeout(pending.timeout); + pending.headResolved = true; + pending.resolveHead({ + status: response.status, + statusText: response.statusText, + headers: response.headers as Array<[string, string]>, + }); + continue; + } + if ( + response.event === "chunk" && + typeof response.bodyBase64 === "string" + ) { + const chunk = Buffer.from(response.bodyBase64, "base64"); + if (chunk.byteLength > maxResponseBytes || pending.chunk) { + const error = new AgentOSAppsError( + "agentos_apps_guest_rpc_output_limit", + "guest RPC exceeded its bounded streaming buffer", + { limit: maxResponseBytes }, + ); + bridge.pending.delete(response.id); + sendGuestRpcControl(c, bridge, response.id, "cancel"); + pending.controller?.error(error); + continue; + } + pending.chunk = chunk; + pumpGuestRpc(c, bridge, response.id, pending); + continue; + } + if (response.event === "end") { + pending.ended = true; + pumpGuestRpc(c, bridge, response.id, pending); + continue; + } + if (response.event === "error") { + const error = new AgentOSAppsError( + "agentos_apps_guest_rpc_failed", + typeof response.message === "string" + ? response.message + : "guest RPC failed", + ); + bridge.pending.delete(response.id); + clearTimeout(pending.timeout); + if (pending.headResolved) pending.controller?.error(error); + else pending.reject(error); + continue; + } + c.log.error({ + msg: "Dynamic Apps guest emitted an invalid RPC event", + requestId: response.id, + event: response.event, + }); + continue; + } + if (line) { + const output = boundedOutput(line, DEFAULT_MAX_BUILD_OUTPUT_BYTES); + c.log.info({ + msg: "Dynamic Apps guest stdout", + pid: bridge.pid, + output, + }); + c.broadcast("processOutput", { + pid: bridge.pid, + stream: "stdout", + data: new TextEncoder().encode(`${line}\n`), + }); + } + } + }; + + const cleanupTemporaryArtifact = async ( + c: AnyActorContext, + ): Promise => { + const temporary = temporaryArtifacts.get(c.actorId); + if (!temporary) return; + await rm(temporary.directory, { recursive: true, force: true }); + temporaryArtifacts.delete(c.actorId); + }; + + const agentOSAppsReplica = agentOS({ + onVmStart: async (c: any, vm: AgentOs) => { + const state = c.state as ReplicaState; + const configuration = state.configuration; + if (!configuration) { + fail( + "agentos_apps_replica_unconfigured", + "execution replica must be configured before its VM starts", + ); + } + const runtime = configuration.runtime; + if (configuration.usesRivetKit) { + const connection = resolveDefaultRivetConnection(); + if ( + connection.endpoint.replace(/\/$/, "") !== + runtime.endpoint.replace(/\/$/, "") + ) { + fail( + "agentos_apps_runtime_changed", + "the host Rivet endpoint changed for an immutable execution replica", + ); + } + } + const pendingStdout: Uint8Array[] = []; + const pendingStderr: Uint8Array[] = []; + let processPid: number | undefined; + let bridge: GuestBridge | undefined; + const engineRegistration = guestEngineRegistrations.get(c.actorId); + if (configuration.usesRivetKit && !engineRegistration) { + fail( + "agentos_apps_guest_engine_proxy_missing", + "RivetKit execution replica is missing its scoped Engine capability", + ); + } + const handleStderr = (data: Uint8Array) => { + if (processPid === undefined) { + pendingStderr.push(data); + return; + } + const output = boundedOutput( + new TextDecoder().decode(data), + DEFAULT_MAX_BUILD_OUTPUT_BYTES, + ); + c.log.error({ + msg: "Dynamic Apps guest stderr", + pid: processPid, + output, + }); + c.broadcast("processOutput", { + pid: processPid, + stream: "stderr", + data, + }); + }; + const process = await vm.process.spawn("node", ["/app/main.mjs"], { + cwd: "/app", + // Never forward the host's RIVET_TOKEN. RivetKit releases receive + // only non-secret placement metadata. + env: replicaGuestEnvironment( + configuration, + engineRegistration?.endpoint, + ), + onStdout: (data) => { + if (bridge) handleGuestStdout(c, bridge, data); + else pendingStdout.push(data); + }, + onStderr: handleStderr, + }); + processPid = process.pid; + bridge = { + vm, + pid: process.pid, + stdoutBuffer: "", + pending: new Map(), + }; + guestBridges.set(c.actorId, bridge); + for (const data of pendingStdout) handleGuestStdout(c, bridge, data); + for (const data of pendingStderr) handleStderr(data); + state.guestPid = process.pid; + void c + .keepAwake( + vm.process.wait(process.pid).then((exit) => { + const exitCode = exit.exitCode ?? 1; + c.broadcast("processExit", { pid: process.pid, exitCode }); + if (exitCode === 0) { + c.log.info({ + msg: "Dynamic Apps guest process exited", + pid: process.pid, + exitCode, + }); + } else { + c.log.error({ + msg: "Dynamic Apps guest process exited", + pid: process.pid, + exitCode, + }); + } + return exitCode; + }), + ) + .catch((error: unknown) => + c.log.error({ + msg: "Dynamic Apps guest process wait failed", + pid: process.pid, + error, + }), + ); + }, + onVmStop: async ( + c: any, + vm: AgentOs, + reason: "sleep" | "destroy" | "error", + ) => { + const state = c.state as ReplicaState; + const guestPid = state.guestPid; + rejectGuestBridge( + c.actorId, + new AgentOSAppsError( + "agentos_apps_guest_stopped", + `Dynamic Apps guest stopped while requests were pending (${reason})`, + ), + ); + if (typeof guestPid === "number") { + await vm.process.signal(guestPid, "SIGTERM"); + let timeout: ReturnType | undefined; + const exited = await Promise.race([ + vm.process.wait(guestPid).then(() => true), + new Promise((resolve) => { + timeout = setTimeout( + () => resolve(false), + GUEST_SHUTDOWN_TIMEOUT_MS, + ); + }), + ]); + if (timeout) clearTimeout(timeout); + if (!exited) { + c.log.warn({ + msg: "Dynamic Apps guest shutdown timed out; disposing VM", + guestPid, + timeoutMs: GUEST_SHUTDOWN_TIMEOUT_MS, + reason, + }); + } + state.guestPid = null; + } + }, + onVmDisposed: async (c: any) => { + rejectGuestBridge( + c.actorId, + new AgentOSAppsError( + "agentos_apps_guest_disposed", + "Dynamic Apps guest VM was disposed", + ), + ); + await cleanupTemporaryArtifact(c); + unregisterGuestEngineProxy(c.actorId); + guestEngineRegistrations.delete(c.actorId); + }, + options: { + noSleep: true, + }, + inspector: { + tabs: [ + { + id: "agentos-replica", + label: "Execution replica", + icon: "cpu", + source: `${INSPECTOR_ROOT}/replica`, + }, + ], + }, + mounts: undefined, + state: { + configuration: null, + startedAt: null, + guestPid: null, + } as ReplicaState, + permissions: { + fs: "allow", + childProcess: "allow", + process: "allow", + env: "allow", + network: "allow", + }, + limits: { + http: { + maxFetchResponseBytes: maxResponseBytes, + }, + }, + resolveOptions: async (c: any) => { + const state = c.state as ReplicaState; + if (!state.configuration) { + fail( + "agentos_apps_replica_unconfigured", + "execution replica must be configured with an artifact before its VM boots", + ); + } + await cleanupTemporaryArtifact(c); + unregisterGuestEngineProxy(c.actorId); + guestEngineRegistrations.delete(c.actorId); + const directory = await mkdtemp(join(tmpdir(), "agentos-apps-replica-")); + const path = join(directory, `${state.configuration.release}.aospkg`); + try { + const appHandle = c + .client() + [APP_ACTOR_NAME].getOrCreate([state.configuration.appId]); + const manifest = (await appHandle.getArtifactManifest( + state.configuration.release, + )) as { + hash: string; + bytes: number; + chunks: number; + chunkBytes: number; + }; + if ( + manifest.hash !== state.configuration.artifactHash || + manifest.bytes !== state.configuration.artifactBytes || + manifest.chunks > MAX_ARTIFACT_CHUNKS + ) { + fail( + "agentos_apps_artifact_manifest_mismatch", + "replica artifact manifest does not match its immutable configuration", + ); + } + const digest = createHash("sha256"); + const artifactFile = await open(path, "wx", 0o600); + let bytes = 0; + try { + for (let index = 0; index < manifest.chunks; index += 1) { + const chunk = new Uint8Array( + await appHandle.readArtifactChunk( + state.configuration.release, + index, + ), + ); + bytes += chunk.byteLength; + if ( + chunk.byteLength > ARTIFACT_CHUNK_BYTES || + bytes > manifest.bytes + ) { + fail( + "agentos_apps_artifact_chunk_invalid", + "replica received an invalid artifact chunk length", + ); + } + digest.update(chunk); + await artifactFile.writeFile(chunk); + } + } finally { + await artifactFile.close(); + } + const hash = digest.digest("hex"); + if (bytes !== manifest.bytes || hash !== manifest.hash) { + fail( + "agentos_apps_artifact_hash_mismatch", + "rehydrated replica artifact failed size or hash verification", + { + expectedBytes: manifest.bytes, + actualBytes: bytes, + expectedHash: manifest.hash, + actualHash: hash, + }, + ); + } + temporaryArtifacts.set(c.actorId, { directory, path }); + } catch (error) { + await rm(directory, { recursive: true, force: true }).catch( + (removeError) => { + c.log.error({ + msg: "failed to remove unsuccessful replica artifact", + removeError, + }); + }, + ); + throw error; + } + let engineRegistration: GuestEngineProxyRegistration | undefined; + if (state.configuration.usesRivetKit) { + const connection = resolveDefaultRivetConnection(); + if ( + connection.endpoint.replace(/\/$/, "") !== + state.configuration.runtime.endpoint.replace(/\/$/, "") + ) { + fail( + "agentos_apps_runtime_changed", + "the host Rivet endpoint changed for an immutable execution replica", + ); + } + engineRegistration = await registerGuestEngineProxy({ + owner: c.actorId, + upstreamEndpoint: state.configuration.runtime.endpoint, + upstreamToken: connection.token, + namespace: state.configuration.runtime.namespace, + pool: state.configuration.runtime.pool, + maxRequestBytes, + maxResponseBytes, + }); + guestEngineRegistrations.set(c.actorId, engineRegistration); + } + return { + loopbackExemptPorts: replicaLoopbackExemptPorts( + state.configuration, + engineRegistration?.port, + ), + mounts: [ + { + path: "/app", + plugin: { + id: "agentos_packages", + config: { + kind: "tar", + tarPath: path, + root: "/", + readOnly: true, + }, + }, + readOnly: true, + }, + ], + }; + }, + onRequest: async (c: any, request: Request): Promise => { + const pathname = new URL(request.url).pathname; + if ( + pathname !== "/api/rivet/metadata" && + pathname !== "/api/rivet/start" + ) { + return new Response("Not Found", { status: 404 }); + } + const bridge = guestBridges.get(c.actorId); + if (!bridge) { + return new Response("Dynamic Apps guest is not ready", { status: 503 }); + } + if (bridge.pending.size >= MAX_PENDING_GUEST_RPCS) { + return new Response("Dynamic Apps guest request limit exceeded", { + status: 503, + }); + } + const body = await readBoundedRequestBody(request, maxRequestBytes); + if (body === null) { + return new Response("Request body exceeds Dynamic Apps limit", { + status: 413, + }); + } + const headers: Record = {}; + request.headers.forEach((value, name) => { + headers[name] = value; + }); + const configuration = (c.state as ReplicaState).configuration; + const engineRegistration = guestEngineRegistrations.get(c.actorId); + if (configuration?.usesRivetKit) { + if (!engineRegistration) { + return new Response("Dynamic Apps Engine capability is not ready", { + status: 503, + }); + } + headers["x-rivet-endpoint"] = engineRegistration.endpoint; + headers["x-rivet-namespace-name"] = configuration.runtime.namespace; + headers["x-rivet-pool-name"] = configuration.runtime.pool; + delete headers["x-rivet-token"]; + delete headers.authorization; + } + const id = randomUUID(); + let pending!: PendingGuestRpc; + const headPromise = new Promise((resolve, reject) => { + pending = { + resolveHead: resolve, + reject, + timeout: setTimeout(() => { + bridge.pending.delete(id); + sendGuestRpcControl(c, bridge, id, "cancel"); + reject( + new AgentOSAppsError( + "agentos_apps_guest_rpc_timeout", + `guest RivetKit response headers exceeded ${GUEST_RPC_TIMEOUT_MS} ms`, + { timeoutMs: GUEST_RPC_TIMEOUT_MS }, + ), + ); + }, GUEST_RPC_TIMEOUT_MS), + ended: false, + headResolved: false, + }; + bridge.pending.set(id, pending); + }); + try { + await bridge.vm.process.writeStdin( + bridge.pid, + `${JSON.stringify({ + id, + method: request.method, + url: request.url, + headers, + bodyBase64: + body && body.byteLength > 0 + ? Buffer.from(body).toString("base64") + : undefined, + })}\n`, + ); + } catch (error) { + bridge.pending.delete(id); + clearTimeout(pending.timeout); + pending.reject(error); + } + const head = await headPromise; + const stream = new ReadableStream( + { + start(controller) { + pending.controller = controller; + pumpGuestRpc(c, bridge, id, pending); + }, + pull() { + pumpGuestRpc(c, bridge, id, pending); + }, + cancel() { + bridge.pending.delete(id); + sendGuestRpcControl(c, bridge, id, "cancel"); + }, + }, + { highWaterMark: 1 }, + ); + return new Response(stream, { + status: head.status, + statusText: head.statusText, + headers: head.headers, + }); + }, + actions: { + destroy: (c: any) => c.destroy(), + configure: (c: any, input: ReplicaState["configuration"]) => { + if (!input) { + fail( + "agentos_apps_replica_invalid_config", + "execution replica configuration is required", + ); + } + const state = c.state as ReplicaState; + if ( + state.configuration && + (state.configuration.release !== input.release || + state.configuration.artifactHash !== input.artifactHash || + Boolean(state.configuration.usesRivetKit) !== + Boolean(input.usesRivetKit)) + ) { + fail( + "agentos_apps_replica_config_collision", + "execution replica cannot be reassigned to another immutable release", + ); + } + state.configuration = { + ...input, + usesRivetKit: Boolean(input.usesRivetKit), + }; + }, + markStarted: (c: any) => { + (c.state as ReplicaState).startedAt = Date.now(); + }, + inspect: (c: any) => { + const state = c.state as ReplicaState; + return { + release: state.configuration?.release ?? null, + artifactHash: state.configuration?.artifactHash ?? null, + namespace: state.configuration?.namespace ?? null, + pool: state.configuration?.runtime.pool ?? null, + startedAt: state.startedAt, + }; + }, + }, + }); + + return { + agentOSAppsApp, + agentOSAppsScaler, + agentOSAppsReplica, + }; +} diff --git a/packages/dynamic-apps/src/advanced.ts b/packages/dynamic-apps/src/advanced.ts new file mode 100644 index 000000000..9ff58cd1a --- /dev/null +++ b/packages/dynamic-apps/src/advanced.ts @@ -0,0 +1,4 @@ +export { + type CreateAppsRouterOptions, + createAppsRouter, +} from "./router.js"; diff --git a/packages/dynamic-apps/src/control-plane.ts b/packages/dynamic-apps/src/control-plane.ts new file mode 100644 index 000000000..8e5e3ae27 --- /dev/null +++ b/packages/dynamic-apps/src/control-plane.ts @@ -0,0 +1,177 @@ +import { createHash } from "node:crypto"; +import { controlFetch } from "./control-request.js"; +import { AgentOSAppsError } from "./errors.js"; +import { appRunnerPool, ensureServerlessRunnerConfig } from "./runtime.js"; + +const DEFAULT_ENDPOINT = "http://localhost:6420"; +const MAX_CONTROL_RESPONSE_BYTES = 1024 * 1024; + +export interface ResolvedRivetConnection { + endpoint: string; + namespace: string; + token?: string; +} + +async function readBoundedJson(response: Response): Promise { + if (!response.body) return null; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let bytes = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + bytes += value.byteLength; + if (bytes > MAX_CONTROL_RESPONSE_BYTES) { + await reader.cancel("Dynamic Apps control response limit exceeded"); + throw new AgentOSAppsError( + "agentos_apps_control_response_limit", + `Rivet control response exceeded ${MAX_CONTROL_RESPONSE_BYTES} bytes`, + { limit: MAX_CONTROL_RESPONSE_BYTES }, + ); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const text = new TextDecoder().decode(Buffer.concat(chunks)); + return text ? JSON.parse(text) : null; +} + +/** + * Resolves the same standard Rivet connection variables as createClient(), but + * only when a deployment or replica actually needs control-plane information. + * This is intentionally not called by setupApps() or at module import time. + */ +export function resolveDefaultRivetConnection(): ResolvedRivetConnection { + const rawEndpoint = + process.env.RIVET_ENGINE ?? process.env.RIVET_ENDPOINT ?? DEFAULT_ENDPOINT; + const url = new URL(rawEndpoint); + const endpointNamespace = url.username + ? decodeURIComponent(url.username) + : undefined; + const endpointToken = url.password + ? decodeURIComponent(url.password) + : undefined; + url.username = ""; + url.password = ""; + return { + endpoint: url.toString().replace(/\/$/, ""), + namespace: endpointNamespace ?? process.env.RIVET_NAMESPACE ?? "default", + token: endpointToken ?? process.env.RIVET_TOKEN, + }; +} + +function namespaceName(appId: string, hostNamespace: string): string { + const suffix = createHash("sha256") + .update(hostNamespace) + .update("\0") + .update(appId) + .digest("hex") + .slice(0, 10); + return `agentos-app-${appId}`.slice(0, 63 - suffix.length - 1) + `-${suffix}`; +} + +function controlHeaders(token?: string): Record { + return { + accept: "application/json", + "content-type": "application/json", + ...(token ? { authorization: `Bearer ${token}` } : {}), + }; +} + +function serverlessAppUrl( + appActorId: string, + connection: ResolvedRivetConnection, +): string { + const url = new URL( + `/gateway/${encodeURIComponent(appActorId)}/request/.agentos/apps/rivet`, + connection.endpoint, + ); + return url.toString(); +} + +/** Idempotently provisions the isolated application namespace. */ +export async function provisionAppNamespace( + appId: string, + connection = resolveDefaultRivetConnection(), +): Promise<{ namespace: string; endpoint: string; pool: string }> { + const namespace = namespaceName(appId, connection.namespace); + const headers = controlHeaders(connection.token); + const lookupUrl = new URL("/namespaces", connection.endpoint); + lookupUrl.searchParams.set("name", namespace); + lookupUrl.searchParams.set("limit", "1"); + const lookup = async (): Promise => { + const response = await controlFetch(lookupUrl, { + headers, + }); + if (!response.ok) { + throw new AgentOSAppsError( + "agentos_apps_namespace_lookup_failed", + `Rivet namespace lookup failed with HTTP ${response.status}`, + { status: response.status }, + ); + } + const body = (await readBoundedJson(response)) as { + namespaces?: Array<{ name?: unknown }>; + }; + return body.namespaces?.some((entry) => entry.name === namespace) ?? false; + }; + + if (!(await lookup())) { + const response = await controlFetch( + new URL("/namespaces", connection.endpoint), + { + method: "POST", + headers, + body: JSON.stringify({ + name: namespace, + display_name: `Dynamic App ${appId}`, + }), + }, + ); + if (!response.ok && !(await lookup())) { + throw new AgentOSAppsError( + "agentos_apps_namespace_create_failed", + `Rivet namespace creation failed with HTTP ${response.status}`, + { status: response.status }, + ); + } + } + + return { + namespace, + endpoint: connection.endpoint, + pool: appRunnerPool(appId), + }; +} + +/** Configures the guest pool only after a healthy release has been activated. */ +export async function configureAppNamespaceRunner( + appActorId: string, + runtime: { endpoint: string; namespace: string; pool: string }, + callbackSecret: string, + connection = resolveDefaultRivetConnection(), +): Promise { + try { + await ensureServerlessRunnerConfig({ + endpoint: runtime.endpoint, + namespace: runtime.namespace, + url: serverlessAppUrl(appActorId, connection), + pool: runtime.pool, + token: connection.token, + callbackSecret, + }); + } catch (error) { + throw new AgentOSAppsError( + "agentos_apps_runner_config_failed", + `Rivet runner configuration failed for namespace ${runtime.namespace} and pool ${runtime.pool}`, + { + namespace: runtime.namespace, + pool: runtime.pool, + error: error instanceof Error ? error.message : String(error), + }, + ); + } +} diff --git a/packages/dynamic-apps/src/control-request.ts b/packages/dynamic-apps/src/control-request.ts new file mode 100644 index 000000000..03cb2132b --- /dev/null +++ b/packages/dynamic-apps/src/control-request.ts @@ -0,0 +1,65 @@ +const CONTROL_TIMEOUT_MS = 15_000; +const MAX_CONTROL_ATTEMPTS = 4; +const BASE_RETRY_DELAY_MS = 50; +const MAX_RETRY_DELAY_MS = 1_000; + +function retryableStatus(status: number): boolean { + return status === 409 || status === 429 || status >= 500; +} + +function retryDelay(response: Response | undefined, attempt: number): number { + const retryAfter = response?.headers.get("retry-after"); + if (retryAfter) { + const seconds = Number(retryAfter); + if (Number.isFinite(seconds) && seconds >= 0) { + return Math.min(seconds * 1_000, MAX_RETRY_DELAY_MS); + } + const at = Date.parse(retryAfter); + if (Number.isFinite(at)) { + return Math.min(Math.max(0, at - Date.now()), MAX_RETRY_DELAY_MS); + } + } + return Math.min( + BASE_RETRY_DELAY_MS * 2 ** Math.max(0, attempt - 1), + MAX_RETRY_DELAY_MS, + ); +} + +/** Bounded retry policy for idempotent Rivet control-plane requests. */ +export async function controlFetch( + input: string | URL, + init: RequestInit = {}, +): Promise { + const deadline = Date.now() + CONTROL_TIMEOUT_MS; + let lastError: unknown; + + for (let attempt = 1; attempt <= MAX_CONTROL_ATTEMPTS; attempt += 1) { + let response: Response | undefined; + try { + response = await fetch(input, { + ...init, + signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())), + }); + if ( + !retryableStatus(response.status) || + attempt === MAX_CONTROL_ATTEMPTS + ) { + return response; + } + } catch (error) { + lastError = error; + if (attempt === MAX_CONTROL_ATTEMPTS || Date.now() >= deadline) + throw error; + } + + if (response?.body) await response.body.cancel(); + const delay = retryDelay(response, attempt); + if (Date.now() + delay >= deadline) { + if (response) return response; + throw lastError; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + throw lastError; +} diff --git a/packages/dynamic-apps/src/deploy.ts b/packages/dynamic-apps/src/deploy.ts new file mode 100644 index 000000000..83a8505c2 --- /dev/null +++ b/packages/dynamic-apps/src/deploy.ts @@ -0,0 +1,115 @@ +import { createClient } from "rivetkit/client"; +import { + provisionAppNamespace, + resolveDefaultRivetConnection, +} from "./control-plane.js"; +import { AgentOSAppsError } from "./errors.js"; +import { appRunnerPool } from "./runtime.js"; +import { prepareSource } from "./source.js"; +import type { + DeployAppInput, + Deployment, + PreparedDeployAppInput, +} from "./types.js"; + +interface DeploymentHandle { + deploy( + input: PreparedDeployAppInput, + ): Promise; +} + +export interface DeployAppOptions { + /** An ordinary RivetKit client. The default client is created lazily. */ + client?: { + agentOSAppsApp: { + getOrCreate(key: string | string[]): DeploymentHandle; + }; + }; +} + +let defaultClient: NonNullable | undefined; +const HOST_REGISTRY_READY_TIMEOUT_MS = 15_000; +const HOST_REGISTRY_RETRY_DELAY_MS = 50; + +function getDefaultClient(): NonNullable { + defaultClient ??= createClient() as unknown as NonNullable< + DeployAppOptions["client"] + >; + return defaultClient; +} + +export async function deployApp( + input: DeployAppInput, + options: DeployAppOptions = {}, +): Promise { + const files = await prepareSource(input); + const connection = resolveDefaultRivetConnection(); + const runtime = input.createNamespace + ? await provisionAppNamespace(input.appId, connection) + : { + endpoint: connection.endpoint, + namespace: connection.namespace, + pool: appRunnerPool(input.appId), + }; + const client = options.client ?? getDefaultClient(); + const app = client.agentOSAppsApp.getOrCreate([input.appId]); + const result = await deployWhenHostRegistryIsReady(app, { + appId: input.appId, + files, + regions: input.regions, + scaling: input.scaling, + namespace: runtime.namespace, + runtime: { + endpoint: runtime.endpoint, + pool: runtime.pool, + }, + }); + return { + appId: input.appId, + release: result.release, + namespace: result.namespace, + pool: runtime.pool, + regions: result.regions, + }; +} + +async function deployWhenHostRegistryIsReady( + app: DeploymentHandle, + input: PreparedDeployAppInput, +): Promise { + const deadline = Date.now() + HOST_REGISTRY_READY_TIMEOUT_MS; + let lastError: unknown; + + do { + try { + return await app.deploy(input); + } catch (error) { + if (getErrorCode(error) !== "no_runner_config_configured") throw error; + lastError = error; + await new Promise((resolve) => + setTimeout(resolve, HOST_REGISTRY_RETRY_DELAY_MS), + ); + } + } while (Date.now() < deadline); + + throw new AgentOSAppsError( + "host_registry_not_ready", + `Dynamic Apps could not reach the host actor runner within ${HOST_REGISTRY_READY_TIMEOUT_MS}ms. Call registry.start() before deployApp().`, + { + timeoutMs: HOST_REGISTRY_READY_TIMEOUT_MS, + lastCode: getErrorCode(lastError), + }, + ); +} + +function getErrorCode(error: unknown): string | undefined { + if (typeof error !== "object" || error === null || !("code" in error)) { + return undefined; + } + return typeof error.code === "string" ? error.code : undefined; +} + +/** @internal Test-only reset for verifying lazy client creation. */ +export function resetDefaultAppsClientForTest(): void { + defaultClient = undefined; +} diff --git a/packages/dynamic-apps/src/engine-proxy.ts b/packages/dynamic-apps/src/engine-proxy.ts new file mode 100644 index 000000000..36e086d51 --- /dev/null +++ b/packages/dynamic-apps/src/engine-proxy.ts @@ -0,0 +1,625 @@ +import { randomUUID } from "node:crypto"; +import { + createServer, + type IncomingMessage, + type Server, + type ServerResponse, +} from "node:http"; +import { type AddressInfo, connect as connectTcp } from "node:net"; +import type { Duplex } from "node:stream"; +import { connect as connectTls } from "node:tls"; + +const MAX_CAPABILITIES = 16_384; +const MAX_REQUESTS_PER_CAPABILITY = 256; +const MAX_ACTOR_IDS_PER_CAPABILITY = 4_096; +const MAX_WEBSOCKET_HANDSHAKE_BYTES = 64 * 1024; +const CAPABILITY_WARNING_THRESHOLD = MAX_REQUESTS_PER_CAPABILITY / 2; + +interface Capability { + owner: string; + path: string; + upstreamEndpoint: string; + upstreamToken?: string; + namespace: string; + pool: string; + maxRequestBytes: number; + maxResponseBytes: number; + activeRequests: number; + warned: boolean; + actorIds: Set; + sockets: Set; +} + +export interface GuestEngineProxyRegistration { + endpoint: string; + port: number; +} + +const capabilities = new Map(); +const ownerPaths = new Map(); +let serverPromise: Promise<{ server: Server; port: number }> | undefined; + +function requestPathAllowed(method: string, pathname: string): boolean { + if (method === "GET" && pathname === "/metadata") return true; + if (method === "GET" && pathname === "/envoys/connect") return true; + if (pathname === "/actors" && (method === "POST" || method === "PUT")) { + return true; + } + if (method === "GET" && /^\/actors\/[^/]+\/kv\/keys\/[^/]+$/.test(pathname)) { + return true; + } + if (method === "DELETE" && /^\/actors\/[^/]+$/.test(pathname)) { + return true; + } + return pathname.startsWith("/gateway/"); +} + +function stripHopByHopHeaders(headers: Headers): void { + for (const name of [ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + ]) { + headers.delete(name); + } +} + +async function readRequest( + request: IncomingMessage, + limit: number, +): Promise { + const chunks: Buffer[] = []; + let bytes = 0; + for await (const chunk of request) { + const buffer = Buffer.from(chunk); + bytes += buffer.byteLength; + if (bytes > limit) { + throw new RangeError( + `guest Engine proxy request exceeds maxRequestBytes ${limit}`, + ); + } + chunks.push(buffer); + } + return bytes === 0 ? undefined : Buffer.concat(chunks, bytes); +} + +async function readResponse( + response: Response, + limit: number, +): Promise { + if (!response.body) return Buffer.alloc(0); + const chunks: Buffer[] = []; + let bytes = 0; + const reader = response.body.getReader(); + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + const buffer = Buffer.from(value); + bytes += buffer.byteLength; + if (bytes > limit) { + await reader.cancel("guest Engine proxy response limit exceeded"); + throw new RangeError( + `guest Engine proxy response exceeds maxResponseBytes ${limit}`, + ); + } + chunks.push(buffer); + } + } finally { + reader.releaseLock(); + } + return Buffer.concat(chunks, bytes); +} + +function actorIdFromPath(pathname: string): string | undefined { + const match = pathname.match(/^\/(?:gateway|actors)\/([^/]+)/); + if (!match?.[1]) return undefined; + const actorId = decodeURIComponent(match[1]); + return actorId.includes("@") ? undefined : actorId; +} + +async function actorBelongsToCapability( + capability: Capability, + actorId: string, +): Promise { + if (capability.actorIds.has(actorId)) return true; + const url = new URL("/actors", capability.upstreamEndpoint); + url.searchParams.set("namespace", capability.namespace); + url.searchParams.set("actor_ids", actorId); + const response = await fetch(url, { + headers: capability.upstreamToken + ? { authorization: `Bearer ${capability.upstreamToken}` } + : undefined, + }); + if (!response.ok) return false; + const body = (await response.json()) as { + actors?: Array<{ + actor_id?: unknown; + runner_name_selector?: unknown; + }>; + }; + const belongs = + body.actors?.some( + (actor) => + actor.actor_id === actorId && + actor.runner_name_selector === capability.pool, + ) ?? false; + if (belongs) { + if (capability.actorIds.size >= MAX_ACTOR_IDS_PER_CAPABILITY) { + throw new RangeError( + `guest Engine proxy actor cache reached ${MAX_ACTOR_IDS_PER_CAPABILITY}; deploy fewer actors or raise the host limit`, + ); + } + capability.actorIds.add(actorId); + } + return belongs; +} + +async function resolveQueryGatewayActor( + capability: Capability, + pathname: string, + upstreamUrl: URL, +): Promise { + const match = pathname.match(/^\/gateway\/([^/]+)(\/.*)?$/); + if (!match?.[1]) return false; + const url = new URL("/actors", capability.upstreamEndpoint); + url.searchParams.set("namespace", capability.namespace); + url.searchParams.set("name", decodeURIComponent(match[1])); + const rawKey = upstreamUrl.searchParams.get("rvt-key"); + const keyParts = rawKey === null || rawKey === "" ? [] : rawKey.split(","); + url.searchParams.set( + "key", + keyParts.length === 0 + ? "/" + : keyParts + .map((part) => + part === "" + ? "\\0" + : part.replaceAll("\\", "\\\\").replaceAll("/", "\\/"), + ) + .join("/"), + ); + const response = await fetch(url, { + headers: capability.upstreamToken + ? { authorization: `Bearer ${capability.upstreamToken}` } + : undefined, + }); + if (!response.ok) return false; + const body = (await response.json()) as { + actors?: Array<{ + actor_id?: unknown; + runner_name_selector?: unknown; + }>; + }; + const actor = body.actors?.find( + (candidate) => + typeof candidate.actor_id === "string" && + candidate.runner_name_selector === capability.pool, + ); + if (!actor || typeof actor.actor_id !== "string") return false; + upstreamUrl.pathname = `/gateway/${encodeURIComponent(actor.actor_id)}${match[2] ?? ""}`; + for (const name of [ + ...new Set( + Array.from(upstreamUrl.searchParams.keys()).filter((candidate) => + candidate.startsWith("rvt-"), + ), + ), + ]) { + upstreamUrl.searchParams.delete(name); + } + if (capability.actorIds.size < MAX_ACTOR_IDS_PER_CAPABILITY) { + capability.actorIds.add(actor.actor_id); + } + return true; +} + +async function scopedUpstreamUrl( + capability: Capability, + pathname: string, + incomingUrl: URL, + method: string, +): Promise { + const upstreamUrl = new URL(pathname, capability.upstreamEndpoint); + for (const [name, value] of incomingUrl.searchParams) { + upstreamUrl.searchParams.append(name, value); + } + upstreamUrl.searchParams.delete("rvt-token"); + if (pathname === "/envoys/connect") { + upstreamUrl.searchParams.set("namespace", capability.namespace); + upstreamUrl.searchParams.set("pool_name", capability.pool); + } else if (pathname.startsWith("/gateway/")) { + const queryMethod = upstreamUrl.searchParams.get("rvt-method"); + if (queryMethod === "get") { + if ( + !(await resolveQueryGatewayActor(capability, pathname, upstreamUrl)) + ) { + return undefined; + } + } else if (queryMethod === "getOrCreate") { + upstreamUrl.searchParams.set("rvt-namespace", capability.namespace); + upstreamUrl.searchParams.set("rvt-pool", capability.pool); + upstreamUrl.searchParams.set("rvt-runner", capability.pool); + } else if (queryMethod) { + return undefined; + } else { + const actorId = actorIdFromPath(pathname); + if (!actorId || !(await actorBelongsToCapability(capability, actorId))) { + return undefined; + } + for (const name of [ + ...new Set( + Array.from(upstreamUrl.searchParams.keys()).filter((candidate) => + candidate.startsWith("rvt-"), + ), + ), + ]) { + upstreamUrl.searchParams.delete(name); + } + } + } else { + upstreamUrl.searchParams.set("namespace", capability.namespace); + } + if ( + (method === "DELETE" || pathname.includes("/kv/keys/")) && + actorIdFromPath(pathname) + ) { + const actorId = actorIdFromPath(pathname); + if (actorId && !(await actorBelongsToCapability(capability, actorId))) { + return undefined; + } + } + return upstreamUrl; +} + +async function forward( + request: IncomingMessage, + response: ServerResponse, + capability: Capability, + pathname: string, +): Promise { + const method = request.method ?? "GET"; + if (!requestPathAllowed(method, pathname)) { + response.writeHead(403).end("Engine management route is not available"); + return; + } + + const incomingUrl = new URL(request.url ?? "/", "http://agentos.invalid"); + const upstreamUrl = await scopedUpstreamUrl( + capability, + pathname, + incomingUrl, + method, + ); + if (!upstreamUrl) { + response.writeHead(403).end("Actor does not belong to this app"); + return; + } + + const headers = new Headers(); + for (const [name, value] of Object.entries(request.headers)) { + if (value !== undefined) { + headers.set(name, Array.isArray(value) ? value.join(", ") : value); + } + } + stripHopByHopHeaders(headers); + headers.delete("host"); + headers.delete("authorization"); + headers.delete("x-rivet-token"); + if (capability.upstreamToken && !pathname.startsWith("/gateway/")) { + headers.set("authorization", `Bearer ${capability.upstreamToken}`); + } + + let body = await readRequest(request, capability.maxRequestBytes); + if ( + body && + pathname === "/actors" && + (method === "POST" || method === "PUT") + ) { + const parsed = JSON.parse(new TextDecoder().decode(body)) as Record< + string, + unknown + >; + parsed.runner_name_selector = capability.pool; + body = Buffer.from(JSON.stringify(parsed)); + headers.set("content-type", "application/json"); + headers.set("content-length", String(body.byteLength)); + } + + const upstream = await fetch(upstreamUrl, { + method, + headers, + body: + method === "GET" || method === "HEAD" + ? undefined + : Buffer.from(body ?? []), + redirect: "manual", + }); + let responseBody = await readResponse(upstream, capability.maxResponseBytes); + const responseHeaders = new Headers(upstream.headers); + stripHopByHopHeaders(responseHeaders); + if ( + pathname === "/metadata" && + upstream.ok && + responseHeaders.get("content-type")?.includes("application/json") + ) { + const metadata = JSON.parse(responseBody.toString("utf8")) as Record< + string, + unknown + >; + metadata.clientEndpoint = `http://127.0.0.1:${(await ensureServer()).port}/${capability.path}`; + metadata.clientNamespace = capability.namespace; + delete metadata.clientToken; + responseBody = Buffer.from(JSON.stringify(metadata)); + responseHeaders.set("content-length", String(responseBody.byteLength)); + } + responseHeaders.forEach((value, name) => { + response.setHeader(name, value); + }); + response.statusCode = upstream.status; + response.statusMessage = upstream.statusText; + response.end(responseBody); +} + +async function handleRequest( + request: IncomingMessage, + response: ServerResponse, +): Promise { + const url = new URL(request.url ?? "/", "http://agentos.invalid"); + const parts = url.pathname.split("/").filter(Boolean); + const capabilityPath = parts.shift(); + const capability = capabilityPath + ? capabilities.get(capabilityPath) + : undefined; + if (!capability) { + response.writeHead(401).end("Unknown guest Engine capability"); + return; + } + if (capability.activeRequests >= MAX_REQUESTS_PER_CAPABILITY) { + response + .writeHead(503) + .end( + `guest Engine proxy reached ${MAX_REQUESTS_PER_CAPABILITY} concurrent requests; raise the host limit`, + ); + return; + } + capability.activeRequests += 1; + if ( + !capability.warned && + capability.activeRequests >= CAPABILITY_WARNING_THRESHOLD + ) { + capability.warned = true; + console.warn( + `Dynamic Apps guest Engine proxy passed 50% of its ${MAX_REQUESTS_PER_CAPABILITY}-request limit`, + ); + } + try { + await forward(request, response, capability, `/${parts.join("/")}`); + } catch (error) { + console.error("Dynamic Apps guest Engine proxy failed", error); + if (!response.headersSent) response.writeHead(502); + if (!response.writableEnded) response.end("Guest Engine proxy failed"); + } finally { + capability.activeRequests -= 1; + if (capability.activeRequests < CAPABILITY_WARNING_THRESHOLD) { + capability.warned = false; + } + } +} + +async function handleUpgrade( + request: IncomingMessage, + socket: Duplex, + head: Buffer, +): Promise { + const incomingUrl = new URL(request.url ?? "/", "http://agentos.invalid"); + const parts = incomingUrl.pathname.split("/").filter(Boolean); + const capabilityPath = parts.shift(); + const capability = capabilityPath + ? capabilities.get(capabilityPath) + : undefined; + const pathname = `/${parts.join("/")}`; + if ( + !capability || + !requestPathAllowed("GET", pathname) || + capability.activeRequests >= MAX_REQUESTS_PER_CAPABILITY + ) { + socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n"); + return; + } + capability.activeRequests += 1; + capability.sockets.add(socket); + if ( + !capability.warned && + capability.activeRequests >= CAPABILITY_WARNING_THRESHOLD + ) { + capability.warned = true; + console.warn( + `Dynamic Apps guest Engine proxy passed 50% of its ${MAX_REQUESTS_PER_CAPABILITY}-request limit`, + ); + } + let released = false; + const release = () => { + if (released) return; + released = true; + capability.activeRequests -= 1; + capability.sockets.delete(socket); + if (capability.activeRequests < CAPABILITY_WARNING_THRESHOLD) { + capability.warned = false; + } + }; + try { + const upstreamUrl = await scopedUpstreamUrl( + capability, + pathname, + incomingUrl, + "GET", + ); + if (!upstreamUrl) { + socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n"); + release(); + return; + } + const secure = upstreamUrl.protocol === "https:"; + if (!secure && upstreamUrl.protocol !== "http:") { + throw new TypeError( + `unsupported Engine proxy protocol ${upstreamUrl.protocol}`, + ); + } + const port = Number(upstreamUrl.port || (secure ? 443 : 80)); + const upstream = secure + ? connectTls({ + host: upstreamUrl.hostname, + port, + servername: upstreamUrl.hostname, + }) + : connectTcp({ host: upstreamUrl.hostname, port }); + const close = () => { + release(); + if (!socket.destroyed) socket.destroy(); + if (!upstream.destroyed) upstream.destroy(); + }; + socket.once("close", close); + upstream.once("close", close); + upstream.once("error", (error) => { + console.error("Dynamic Apps guest Engine WebSocket proxy failed", error); + if (!socket.destroyed) { + socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n"); + } + }); + const onConnected = () => { + const headers = new Map(); + for (const [name, value] of Object.entries(request.headers)) { + if (value !== undefined) { + headers.set(name, Array.isArray(value) ? value.join(", ") : value); + } + } + headers.set("host", upstreamUrl.host); + headers.delete("authorization"); + headers.delete("x-rivet-token"); + const protocols = (headers.get("sec-websocket-protocol") ?? "") + .split(",") + .map((protocol) => protocol.trim()) + .filter( + (protocol) => + protocol.length > 0 && !protocol.startsWith("rivet_token."), + ); + if (pathname === "/envoys/connect" && capability.upstreamToken) { + protocols.push(`rivet_token.${capability.upstreamToken}`); + } + if (protocols.length > 0) { + headers.set("sec-websocket-protocol", protocols.join(", ")); + } else { + headers.delete("sec-websocket-protocol"); + } + const requestHead = [ + `GET ${upstreamUrl.pathname}${upstreamUrl.search} HTTP/1.1`, + ...Array.from(headers, ([name, value]) => `${name}: ${value}`), + "", + "", + ].join("\r\n"); + upstream.write(requestHead); + if (head.byteLength > 0) upstream.write(head); + socket.pipe(upstream); + let handshake = Buffer.alloc(0); + const receiveHandshake = (chunk: Buffer) => { + handshake = Buffer.concat([handshake, chunk]); + if (handshake.byteLength > MAX_WEBSOCKET_HANDSHAKE_BYTES) { + close(); + return; + } + const boundary = handshake.indexOf("\r\n\r\n"); + if (boundary < 0) return; + upstream.pause(); + upstream.off("data", receiveHandshake); + const header = handshake + .subarray(0, boundary) + .toString("latin1") + .split("\r\n") + .filter( + (line) => + !( + line.toLowerCase().startsWith("sec-websocket-protocol:") && + line.includes("rivet_token.") + ), + ) + .join("\r\n"); + socket.write(`${header}\r\n\r\n`, "latin1"); + const remainder = handshake.subarray(boundary + 4); + if (remainder.byteLength > 0) socket.write(remainder); + upstream.pipe(socket); + upstream.resume(); + }; + upstream.on("data", receiveHandshake); + }; + if (secure) upstream.once("secureConnect", onConnected); + else upstream.once("connect", onConnected); + } catch (error) { + console.error("Dynamic Apps guest Engine WebSocket proxy failed", error); + socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n"); + release(); + } +} + +async function ensureServer(): Promise<{ server: Server; port: number }> { + serverPromise ??= new Promise((resolve, reject) => { + const server = createServer((request, response) => { + void handleRequest(request, response); + }); + server.on("upgrade", (request, socket, head) => { + void handleUpgrade(request, socket, head); + }); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve({ + server, + port: (server.address() as AddressInfo).port, + }); + }); + }); + return serverPromise; +} + +export async function registerGuestEngineProxy(input: { + owner: string; + upstreamEndpoint: string; + upstreamToken?: string; + namespace: string; + pool: string; + maxRequestBytes: number; + maxResponseBytes: number; +}): Promise { + const existing = ownerPaths.get(input.owner); + if (existing) capabilities.delete(existing); + if (capabilities.size >= MAX_CAPABILITIES) { + throw new RangeError( + `Dynamic Apps guest Engine proxy reached ${MAX_CAPABILITIES} capabilities; raise the host limit`, + ); + } + const { port } = await ensureServer(); + const path = + randomUUID().replaceAll("-", "") + randomUUID().replaceAll("-", ""); + capabilities.set(path, { + ...input, + path, + activeRequests: 0, + warned: false, + actorIds: new Set(), + sockets: new Set(), + }); + ownerPaths.set(input.owner, path); + return { endpoint: `http://127.0.0.1:${port}/${path}`, port }; +} + +export function unregisterGuestEngineProxy(owner: string): void { + const path = ownerPaths.get(owner); + if (!path) return; + ownerPaths.delete(owner); + for (const socket of capabilities.get(path)?.sockets ?? []) socket.destroy(); + capabilities.delete(path); +} diff --git a/packages/dynamic-apps/src/errors.ts b/packages/dynamic-apps/src/errors.ts new file mode 100644 index 000000000..d0bf922e9 --- /dev/null +++ b/packages/dynamic-apps/src/errors.ts @@ -0,0 +1,18 @@ +export class DynamicAppsError extends Error { + readonly code: string; + readonly metadata?: Record; + + constructor( + code: string, + message: string, + metadata?: Record, + ) { + super(message); + this.name = "DynamicAppsError"; + this.code = code; + this.metadata = metadata; + } +} + +/** @deprecated Dynamic Apps moved out of agentOS. Use `DynamicAppsError`. */ +export const AgentOSAppsError = DynamicAppsError; diff --git a/packages/dynamic-apps/src/index.ts b/packages/dynamic-apps/src/index.ts new file mode 100644 index 000000000..d36dbf691 --- /dev/null +++ b/packages/dynamic-apps/src/index.ts @@ -0,0 +1,33 @@ +import { setup as agentOSSetup } from "@rivet-dev/agentos"; +import type { setup as rivetkitSetup } from "rivetkit"; +import { createAppsActors, type DynamicAppsActors } from "./actors.js"; + +export { deployApp } from "./deploy.js"; +export { AgentOSAppsError, DynamicAppsError } from "./errors.js"; +export { + type AgentOSAppsRoutingClient, + appsRouter, + type CreateAppsRouterOptions, + createAppsRouter, + type DynamicAppsRoutingClient, +} from "./router.js"; +export type { + AppReleaseInfo, + AppScaling, + DeployAppInput, + Deployment, +} from "./types.js"; + +/** + * RivetKit setup configured for the VM runtime used by Dynamic Apps. + * + * The public type intentionally comes from RivetKit. The underlying VM runtime + * is a transitive implementation detail and does not need user configuration. + */ +export const setup: typeof rivetkitSetup = agentOSSetup as typeof rivetkitSetup; + +export function setupApps(): { appsActors: DynamicAppsActors } { + return { + appsActors: createAppsActors(), + }; +} diff --git a/packages/dynamic-apps/src/router.ts b/packages/dynamic-apps/src/router.ts new file mode 100644 index 000000000..413775bb8 --- /dev/null +++ b/packages/dynamic-apps/src/router.ts @@ -0,0 +1,112 @@ +import { Hono } from "hono"; +import { createClient } from "rivetkit/client"; +import { DynamicAppsError } from "./errors.js"; +import { validateAppId } from "./source.js"; + +export interface DynamicAppsRoutingClient { + agentOSAppsApp: { + getOrCreate(key?: string | string[]): { + fetch(request: Request): Promise; + }; + }; +} + +export interface CreateAppsRouterOptions { + /** An ordinary RivetKit client. */ + client?: DynamicAppsRoutingClient; +} + +/** @deprecated Use `DynamicAppsRoutingClient`. */ +export type AgentOSAppsRoutingClient = DynamicAppsRoutingClient; + +let defaultClient: DynamicAppsRoutingClient | undefined; + +function getDefaultClient(): DynamicAppsRoutingClient { + defaultClient ??= createClient() as unknown as DynamicAppsRoutingClient; + return defaultClient; +} + +function errorResponse(error: unknown): Response { + const code = + error instanceof DynamicAppsError + ? error.code + : typeof error === "object" && + error !== null && + "code" in error && + typeof error.code === "string" + ? error.code + : undefined; + const status = + code === "agentos_apps_invalid_app_id" + ? 400 + : code === "agentos_apps_not_deployed" + ? 404 + : code === "agentos_apps_region_not_deployed" + ? 404 + : code === "agentos_apps_request_limit" + ? 413 + : code?.startsWith("agentos_apps_") + ? 503 + : 500; + return Response.json( + { + error: { + code: code ?? "agentos_apps_internal_error", + message: + error instanceof Error + ? error.message + : "Dynamic Apps request failed", + }, + }, + { status }, + ); +} + +export function createAppsRouter(options: CreateAppsRouterOptions = {}): Hono { + const router = new Hono(); + const handler = async (context: { + req: { + param(name: string): string | undefined; + path: string; + routePath: string; + raw: Request; + }; + }) => { + try { + const appId = context.req.param("appId") ?? ""; + validateAppId(appId); + const original = context.req.raw; + const url = new URL(original.url); + const parameterOffset = context.req.routePath.indexOf("/:appId"); + const mountPath = + parameterOffset < 0 + ? "" + : context.req.routePath.slice(0, parameterOffset); + const applicationPath = `${mountPath}/${appId}`; + const suffix = context.req.path.startsWith(applicationPath) + ? context.req.path.slice(applicationPath.length) + : ""; + if (suffix === "") { + url.pathname = `${url.pathname}/`; + return Response.redirect(url, 308); + } + url.pathname = suffix.startsWith("/") ? suffix : `/${suffix}`; + const forwarded = new Request(url, original); + return await (options.client ?? getDefaultClient()).agentOSAppsApp + .getOrCreate([appId]) + .fetch(forwarded); + } catch (error) { + return errorResponse(error); + } + }; + router.all("/:appId", handler); + router.all("/:appId/*", handler); + return router; +} + +export const appsRouter = createAppsRouter(); + +/** @internal Test-only reset for verifying lazy client creation. */ +export function resetDefaultRouterClientForTest(): void { + defaultClient = undefined; +} diff --git a/packages/dynamic-apps/src/runtime.ts b/packages/dynamic-apps/src/runtime.ts new file mode 100644 index 000000000..d9c772b51 --- /dev/null +++ b/packages/dynamic-apps/src/runtime.ts @@ -0,0 +1,560 @@ +import { createHash } from "node:crypto"; +import { posix } from "node:path"; +import { controlFetch } from "./control-request.js"; + +const MAX_FILE_PATH_BYTES = 1_024; +const MAX_ENGINE_RESPONSE_BYTES = 1024 * 1024; +const MAX_ENGINE_DATACENTERS = 128; +const DEFAULT_SERVERLESS_REQUEST_LIFESPAN_SECONDS = 60 * 60; +const DEFAULT_SERVERLESS_MAX_RUNNERS = 1_024; +const DEFAULT_SERVERLESS_METADATA_POLL_INTERVAL_MS = 1_000; + +export const APP_CALLBACK_SECRET_HEADER = "x-agentos-app-callback-token"; + +/** Stable per-app pool so several apps can share one Rivet namespace safely. */ +export function appRunnerPool(appId: string): string { + const suffix = createHash("sha256").update(appId).digest("hex").slice(0, 16); + return `agentos-apps-${suffix}`; +} + +async function readBoundedText(response: Response): Promise { + if (!response.body) return ""; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let bytes = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + bytes += value.byteLength; + if (bytes > MAX_ENGINE_RESPONSE_BYTES) { + await reader.cancel(); + throw new RangeError( + `Rivet Engine response exceeded ${MAX_ENGINE_RESPONSE_BYTES} bytes`, + ); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + return new TextDecoder().decode(Buffer.concat(chunks)); +} + +function engineUrl(endpoint: string, path: string[], namespace: string): URL { + const url = new URL(endpoint); + url.pathname = `/${path.map(encodeURIComponent).join("/")}`; + url.search = ""; + url.searchParams.set("namespace", namespace); + return url; +} + +/** Idempotently points a guest namespace's serverless pool at its Apps actor. */ +export async function ensureServerlessRunnerConfig(input: { + endpoint: string; + namespace: string; + url: string; + pool: string; + token?: string; + callbackSecret: string; +}): Promise { + const headers = { + accept: "application/json", + "content-type": "application/json", + ...(input.token ? { authorization: `Bearer ${input.token}` } : {}), + }; + const datacentersResponse = await controlFetch( + engineUrl(input.endpoint, ["datacenters"], input.namespace), + { headers }, + ); + const datacentersText = await readBoundedText(datacentersResponse); + if (!datacentersResponse.ok) { + throw new Error( + `Rivet datacenter lookup failed with HTTP ${datacentersResponse.status}: ${datacentersText}`, + ); + } + const parsed = JSON.parse(datacentersText) as { + datacenters?: Array<{ name?: unknown }>; + }; + if ( + !Array.isArray(parsed.datacenters) || + parsed.datacenters.length === 0 || + parsed.datacenters.length > MAX_ENGINE_DATACENTERS + ) { + throw new Error( + `Rivet datacenter lookup returned an invalid count; expected 1-${MAX_ENGINE_DATACENTERS}`, + ); + } + const datacenters: Record = {}; + for (const datacenter of parsed.datacenters) { + if (typeof datacenter.name !== "string" || datacenter.name.length === 0) { + throw new Error("Rivet datacenter lookup returned an invalid name"); + } + datacenters[datacenter.name] = { + serverless: { + url: input.url, + headers: { + [APP_CALLBACK_SECRET_HEADER]: input.callbackSecret, + }, + request_lifespan: DEFAULT_SERVERLESS_REQUEST_LIFESPAN_SECONDS, + metadata_poll_interval: DEFAULT_SERVERLESS_METADATA_POLL_INTERVAL_MS, + max_runners: DEFAULT_SERVERLESS_MAX_RUNNERS, + min_runners: 0, + runners_margin: 0, + slots_per_runner: 1, + }, + metadata: {}, + drain_on_version_upgrade: true, + }; + } + + const response = await controlFetch( + engineUrl(input.endpoint, ["runner-configs", input.pool], input.namespace), + { + method: "PUT", + headers, + body: JSON.stringify({ datacenters }), + }, + ); + const responseText = await readBoundedText(response); + if (!response.ok) { + throw new Error( + `Rivet runner config upsert failed with HTTP ${response.status}: ${responseText}`, + ); + } +} + +export function normalizeAppPath(input: string): string { + if (typeof input !== "string" || input.length === 0 || input.includes("\0")) { + throw new Error( + "application file paths must be non-empty strings without NUL bytes", + ); + } + const normalized = posix.normalize(`/${input}`).slice(1); + if ( + input.startsWith("/") || + input.split("/").includes("..") || + normalized === "" || + normalized === "." || + normalized === ".." || + normalized.startsWith("../") || + Buffer.byteLength(normalized) > MAX_FILE_PATH_BYTES + ) { + throw new Error( + `application file path escapes its root: ${JSON.stringify(input)}`, + ); + } + return normalized; +} + +export function canonicalDeploymentHash(input: { + files: Record; + entrypoint: string; + build: boolean; + staticRoot?: string; + packagingIdentity: string; + deploymentIdentity?: string; +}): string { + const hash = createHash("sha256"); + // The release hash covers generated runner semantics as well as user input. + // Bump this tag whenever runnerSource changes so an existing deployment cannot + // reuse an artifact built with an older host adapter. + hash.update("agentos-apps-release-v15\0"); + const field = (value: string | Uint8Array) => { + const bytes = typeof value === "string" ? Buffer.from(value) : value; + const length = Buffer.allocUnsafe(8); + length.writeBigUInt64BE(BigInt(bytes.byteLength)); + hash.update(length); + hash.update(bytes); + }; + for (const [path, content] of Object.entries(input.files).sort(([a], [b]) => + a < b ? -1 : a > b ? 1 : 0, + )) { + field(normalizeAppPath(path)); + field(content); + } + field(normalizeAppPath(input.entrypoint)); + field(JSON.stringify({ build: input.build, staticRoot: input.staticRoot })); + field(input.packagingIdentity); + field(input.deploymentIdentity ?? ""); + return hash.digest("hex"); +} + +export function releaseEnvoyVersion(release: string): number { + const version = Number.parseInt(release.slice(0, 8), 16) & 0x7fffffff; + return version || 1; +} + +export function runtimeLoopbackPort( + endpoint: string | undefined, +): number | undefined { + if (!endpoint) return undefined; + const url = new URL(endpoint); + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new TypeError(`unsupported Rivet endpoint protocol: ${url.protocol}`); + } + const hostname = url.hostname.toLowerCase(); + const loopback = + hostname === "localhost" || + hostname === "[::1]" || + hostname === "::1" || + /^127(?:\.|$)/.test(hostname); + if (!loopback) return undefined; + if (url.port) return Number.parseInt(url.port, 10); + return url.protocol === "http:" ? 80 : 443; +} + +export function runnerSource(input: { + entrypoint: string; + release: string; + port: number; + maxRequestBytes: number; + maxResponseBytes: number; + usesRivetKit: boolean; +}): string { + const entrypoint = `./${normalizeAppPath(input.entrypoint)}`; + const rivetKitImport = input.usesRivetKit + ? 'import { createRequire } from "node:module";' + : ""; + const rivetKitBootstrap = input.usesRivetKit + ? `// agentOS applies per-process environment state before evaluating this module's +// body. Initialize RivetKit's wasm-bindgen module from bytes because Node fetch +// does not support the file: URL used by wasm-bindgen's default initializer. +const [ + { default: initializeRivetKit }, + { readFile }, + { Registry }, +] = + await Promise.all([ + import("@rivetkit/rivetkit-wasm"), + import("node:fs/promises"), + import("rivetkit"), + ]); +const wasmPath = + typeof __AGENTOS_RIVETKIT_WASM_PATH__ === "string" + ? new URL(__AGENTOS_RIVETKIT_WASM_PATH__, import.meta.url) + : createRequire(import.meta.url).resolve( + "@rivetkit/rivetkit-wasm/rivetkit_wasm_bg.wasm", + ); +await initializeRivetKit({ + module_or_path: await readFile(wasmPath), +}); +const originalStart = Registry.prototype.start; +Registry.prototype.start = function agentOSManagedStart() {}; +let appModule; +try { + appModule = await import(${JSON.stringify(entrypoint)}); +} finally { + Registry.prototype.start = originalStart; +} +const guestRegistry = appModule.registry; +if (typeof guestRegistry?.handler !== "function") { + throw new TypeError( + "Dynamic App using RivetKit must export const registry = setup(...)", + ); +}` + : `const appModule = await import(${JSON.stringify(entrypoint)}); +const guestRegistry = undefined;`; + return `import http from "node:http"; +${rivetKitImport} + +const release = ${JSON.stringify(input.release)}; +const port = ${input.port}; +const maxRequestBytes = ${input.maxRequestBytes}; +const maxResponseBytes = ${input.maxResponseBytes}; +${rivetKitBootstrap} +const exported = appModule.fetch ?? appModule.default?.fetch ?? appModule.default; +const appFetch = typeof exported === "function" + ? exported + : typeof exported?.fetch === "function" + ? exported.fetch.bind(exported) + : undefined; +if (!appFetch) { + throw new TypeError("Dynamic App entrypoint must export fetch(request) or a default fetch handler"); +} + +async function dispatchRequest(request) { + const response = guestRegistry !== undefined && + new URL(request.url).pathname.startsWith("/api/rivet") + ? await guestRegistry.handler(request) + : await appFetch(request); + if (!(response instanceof Response)) { + throw new TypeError("Dynamic App fetch handler must return a Response"); + } + return response; +} + +// Rivet Engine callbacks may cause the guest WASM runtime to call back into the +// Engine before returning. Keep that nested path off the VM HTTP request lane, +// which is intentionally waiting for response headers. The line protocol is +// streaming because RivetKit's /start response is a long-lived SSE body. +const rpcPrefix = "AGENTOS_APPS_RPC "; +const rpcRequests = new Map(); +let stdinBuffer = ""; +async function writeRpc(message) { + if (!process.stdout.write(rpcPrefix + JSON.stringify(message) + "\\n")) { + await new Promise((resolve) => process.stdout.once("drain", resolve)); + } +} +process.stdin.setEncoding("utf8"); +process.stdin.on("data", (chunk) => { + stdinBuffer += chunk; + for (;;) { + const newline = stdinBuffer.indexOf("\\n"); + if (newline < 0) break; + const line = stdinBuffer.slice(0, newline); + stdinBuffer = stdinBuffer.slice(newline + 1); + if (!line) continue; + void (async () => { + let id = "unknown"; + try { + const message = JSON.parse(line); + id = String(message.id); + if (message.event === "cancel") { + const rpc = rpcRequests.get(id); + rpc?.abort.abort(new Error("host cancelled guest request")); + rpc?.ack?.(); + return; + } + if (message.event === "ack") { + const rpc = rpcRequests.get(id); + rpc?.ack?.(); + if (rpc) rpc.ack = undefined; + return; + } + const abort = new AbortController(); + const rpc = { abort, ack: undefined }; + rpcRequests.set(id, rpc); + const body = message.bodyBase64 + ? Buffer.from(message.bodyBase64, "base64") + : undefined; + const request = new Request(message.url, { + method: message.method, + headers: message.headers, + body: message.method === "GET" || message.method === "HEAD" + ? undefined + : body, + signal: abort.signal, + }); + const response = await dispatchRequest(request); + const headers = []; + response.headers.forEach((value, name) => { + if (name !== "set-cookie") headers.push([name, value]); + }); + for (const cookie of response.headers.getSetCookie?.() ?? []) { + headers.push(["set-cookie", cookie]); + } + await writeRpc({ + id, + event: "head", + status: response.status, + statusText: response.statusText, + headers, + }); + if (response.body) { + for await (const chunk of response.body) { + if (chunk.byteLength > maxResponseBytes) { + throw new RangeError("Response chunk exceeds Dynamic Apps limit"); + } + const acknowledged = new Promise((resolve) => { + rpc.ack = resolve; + }); + await writeRpc({ + id, + event: "chunk", + bodyBase64: Buffer.from(chunk).toString("base64"), + }); + await acknowledged; + if (abort.signal.aborted) return; + } + } + await writeRpc({ id, event: "end" }); + } catch (error) { + if (rpcRequests.get(id)?.abort.signal.aborted) return; + console.error("Dynamic App RPC request failed", error); + await writeRpc({ + id, + event: "error", + status: 500, + statusText: "Internal Server Error", + headers: [["content-type", "text/plain; charset=utf-8"]], + message: "Internal Server Error", + }); + } finally { + rpcRequests.delete(id); + } + })(); + } +}); + +http.createServer(async (incoming, outgoing) => { + const abort = new AbortController(); + const cancel = () => abort.abort(new Error("client disconnected")); + incoming.once("aborted", cancel); + outgoing.once("close", () => { + if (!outgoing.writableEnded) cancel(); + }); + try { + if (incoming.url === "/.agentos/ready") { + outgoing.setHeader("content-type", "application/json"); + outgoing.end(JSON.stringify({ release })); + return; + } + + const body = await new Promise((resolve, reject) => { + const chunks = []; + let requestBytes = 0; + let tooLarge = false; + incoming.on("data", (chunk) => { + requestBytes += chunk.byteLength; + if (requestBytes > maxRequestBytes) { + tooLarge = true; + chunks.length = 0; + return; + } + if (!tooLarge) chunks.push(Buffer.from(chunk)); + }); + incoming.once("end", () => resolve({ + tooLarge, + bytes: tooLarge ? undefined : Buffer.concat(chunks), + })); + incoming.once("error", reject); + }); + if (body.tooLarge) { + outgoing.writeHead(413); + outgoing.end("Request body exceeds Dynamic Apps limit"); + return; + } + + const origin = "http://" + (incoming.headers.host ?? "agentos-app"); + const request = new Request(new URL(incoming.url ?? "/", origin), { + method: incoming.method, + headers: incoming.headers, + body: incoming.method === "GET" || incoming.method === "HEAD" + ? undefined + : body.bytes, + signal: abort.signal, + }); + const response = await dispatchRequest(request); + + outgoing.statusCode = response.status; + outgoing.statusMessage = response.statusText; + const setCookies = response.headers.getSetCookie?.() ?? []; + response.headers.forEach((value, name) => { + if (name !== "set-cookie") outgoing.setHeader(name, value); + }); + if (setCookies.length > 0) outgoing.setHeader("set-cookie", setCookies); + outgoing.flushHeaders?.(); + if (!response.body) { + outgoing.end(); + return; + } + + let responseBytes = 0; + for await (const chunk of response.body) { + responseBytes += chunk.byteLength; + if (responseBytes > maxResponseBytes) { + throw new RangeError("Response body exceeds Dynamic Apps limit"); + } + if (!outgoing.write(Buffer.from(chunk))) { + await new Promise((resolve) => outgoing.once("drain", resolve)); + } + } + outgoing.end(); + } catch (error) { + console.error("Dynamic App request failed", error); + if (!outgoing.headersSent) outgoing.writeHead(500); + if (!outgoing.writableEnded) outgoing.end("Internal Server Error"); + } +}).listen(port, "0.0.0.0"); +`; +} + +export function staticRunnerSource(input: { + root: string; + release: string; + port: number; +}): string { + const root = normalizeAppPath(input.root === "." ? "index.html" : input.root); + const staticRoot = input.root === "." ? "." : root; + return `import http from "node:http"; +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import { extname, join, normalize, resolve, sep } from "node:path"; + +const release = ${JSON.stringify(input.release)}; +const port = ${input.port}; +const root = resolve("/app", ${JSON.stringify(staticRoot)}); +const types = { + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml", + ".txt": "text/plain; charset=utf-8", + ".wasm": "application/wasm", + ".webp": "image/webp", +}; + +http.createServer(async (request, response) => { + try { + const url = new URL(request.url ?? "/", "http://agentos-app"); + if (url.pathname === "/.agentos/ready") { + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify({ release })); + return; + } + if (request.method !== "GET" && request.method !== "HEAD") { + response.writeHead(405, { allow: "GET, HEAD" }); + response.end("Method Not Allowed"); + return; + } + const decoded = decodeURIComponent(url.pathname); + const relative = normalize(decoded).replace(/^[/\\\\]+/, ""); + let path = resolve(root, relative || "index.html"); + if (path !== root && !path.startsWith(root + sep)) { + response.writeHead(400); + response.end("Bad Request"); + return; + } + let info; + try { + info = await stat(path); + if (info.isDirectory()) { + path = join(path, "index.html"); + info = await stat(path); + } + } catch { + path = join(root, "index.html"); + info = await stat(path).catch(() => null); + } + if (!info?.isFile()) { + response.writeHead(404); + response.end("Not Found"); + return; + } + response.setHeader("content-type", types[extname(path).toLowerCase()] ?? "application/octet-stream"); + response.setHeader("content-length", String(info.size)); + if (request.method === "HEAD") { + response.end(); + return; + } + createReadStream(path).on("error", (error) => { + console.error("static response failed", error); + if (!response.headersSent) response.writeHead(500); + response.end(); + }).pipe(response); + } catch (error) { + console.error("static request failed", error); + if (!response.headersSent) response.writeHead(500); + response.end("Internal Server Error"); + } +}).listen(port, "0.0.0.0"); +`; +} diff --git a/packages/dynamic-apps/src/source.ts b/packages/dynamic-apps/src/source.ts new file mode 100644 index 000000000..5c2937db4 --- /dev/null +++ b/packages/dynamic-apps/src/source.ts @@ -0,0 +1,128 @@ +import { lstat, readdir, readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { AgentOSAppsError } from "./errors.js"; +import { normalizeAppPath } from "./runtime.js"; +import type { DeployAppInput } from "./types.js"; + +const DEFAULT_MAX_FILES = 2_000; +const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024; +const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024; +const IGNORED_DIRECTORIES = new Set([".git", ".agentos", "node_modules"]); +const APP_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62})$/; + +export function validateAppId(appId: string): void { + if (!APP_ID_PATTERN.test(appId)) { + throw new AgentOSAppsError( + "agentos_apps_invalid_app_id", + "appId must be 1-63 lowercase letters, digits, or hyphens, beginning with a letter or digit", + { appId }, + ); + } +} + +function enforceFileBounds( + path: string, + content: Uint8Array, + state: { files: number; bytes: number }, +): void { + state.files += 1; + state.bytes += content.byteLength; + if (state.files > DEFAULT_MAX_FILES) { + throw new AgentOSAppsError( + "agentos_apps_file_limit", + `application contains more than maxFiles ${DEFAULT_MAX_FILES}; reduce the source tree`, + { limit: DEFAULT_MAX_FILES }, + ); + } + if (content.byteLength > DEFAULT_MAX_FILE_BYTES) { + throw new AgentOSAppsError( + "agentos_apps_file_size_limit", + `${path} is ${content.byteLength} bytes, exceeding maxFileBytes ${DEFAULT_MAX_FILE_BYTES}; reduce the file size`, + { path, observed: content.byteLength, limit: DEFAULT_MAX_FILE_BYTES }, + ); + } + if (state.bytes > DEFAULT_MAX_SOURCE_BYTES) { + throw new AgentOSAppsError( + "agentos_apps_source_limit", + `application source exceeds maxSourceBytes ${DEFAULT_MAX_SOURCE_BYTES}; reduce the source tree`, + { observed: state.bytes, limit: DEFAULT_MAX_SOURCE_BYTES }, + ); + } +} + +async function loadDirectory(source: URL): Promise> { + if (source.protocol !== "file:") { + throw new AgentOSAppsError( + "agentos_apps_invalid_source", + "deployApp source must be a file: directory URL", + { protocol: source.protocol }, + ); + } + const root = fileURLToPath(source); + const rootStat = await lstat(root); + if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) { + throw new AgentOSAppsError( + "agentos_apps_invalid_source", + "deployApp source must reference a real directory, not a symlink", + ); + } + const output: Record = {}; + const bounds = { files: 0, bytes: 0 }; + + const visit = async (directory: string, prefix: string): Promise => { + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + const path = `${directory}/${entry.name}`; + const entryStat = await lstat(path); + if (entryStat.isSymbolicLink()) { + throw new AgentOSAppsError( + "agentos_apps_source_symlink", + `application source contains unsupported symlink ${relative}`, + { path: relative }, + ); + } + if (entryStat.isDirectory()) { + if (!IGNORED_DIRECTORIES.has(entry.name)) { + await visit(path, relative); + } + continue; + } + if (!entryStat.isFile()) { + throw new AgentOSAppsError( + "agentos_apps_source_file_type", + `application source contains unsupported file type ${relative}`, + { path: relative }, + ); + } + const normalized = normalizeAppPath(relative); + const content = new Uint8Array(await readFile(path)); + enforceFileBounds(normalized, content, bounds); + output[normalized] = content; + } + }; + + await visit(root, ""); + return output; +} + +export async function prepareSource( + input: DeployAppInput, +): Promise> { + validateAppId(input.appId); + if ("source" in input && input.source) return loadDirectory(input.source); + + const output: Record = {}; + const bounds = { files: 0, bytes: 0 }; + for (const [path, value] of Object.entries(input.files).sort(([a], [b]) => + a.localeCompare(b), + )) { + const normalized = normalizeAppPath(path); + const content = + typeof value === "string" ? new TextEncoder().encode(value) : value; + enforceFileBounds(normalized, content, bounds); + output[normalized] = new Uint8Array(content); + } + return output; +} diff --git a/packages/dynamic-apps/src/types.ts b/packages/dynamic-apps/src/types.ts new file mode 100644 index 000000000..a92cbe56f --- /dev/null +++ b/packages/dynamic-apps/src/types.ts @@ -0,0 +1,60 @@ +export interface AppScaling { + minReplicas?: number; + maxReplicas?: number; + targetConcurrency?: number; +} + +interface DeployAppBase { + /** Stable URL-safe identifier used for routing and namespace isolation. */ + appId: string; + /** + * Create a stable Rivet namespace for this app. By default, deployments use + * the namespace already configured for the ordinary Rivet connection. + */ + createNamespace?: boolean; + regions?: string[]; + scaling?: AppScaling; +} + +export type DeployAppInput = + | (DeployAppBase & { + /** Local application directory. */ + source: URL; + files?: never; + }) + | (DeployAppBase & { + /** Complete generated application tree. */ + files: Record; + source?: never; + }); + +export interface Deployment { + appId: string; + release: string; + namespace: string; + pool: string; + regions: string[]; +} + +export interface AppReleaseInfo { + release: string; + artifactHash: string; + artifactBytes: number; + createdAt: number; + regions: string[]; + scaling: Required; + status: "building" | "ready" | "failed"; + error?: string; +} + +export interface PreparedDeployAppInput { + appId: string; + files: Record; + regions?: string[]; + scaling?: AppScaling; + namespace: string; + runtime: { + endpoint: string; + pool: string; + }; +} diff --git a/packages/dynamic-apps/tests/apps.test.ts b/packages/dynamic-apps/tests/apps.test.ts new file mode 100644 index 000000000..6c87e3cfc --- /dev/null +++ b/packages/dynamic-apps/tests/apps.test.ts @@ -0,0 +1,1454 @@ +import { createHash } from "node:crypto"; +import { + mkdir, + mkdtemp, + readFile, + rm, + stat, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { AgentOs } from "@rivet-dev/agentos-core"; +import { appsBuilderVersion } from "@rivet-dev/dynamic-apps-builder"; +import { Hono } from "hono"; +import { setup } from "rivetkit"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { + createAppsActors, + migrateAppsTables, + normalizeScaling, + normalizeServerlessCallbackPath, + replicaGuestEnvironment, + replicaLoopbackExemptPorts, + resolveAppCallbackSecret, + type ScalerState, +} from "../src/actors.js"; +import { + configureAppNamespaceRunner, + provisionAppNamespace, + type ResolvedRivetConnection, +} from "../src/control-plane.js"; +import { deployApp } from "../src/deploy.js"; +import { + AgentOSAppsError, + DynamicAppsError, + setupApps, + setup as setupDynamicApps, +} from "../src/index.js"; +import { + type AgentOSAppsRoutingClient, + createAppsRouter, +} from "../src/router.js"; +import { + appRunnerPool, + canonicalDeploymentHash, + ensureServerlessRunnerConfig, + releaseEnvoyVersion, + runnerSource, + runtimeLoopbackPort, + staticRunnerSource, +} from "../src/runtime.js"; +import { prepareSource } from "../src/source.js"; +import type { PreparedDeployAppInput } from "../src/types.js"; + +vi.mock("@rivet-dev/agentos-toolchain", () => ({ + packAospkgFromTarBytes(source: Buffer) { + return { bytes: source, summary: { name: "app", version: "test" } }; + }, +})); + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + while (temporaryDirectories.length > 0) { + const path = temporaryDirectories.pop(); + if (path) await rm(path, { recursive: true, force: true }); + } +}); + +function logger() { + return { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; +} + +function typecheckPublicApi(): void { + const { appsActors } = setupApps(); + const registry = setup({ use: { ...appsActors } }); + void registry; + void deployApp({ + appId: "directory-app", + source: new URL("../fixtures/app/", import.meta.url), + }); + void deployApp({ + appId: "memory-app", + createNamespace: true, + files: { + "index.html": "

Hello

", + "logo.png": new Uint8Array([137, 80, 78, 71]), + }, + scaling: { maxReplicas: 16 }, + }); +} +void typecheckPublicApi; + +describe("public API", () => { + test("exports a RivetKit-shaped setup wrapper and one error constructor", () => { + const registry = setupDynamicApps({ use: {} }); + expect(registry).toBeDefined(); + expect(AgentOSAppsError).toBe(DynamicAppsError); + expect(new AgentOSAppsError("test", "message")).toBeInstanceOf( + DynamicAppsError, + ); + }); + + test("keeps one callback credential across app rollouts", () => { + expect( + resolveAppCallbackSecret( + [{ callbackSecret: "older" }, { callbackSecret: "newer" }], + { callbackSecret: "active" }, + ), + ).toBe("active"); + expect( + resolveAppCallbackSecret([ + { callbackSecret: "" }, + { callbackSecret: "existing" }, + ]), + ).toBe("existing"); + }); + + test("returns only the three stable actor definitions without doing I/O", () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + const result = setupApps(); + + expect(Object.keys(result)).toEqual(["appsActors"]); + expect(Object.keys(result.appsActors)).toEqual([ + "agentOSAppsApp", + "agentOSAppsScaler", + "agentOSAppsReplica", + ]); + expect(fetchMock).not.toHaveBeenCalled(); + expect( + (result.appsActors.agentOSAppsApp.config.options as { noSleep?: boolean }) + .noSleep, + ).not.toBe(true); + expect( + ( + result.appsActors.agentOSAppsReplica.config.options as { + noSleep?: boolean; + } + ).noSleep, + ).toBe(true); + }); + + test("uses scale-to-zero, 128 replicas, and concurrency 8 by default", () => { + expect(normalizeScaling(undefined)).toEqual({ + minReplicas: 0, + maxReplicas: 128, + targetConcurrency: 8, + }); + expect(normalizeScaling({ maxReplicas: 12 })).toEqual({ + minReplicas: 0, + maxReplicas: 12, + targetConcurrency: 8, + }); + expect(() => normalizeScaling({ minReplicas: 2, maxReplicas: 1 })).toThrow( + "cannot exceed", + ); + }); +}); + +describe("conditional Rivet Engine access", () => { + const configuration = { + appId: "hello", + release: "release-1", + artifactHash: "hash", + artifactBytes: 4, + namespace: "app-hello", + envoyVersion: 7, + runtime: { + endpoint: "http://127.0.0.1:6420", + namespace: "app-hello", + pool: "agentos-apps-hello", + }, + }; + + test("gives plain and static releases no Rivet environment or Engine exemption", () => { + expect(replicaGuestEnvironment(configuration)).toEqual({ + NODE_ENV: "production", + }); + expect(replicaLoopbackExemptPorts(configuration)).toEqual([]); + }); + + test("gives RivetKit releases non-secret routing metadata and only the scoped proxy port", () => { + const input = { ...configuration, usesRivetKit: true }; + + expect( + replicaGuestEnvironment(input, "http://127.0.0.1:3081/capability"), + ).toMatchObject({ + RIVET_ENDPOINT: "http://127.0.0.1:3081/capability", + RIVET_NAMESPACE: "app-hello", + RIVET_POOL: "agentos-apps-hello", + RIVET_RUNNER: "agentos-apps-hello", + RIVET_RUNNER_POOL: "agentos-apps-hello", + }); + expect(replicaGuestEnvironment(input)).not.toHaveProperty("RIVET_TOKEN"); + expect(replicaLoopbackExemptPorts(input, 3_081)).toEqual([3_081]); + }); + + test("adds release security columns to an existing SQLite table once", async () => { + const columns = new Set(["release_id"]); + const statements: string[] = []; + const database = { + execute: vi.fn(async (sql: string) => { + statements.push(sql); + if (sql.startsWith("PRAGMA table_info")) { + return [...columns].map((name) => ({ name })); + } + if (sql.includes("ADD COLUMN callback_secret")) + columns.add("callback_secret"); + if (sql.includes("ADD COLUMN uses_rivetkit")) + columns.add("uses_rivetkit"); + return []; + }), + }; + + await migrateAppsTables(database as never); + await migrateAppsTables(database as never); + + expect( + statements.filter((sql) => + sql.includes("ALTER TABLE agentos_apps_releases"), + ), + ).toHaveLength(2); + expect( + statements.find((sql) => + sql.includes("CREATE TABLE IF NOT EXISTS agentos_apps_releases"), + ), + ).toContain("uses_rivetkit INTEGER NOT NULL DEFAULT 0"); + expect( + statements.find((sql) => + sql.includes("CREATE TABLE IF NOT EXISTS agentos_apps_releases"), + ), + ).toContain("callback_secret TEXT NOT NULL DEFAULT ''"); + }); +}); + +describe("source loading and deployment facade", () => { + test("loads sorted binary files, ignores fixed local directories, and preserves empties", async () => { + const root = await mkdtemp(join(tmpdir(), "agentos-apps-source-")); + temporaryDirectories.push(root); + await mkdir(join(root, "assets"), { recursive: true }); + await mkdir(join(root, "node_modules", "ignored"), { recursive: true }); + await writeFile(join(root, "assets", "empty.txt"), ""); + await writeFile( + join(root, "assets", "logo.bin"), + new Uint8Array([0, 1, 255]), + ); + await writeFile(join(root, "index.html"), "hello"); + await writeFile(join(root, "node_modules", "ignored", "index.js"), "bad"); + + const files = await prepareSource({ + appId: "source-app", + source: new URL(`file://${root}/`), + }); + + expect(Object.keys(files)).toEqual([ + "assets/empty.txt", + "assets/logo.bin", + "index.html", + ]); + expect(files["assets/empty.txt"]).toHaveLength(0); + expect([...files["assets/logo.bin"]!]).toEqual([0, 1, 255]); + }); + + test("rejects source symlinks and invalid app IDs", async () => { + const root = await mkdtemp(join(tmpdir(), "agentos-apps-symlink-")); + temporaryDirectories.push(root); + await writeFile(join(root, "index.html"), "hello"); + await symlink(join(root, "index.html"), join(root, "linked.html")); + + await expect( + prepareSource({ + appId: "source-app", + source: new URL(`file://${root}/`), + }), + ).rejects.toMatchObject({ code: "agentos_apps_source_symlink" }); + await expect( + prepareSource({ appId: "Not Valid", files: { "index.html": "x" } }), + ).rejects.toMatchObject({ code: "agentos_apps_invalid_app_id" }); + }); + + test("deploys in-memory bytes through an ordinary supplied client", async () => { + const calls: unknown[] = []; + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + vi.stubEnv("RIVET_ENGINE", "http://existing.test"); + vi.stubEnv("RIVET_NAMESPACE", "existing"); + const client = { + agentOSAppsApp: { + getOrCreate: (key: string | string[]) => ({ + resolve: async () => "app-actor-id", + deploy: async (input: unknown) => { + calls.push({ key, input }); + return { + appId: "memory-app", + release: "release-1", + namespace: "namespace-1", + pool: appRunnerPool("memory-app"), + regions: ["local"], + appActorId: "app-actor-id", + usesRivetKit: false, + }; + }, + }), + }, + }; + + const result = await deployApp( + { + appId: "memory-app", + files: { + "index.html": "

Hello

", + "asset.bin": new Uint8Array([0, 255]), + }, + }, + { client }, + ); + + expect(result).toEqual({ + appId: "memory-app", + release: "release-1", + namespace: "namespace-1", + pool: appRunnerPool("memory-app"), + regions: ["local"], + }); + expect(calls).toHaveLength(1); + expect(calls[0]).toMatchObject({ + key: ["memory-app"], + input: { + appId: "memory-app", + namespace: "existing", + runtime: { + pool: appRunnerPool("memory-app"), + }, + files: { + "index.html": expect.any(Uint8Array), + "asset.bin": expect.any(Uint8Array), + }, + }, + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + test("creates a namespace only when requested by deployApp", async () => { + let createdName: string | undefined; + const requests: Array<{ url: URL; init?: RequestInit }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = new URL(String(input)); + requests.push({ url, init }); + if (url.pathname === "/namespaces" && init?.method === "POST") { + createdName = (JSON.parse(String(init.body)) as { name: string }) + .name; + return Response.json({}); + } + if (url.pathname === "/namespaces") { + return Response.json({ + namespaces: createdName ? [{ name: createdName }] : [], + }); + } + throw new Error(`unexpected control request ${url}`); + }), + ); + vi.stubEnv("RIVET_ENGINE", "http://engine.test"); + const deploy = vi.fn(async (input: PreparedDeployAppInput) => ({ + appId: input.appId, + release: "release-1", + namespace: input.namespace, + pool: input.runtime.pool, + regions: ["local"], + appActorId: "app-actor-id", + usesRivetKit: false, + })); + + const result = await deployApp( + { + appId: "isolated-app", + createNamespace: true, + files: { "index.html": "hello" }, + }, + { + client: { + agentOSAppsApp: { + getOrCreate: () => ({ + resolve: async () => "app-actor-id", + deploy, + }), + }, + }, + }, + ); + + expect(result.namespace).toMatch(/^agentos-app-isolated-app-[a-f0-9]{10}$/); + expect(deploy).toHaveBeenCalledWith( + expect.objectContaining({ namespace: result.namespace }), + ); + expect( + requests.filter( + (request) => + request.url.pathname === "/namespaces" && + request.init?.method === "POST", + ), + ).toHaveLength(1); + }); +}); + +describe("namespace and runner plumbing", () => { + test("creates one deterministic namespace and configures its app pool", async () => { + let createdName: string | undefined; + let lookupAttempts = 0; + const requests: Array<{ url: URL; init?: RequestInit }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = new URL(String(input)); + requests.push({ url, init }); + if (url.pathname === "/namespaces" && init?.method === "POST") { + const body = JSON.parse(String(init.body)) as { name: string }; + createdName = body.name; + return Response.json({}); + } + if (url.pathname === "/namespaces") { + lookupAttempts += 1; + if (lookupAttempts === 1) { + return new Response("retry", { status: 503 }); + } + return Response.json({ + namespaces: createdName ? [{ name: createdName }] : [], + }); + } + if (url.pathname === "/datacenters") { + return Response.json({ datacenters: [{ name: "us-west" }] }); + } + if (url.pathname.startsWith("/runner-configs/")) { + return Response.json({}); + } + throw new Error(`unexpected request ${url}`); + }), + ); + const connection: ResolvedRivetConnection = { + endpoint: "http://engine.test", + namespace: "default", + token: "secret", + }; + + const first = await provisionAppNamespace("hello", connection); + const second = await provisionAppNamespace("hello", connection); + await configureAppNamespaceRunner( + "app-actor-id", + { + endpoint: first.endpoint, + namespace: first.namespace, + pool: first.pool, + }, + "callback-secret", + connection, + ); + const runnerConfigRequest = requests.find((request) => + request.url.pathname.startsWith("/runner-configs/"), + ); + expect( + JSON.parse(String(runnerConfigRequest?.init?.body)).datacenters["us-west"] + .serverless.url, + ).toBe( + "http://engine.test/gateway/app-actor-id/request/.agentos/apps/rivet", + ); + + expect(first).toEqual(second); + expect(first.namespace).toMatch(/^agentos-app-hello-[a-f0-9]{10}$/); + expect(first.pool).toBe(appRunnerPool("hello")); + expect(runnerConfigRequest?.url.pathname).toBe( + `/runner-configs/${appRunnerPool("hello")}`, + ); + expect( + requests.filter( + (request) => + request.url.pathname === "/namespaces" && + request.init?.method === "POST", + ), + ).toHaveLength(1); + expect( + requests.find((request) => + request.url.pathname.startsWith("/runner-configs/"), + )?.init, + ).toMatchObject({ + method: "PUT", + headers: expect.objectContaining({ authorization: "Bearer secret" }), + }); + }); + + test("scopes generated namespace identity to the host namespace", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = new URL(String(input)); + if (url.pathname === "/namespaces" && init?.method === "POST") { + return Response.json({}); + } + if (url.pathname === "/namespaces") { + return Response.json({ namespaces: [] }); + } + throw new Error(`unexpected request ${url}`); + }), + ); + const base = { + endpoint: "http://engine.test", + }; + + const first = await provisionAppNamespace("hello", { + ...base, + namespace: "tenant-a", + }); + const second = await provisionAppNamespace("hello", { + ...base, + namespace: "tenant-b", + }); + + expect(first.namespace).not.toBe(second.namespace); + expect(first.pool).toBe(second.pool); + }); + + test("bounds and formats serverless runner configuration", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response("retry", { + status: 429, + headers: { "retry-after": "0" }, + }), + ) + .mockResolvedValueOnce( + Response.json({ datacenters: [{ name: "us-west" }] }), + ) + .mockResolvedValueOnce(Response.json({})); + vi.stubGlobal("fetch", fetchMock); + + await ensureServerlessRunnerConfig({ + endpoint: "http://engine.test/base", + namespace: "app-hello", + pool: "app-pool", + token: "secret", + callbackSecret: "callback-secret", + url: "http://engine.test/gateway/agentOSAppsApp/.agentos/apps/rivet", + }); + + expect(String(fetchMock.mock.calls[1]?.[0])).toBe( + "http://engine.test/datacenters?namespace=app-hello", + ); + expect(String(fetchMock.mock.calls[2]?.[0])).toBe( + "http://engine.test/runner-configs/app-pool?namespace=app-hello", + ); + expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({ + datacenters: { + "us-west": { + serverless: { + url: "http://engine.test/gateway/agentOSAppsApp/.agentos/apps/rivet", + headers: { + "x-agentos-app-callback-token": "callback-secret", + }, + request_lifespan: 3_600, + metadata_poll_interval: 1_000, + max_runners: 1_024, + min_runners: 0, + runners_margin: 0, + slots_per_runner: 1, + }, + metadata: {}, + drain_on_version_upgrade: true, + }, + }, + }); + }); +}); + +describe("serverless callback credentials", () => { + test("rejects invalid callback secrets and strips trusted credentials from the guest", async () => { + const definitions = createAppsActors(); + const onRequest = definitions.agentOSAppsApp.config.onRequest as ( + context: any, + request: Request, + ) => Promise; + const actions = definitions.agentOSAppsApp.config.actions as Record< + string, + (...args: any[]) => any + >; + const callbackSecret = "release-callback-secret"; + const releaseRow = { + release_id: "release-1", + created_at: Date.now(), + status: "ready", + entrypoint: "index.js", + artifact_hash: "hash", + artifact_bytes: 4, + build_error: null, + regions_json: JSON.stringify(["us-west"]), + scaling_json: JSON.stringify({ + minReplicas: 0, + maxReplicas: 128, + targetConcurrency: 8, + }), + namespace: "app-hello", + envoy_version: 1, + runtime_endpoint: "http://engine.test", + runtime_pool: "agentos-apps-guest", + callback_secret: callbackSecret, + uses_rivetkit: 1, + }; + const acquire = vi.fn(async () => ({ + admissionId: "admission-1", + leaseMs: 60_000, + key: ["hello", "release-1", "us-west", "0"], + release: "release-1", + region: "us-west", + replicaCount: 1, + queueDelayMs: 0, + coldStart: false, + })); + const release = vi.fn(async () => ({ released: true })); + const replicaFetch = vi.fn( + async (_input: string | URL | Request, _init?: RequestInit) => + new Response("metadata", { + headers: { "content-type": "text/plain" }, + }), + ); + const context = { + actorId: "app-actor", + key: ["hello"], + region: "us-west", + state: { + activeRelease: "release-1", + namespace: "app-hello", + revision: 1, + }, + db: { + execute: vi.fn(async (sql: string) => { + if (sql.startsWith("SELECT * FROM agentos_apps_releases")) { + return [releaseRow]; + } + throw new Error(`unexpected SQL: ${sql}`); + }), + }, + client: () => ({ + agentOSAppsScaler: { + getOrCreate: () => ({ acquire, release }), + }, + agentOSAppsReplica: { + getOrCreate: () => ({ fetch: replicaFetch }), + }, + }), + log: logger(), + }; + const callbackUrl = + "http://host.test/gateway/app-actor/request/.agentos/apps/rivet/metadata"; + + const rejected = await onRequest( + context, + new Request(callbackUrl, { + headers: { + "user-agent": "RivetEngine/test", + "x-agentos-app-callback-token": "wrong", + }, + }), + ); + + expect(rejected.status).toBe(401); + expect(acquire).not.toHaveBeenCalled(); + + const accepted = await onRequest( + context, + new Request(callbackUrl, { + headers: { + authorization: "Bearer host-management-token", + "user-agent": "RivetEngine/test", + "x-agentos-app-callback-token": callbackSecret, + "x-rivet-token": "host-management-token", + "x-safe": "yes", + }, + }), + ); + + expect(accepted.status).toBe(200); + expect(await accepted.text()).toBe("metadata"); + const forwarded = replicaFetch.mock.calls[0]?.[1] as + | { headers?: Record } + | undefined; + expect(forwarded?.headers).toMatchObject({ "x-safe": "yes" }); + expect(forwarded?.headers).not.toHaveProperty("authorization"); + expect(forwarded?.headers).not.toHaveProperty("x-rivet-token"); + expect(forwarded?.headers).not.toHaveProperty( + "x-agentos-app-callback-token", + ); + expect(release).toHaveBeenCalledWith("admission-1"); + await expect( + actions.getRelease!(context, "release-1"), + ).resolves.not.toHaveProperty("callbackSecret"); + const inspection = await actions.inspect!(context); + expect(inspection.releases[0]).not.toHaveProperty("callbackSecret"); + }); +}); + +describe("HTTP router", () => { + test("redirects a bare app path so relative static assets stay under the app", async () => { + const getOrCreate = vi.fn(); + const server = new Hono(); + server.route( + "/apps", + createAppsRouter({ + client: { + agentOSAppsApp: { getOrCreate }, + } as unknown as AgentOSAppsRoutingClient, + }), + ); + + const response = await server.request( + "http://host.test/apps/static-site?preview=1", + ); + + expect(response.status).toBe(308); + expect(response.headers.get("location")).toBe( + "http://host.test/apps/static-site/?preview=1", + ); + expect( + new URL("styles.css", response.headers.get("location")!).pathname, + ).toBe("/apps/static-site/styles.css"); + expect(getOrCreate).not.toHaveBeenCalled(); + }); + + test("forwards the canonical app root to the guest root", async () => { + const fetch = vi.fn(async () => new Response(null, { status: 204 })); + const client = { + agentOSAppsApp: { + getOrCreate: () => ({ + fetch, + }), + }, + } as unknown as AgentOSAppsRoutingClient; + const server = new Hono(); + server.route("/apps", createAppsRouter({ client })); + + const response = await server.request( + "http://host.test/apps/static-site/?preview=1", + ); + + expect(response.status).toBe(204); + expect(fetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: "http://host.test/?preview=1", + method: "GET", + }), + ); + }); + + test("normalizes Rivet Engine callbacks before forwarding them to the guest", () => { + expect( + normalizeServerlessCallbackPath( + new Request( + "http://host.test/gateway/actor-id/.agentos/apps/rivet/metadata", + { + headers: { "user-agent": "RivetEngine/test" }, + }, + ), + ), + ).toBe("/api/rivet/metadata"); + expect( + normalizeServerlessCallbackPath( + new Request( + "http://host.test/gateway/actor-id/.agentos/apps/rivet/start", + { + method: "POST", + headers: { "user-agent": "RivetEngine/test" }, + }, + ), + ), + ).toBe("/api/rivet/start"); + expect( + normalizeServerlessCallbackPath( + new Request("http://host.test/api/rivet/metadata"), + ), + ).toBeUndefined(); + }); + + test("mounts under a Hono prefix and streams the replica response", async () => { + const fetch = vi.fn(async (request: Request) => { + expect(request.headers.get("connection")).toBe("keep-alive"); + return new Response("hello world"); + }); + const client = { + agentOSAppsApp: { + getOrCreate: () => ({ + fetch, + }), + }, + } as unknown as AgentOSAppsRoutingClient; + const server = new Hono(); + server.route("/apps", createAppsRouter({ client })); + + const response = await server.request( + "http://host.test/apps/hello/chat/messages?cursor=2", + { headers: { connection: "keep-alive", "x-custom": "yes" } }, + ); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("hello world"); + expect(fetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: "http://host.test/chat/messages?cursor=2", + }), + ); + }); + + test("maps invalid application IDs before touching Rivet", async () => { + const client = { + agentOSAppsApp: { getOrCreate: vi.fn() }, + } as unknown as AgentOSAppsRoutingClient; + const response = await createAppsRouter({ client }).request("/Not-Valid"); + + expect(response.status).toBe(400); + expect(client.agentOSAppsApp.getOrCreate).not.toHaveBeenCalled(); + }); +}); + +describe("regional scaler", () => { + test("warns on upward 50% crossings, retains warm replicas, and scales to zero", async () => { + const definitions = createAppsActors(); + const actions = definitions.agentOSAppsScaler.config.actions as Record< + string, + (...args: any[]) => any + >; + const state = ( + (definitions.agentOSAppsScaler.config as any) + .createState as () => ScalerState + )(); + const release = { + release: "release-1", + artifactHash: "hash", + artifactBytes: 4, + createdAt: Date.now(), + regions: ["us-west"], + scaling: { + minReplicas: 0, + maxReplicas: 2, + targetConcurrency: 1, + }, + status: "ready" as const, + entrypoint: "index.js", + namespace: "app-hello", + envoyVersion: 1, + runtimeEndpoint: "http://localhost:6420", + runtimePool: "agentos-apps-guest", + usesRivetKit: true, + }; + const replica = { + configure: vi.fn(async () => undefined), + inspect: vi.fn(async () => ({ release: null, startedAt: null })), + vmFetch: vi.fn(async () => ({ + status: 200, + statusText: "OK", + headers: {}, + body: new TextEncoder().encode( + JSON.stringify({ release: release.release }), + ), + })), + markStarted: vi.fn(async () => undefined), + destroy: vi.fn(async () => undefined), + }; + const log = logger(); + const context = { + actorId: "scaler-test", + key: ["hello", release.release, "us-west"], + region: "us-west", + state, + client: () => ({ + agentOSAppsApp: { + getOrCreate: () => ({ getRelease: async () => release }), + }, + agentOSAppsReplica: { getOrCreate: () => replica }, + }), + keepAwake: (promise: Promise) => promise, + schedule: { after: vi.fn(async () => undefined) }, + log, + destroy: vi.fn(), + }; + + await actions.prepare!(context, { + appId: "hello", + release, + region: "us-west", + verifyReplica: true, + }); + expect(replica.configure).toHaveBeenCalledWith( + expect.objectContaining({ usesRivetKit: true }), + ); + expect(state.replicas).toHaveLength(1); + expect(log.warn).not.toHaveBeenCalledWith( + expect.objectContaining({ maxReplicas: 2 }), + ); + + const firstAdmission = await actions.acquire!(context); + await vi.waitFor(() => expect(state.replicas).toHaveLength(2)); + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn).toHaveBeenCalledWith( + expect.objectContaining({ + appId: "hello", + release: "release-1", + region: "us-west", + maxReplicas: 2, + utilizationPercent: 100, + }), + ); + await actions.release!(context, firstAdmission.admissionId); + + for (const candidate of state.replicas) { + candidate.lastUsedAt = Date.now() - 5 * 60_000 - 1; + } + await actions.reconcile!(context); + expect(state.replicas).toHaveLength(1); + expect(state.capacityWarningLatched).toBe(false); + state.replicas[0]!.lastUsedAt = Date.now() - 5 * 60_000 - 1; + await actions.reconcile!(context); + expect(state.replicas).toHaveLength(0); + + const coldAdmission = await actions.acquire!(context); + expect(coldAdmission.coldStart).toBe(true); + await vi.waitFor(() => expect(state.replicas).toHaveLength(2)); + expect(log.warn).toHaveBeenCalledTimes(2); + await actions.release!(context, coldAdmission.admissionId); + + const abandonedAdmission = await actions.acquire!(context); + state.admissions![abandonedAdmission.admissionId]!.expiresAt = + Date.now() - 1; + await actions.reconcile!(context); + expect(state.admissions![abandonedAdmission.admissionId]).toBeUndefined(); + expect( + state.replicas.reduce( + (total, candidate) => total + candidate.activeRequests, + 0, + ), + ).toBe(0); + }); + + test("recovers durable warming reservations when the scaler wakes", async () => { + const definitions = createAppsActors(); + const config = definitions.agentOSAppsScaler.config as any; + const state = (config.createState as () => ScalerState)(); + const warmingKey = ["hello", "release-1", "us-west", "3"]; + Object.assign(state, { + appId: "hello", + release: "release-1", + region: "us-west", + scaling: { + minReplicas: 1, + maxReplicas: 128, + targetConcurrency: 8, + }, + warmingReplicas: 1, + warmingReplicaKeys: [warmingKey], + nextReplicaIndex: 4, + }); + const destroyReplica = vi.fn(async () => undefined); + const context = { + actorId: "scaler-recovery", + key: ["hello", "release-1", "us-west"], + region: "us-west", + state, + client: () => ({ + agentOSAppsReplica: { + getOrCreate: vi.fn(() => ({ destroy: destroyReplica })), + }, + }), + schedule: { after: vi.fn(async () => undefined) }, + log: logger(), + destroy: vi.fn(), + }; + + await config.onWake(context); + + expect(destroyReplica).toHaveBeenCalledTimes(1); + expect(state.warmingReplicas).toBe(0); + expect(state.warmingReplicaKeys).toEqual([]); + expect(context.schedule.after).toHaveBeenCalledWith(1, "reconcile"); + expect(context.log.warn).toHaveBeenCalledWith( + expect.objectContaining({ strandedReservations: 1 }), + ); + }); + + test("destroys a replica whose warm completes after scaler retirement", async () => { + const definitions = createAppsActors(); + const actions = definitions.agentOSAppsScaler.config.actions as Record< + string, + (...args: any[]) => any + >; + const state = ( + (definitions.agentOSAppsScaler.config as any) + .createState as () => ScalerState + )(); + const release = { + release: "release-1", + artifactHash: "hash", + artifactBytes: 4, + createdAt: Date.now(), + regions: ["us-west"], + scaling: { + minReplicas: 0, + maxReplicas: 2, + targetConcurrency: 1, + }, + status: "ready" as const, + entrypoint: "index.js", + namespace: "app-hello", + envoyVersion: 1, + runtimeEndpoint: "http://localhost:6420", + runtimePool: "agentos-apps-guest", + }; + let finishSecondConfigure!: () => void; + const secondConfigure = new Promise((resolve) => { + finishSecondConfigure = resolve; + }); + const makeReplica = (configure: () => Promise) => ({ + configure: vi.fn(configure), + inspect: vi.fn(async () => ({ release: null, startedAt: null })), + vmFetch: vi.fn(async () => ({ + status: 200, + statusText: "OK", + headers: {}, + body: new TextEncoder().encode( + JSON.stringify({ release: release.release }), + ), + })), + markStarted: vi.fn(async () => undefined), + destroy: vi.fn(async () => undefined), + }); + const firstReplica = makeReplica(async () => undefined); + const secondReplica = makeReplica(() => secondConfigure); + const keepAwake = vi.fn((promise: Promise) => promise); + const context = { + actorId: "scaler-retire-during-warm", + key: ["hello", release.release, "us-west"], + region: "us-west", + state, + client: () => ({ + agentOSAppsApp: { + getOrCreate: () => ({ getRelease: async () => release }), + }, + agentOSAppsReplica: { + getOrCreate: (key: string[]) => + key.at(-1) === "0" ? firstReplica : secondReplica, + }, + }), + keepAwake, + schedule: { after: vi.fn(async () => undefined) }, + log: logger(), + destroy: vi.fn(), + }; + + await actions.prepare!(context, { + appId: "hello", + release, + region: "us-west", + verifyReplica: true, + }); + const admission = await actions.acquire!(context); + await vi.waitFor(() => expect(secondReplica.configure).toHaveBeenCalled()); + expect(state.warmingReplicas).toBe(1); + + await actions.retire!(context); + finishSecondConfigure(); + + await vi.waitFor(() => expect(secondReplica.destroy).toHaveBeenCalled()); + expect(state.warmingReplicas).toBe(0); + expect(state.warmingReplicaKeys).toEqual([]); + expect(state.replicas).not.toContainEqual( + expect.objectContaining({ key: ["hello", "release-1", "us-west", "1"] }), + ); + expect(keepAwake).toHaveBeenCalled(); + + await actions.release!(context, admission.admissionId); + expect(firstReplica.destroy).toHaveBeenCalled(); + expect(context.destroy).toHaveBeenCalled(); + }); + + test("logs a failed background warm and clears its reservation", async () => { + const definitions = createAppsActors(); + const actions = definitions.agentOSAppsScaler.config.actions as Record< + string, + (...args: any[]) => any + >; + const state = ( + (definitions.agentOSAppsScaler.config as any) + .createState as () => ScalerState + )(); + const release = { + release: "release-1", + artifactHash: "hash", + artifactBytes: 4, + createdAt: Date.now(), + regions: ["us-west"], + scaling: { + minReplicas: 0, + maxReplicas: 2, + targetConcurrency: 1, + }, + status: "ready" as const, + entrypoint: "index.js", + namespace: "app-hello", + envoyVersion: 1, + runtimeEndpoint: "http://localhost:6420", + runtimePool: "agentos-apps-guest", + }; + Object.assign(state, { + appId: "hello", + release: release.release, + region: "us-west", + scaling: release.scaling, + replicas: [ + { + key: ["hello", release.release, "us-west", "0"], + readyAt: Date.now(), + activeRequests: 0, + lastUsedAt: Date.now(), + draining: false, + }, + ], + nextReplicaIndex: 1, + }); + const warmError = new Error("warm failed"); + const warmingReplica = { + configure: vi.fn(async () => { + throw warmError; + }), + inspect: vi.fn(), + vmFetch: vi.fn(), + markStarted: vi.fn(), + destroy: vi.fn(async () => undefined), + }; + const log = logger(); + const keepAwake = vi.fn((promise: Promise) => promise); + const context = { + actorId: "scaler-failed-background-warm", + key: ["hello", release.release, "us-west"], + region: "us-west", + state, + client: () => ({ + agentOSAppsApp: { + getOrCreate: () => ({ getRelease: async () => release }), + }, + agentOSAppsReplica: { + getOrCreate: () => warmingReplica, + }, + }), + keepAwake, + schedule: { after: vi.fn(async () => undefined) }, + log, + destroy: vi.fn(), + }; + + const admission = await actions.acquire!(context); + + await vi.waitFor(() => + expect(log.error).toHaveBeenCalledWith({ + msg: "Dynamic Apps background replica warm failed", + error: warmError, + }), + ); + expect(keepAwake).toHaveBeenCalled(); + expect(warmingReplica.destroy).toHaveBeenCalled(); + expect(state.warmingReplicas).toBe(0); + expect(state.warmingReplicaKeys).toEqual([]); + + await actions.release!(context, admission.admissionId); + }); +}); + +describe("replica artifact lifecycle", () => { + test("rehydrates from chunks and deletes the temporary package after VM disposal", async () => { + const artifact = new Uint8Array([1, 2, 3, 4, 5]); + const hash = createHash("sha256").update(artifact).digest("hex"); + let mountedPath: string | undefined; + let existedDuringDispose = false; + let loopbackExemptPorts: number[] | undefined; + const spawn = vi.fn(() => ({ pid: 7 })); + const vm = { + onCronEvent: vi.fn(), + process: { + spawn, + wait: vi.fn(async () => ({ exitCode: 0 })), + signal: vi.fn(), + writeStdin: vi.fn(), + }, + filesystem: { + readFile: vi.fn(async () => new Uint8Array([9])), + }, + dispose: vi.fn(async () => { + if (mountedPath) { + existedDuringDispose = (await stat(mountedPath)).isFile(); + } + }), + }; + vi.spyOn(AgentOs, "create").mockImplementation(async (options) => { + loopbackExemptPorts = options?.loopbackExemptPorts; + mountedPath = ( + options?.mounts?.[0] as { + plugin?: { config?: { tarPath?: string } }; + } + )?.plugin?.config?.tarPath; + return vm as never; + }); + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = new URL(String(input)); + if (url.pathname === "/datacenters") { + return Response.json({ datacenters: [{ name: "local" }] }); + } + return Response.json({}); + }), + ); + vi.stubEnv("RIVET_TOKEN", "host-management-token"); + const definitions = createAppsActors(); + const replicaDefinition = definitions.agentOSAppsReplica; + const actions = replicaDefinition.config.actions as Record< + string, + (...args: any[]) => any + >; + const context = { + actorId: "replica-lifecycle", + key: ["hello", "release-1", "local", "0"], + region: "local", + state: { + configuration: null, + startedAt: null, + guestPid: null, + }, + client: () => ({ + agentOSAppsApp: { + getOrCreate: () => ({ + getArtifactManifest: async () => ({ + hash, + bytes: artifact.byteLength, + chunks: 1, + chunkBytes: 512 * 1024, + }), + readArtifactChunk: async () => artifact, + }), + }, + }), + actorRuntimeSocket: async () => ({ path: "/tmp/actor.sock" }), + db: { execute: vi.fn(async () => []) }, + keepAwake: (promise: Promise) => promise, + broadcast: vi.fn(), + log: logger(), + }; + await actions.configure!(context, { + appId: "hello", + release: "release-1", + artifactHash: hash, + artifactBytes: artifact.byteLength, + namespace: "app-hello", + envoyVersion: 1, + usesRivetKit: false, + runtime: { + endpoint: "http://localhost:6420", + namespace: "app-hello", + pool: "agentos-apps-guest", + }, + }); + + await expect(actions.readFile!(context, "/app/index.js")).resolves.toEqual( + new Uint8Array([9]), + ); + expect(spawn).toHaveBeenCalledWith( + "node", + ["/app/main.mjs"], + expect.objectContaining({ + env: { NODE_ENV: "production" }, + }), + ); + expect(loopbackExemptPorts).toEqual([]); + expect(mountedPath).toBeTruthy(); + expect(await readFile(mountedPath!)).toEqual(Buffer.from(artifact)); + + await replicaDefinition.config.onDestroy?.(context as never); + + expect(existedDuringDispose).toBe(true); + await expect(stat(mountedPath!)).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); + +describe("runtime generation", () => { + test("preserves the final legacy package release identity", async () => { + const fixture = JSON.parse( + await readFile( + new URL("./fixtures/legacy-0.2.15.json", import.meta.url), + "utf8", + ), + ) as { + actorKeys: string[]; + deploymentIdentity: string; + entrypoint: string; + filesBase64: Record; + packagingIdentity: string; + releaseId: string; + sourcePackages: Record; + sourceRevision: string; + sqliteTables: string[]; + }; + + expect(fixture.sourcePackages).toEqual({ + "@rivet-dev/agentos-apps": appsBuilderVersion, + "@agentos-software/apps-builder": appsBuilderVersion, + }); + expect(fixture.sourceRevision).toMatch(/^[a-f0-9]{40}$/); + expect(Object.keys(createAppsActors())).toEqual(fixture.actorKeys); + const migrationSql: string[] = []; + await migrateAppsTables({ + execute: async (sql: string) => { + migrationSql.push(sql); + return []; + }, + } as never); + expect( + fixture.sqliteTables.every((table) => + migrationSql.some((sql) => + sql.includes(`CREATE TABLE IF NOT EXISTS ${table}`), + ), + ), + ).toBe(true); + expect( + canonicalDeploymentHash({ + files: Object.fromEntries( + Object.entries(fixture.filesBase64).map(([path, content]) => [ + path, + new Uint8Array(Buffer.from(content, "base64")), + ]), + ), + entrypoint: fixture.entrypoint, + build: false, + packagingIdentity: fixture.packagingIdentity, + deploymentIdentity: fixture.deploymentIdentity, + }), + ).toBe(fixture.releaseId); + }); + + test("hashes binary source deterministically with length-delimited fields", () => { + const first = canonicalDeploymentHash({ + files: { a: new TextEncoder().encode("1\0b\0\0\0\0\0\0\0\x012") }, + entrypoint: "a", + build: false, + packagingIdentity: "builder@1", + }); + const second = canonicalDeploymentHash({ + files: { + a: new TextEncoder().encode("1"), + b: new TextEncoder().encode("2"), + }, + entrypoint: "a", + build: false, + packagingIdentity: "builder@1", + }); + expect(first).not.toBe(second); + expect( + canonicalDeploymentHash({ + files: { asset: new Uint8Array([0, 255]) }, + entrypoint: "asset", + build: false, + packagingIdentity: "builder@1", + }), + ).toBe( + canonicalDeploymentHash({ + files: { asset: new Uint8Array([0, 255]) }, + entrypoint: "asset", + build: false, + packagingIdentity: "builder@1", + }), + ); + expect( + canonicalDeploymentHash({ + files: { asset: new Uint8Array([0, 255]) }, + entrypoint: "asset", + build: false, + packagingIdentity: "builder@1", + deploymentIdentity: '{"regions":["us-west"]}', + }), + ).not.toBe( + canonicalDeploymentHash({ + files: { asset: new Uint8Array([0, 255]) }, + entrypoint: "asset", + build: false, + packagingIdentity: "builder@1", + deploymentIdentity: '{"regions":["eu-west"]}', + }), + ); + }); + + test("generates server and static runners with readiness endpoints", () => { + const server = runnerSource({ + entrypoint: "src/index.mjs", + release: "release", + port: 3_080, + maxRequestBytes: 1_024, + maxResponseBytes: 1_024, + usesRivetKit: true, + }); + expect(server).toContain('await import("./src/index.mjs")'); + expect(server).toContain( + 'typeof __AGENTOS_RIVETKIT_WASM_PATH__ === "string"', + ); + expect(server).toContain('"@rivetkit/rivetkit-wasm/rivetkit_wasm_bg.wasm"'); + expect(server).toContain("Registry.prototype.start = function"); + expect(server).toContain("guestRegistry.handler(request)"); + expect(server).toContain('pathname.startsWith("/api/rivet")'); + expect(server).toContain("outgoing.flushHeaders?.();"); + expect(server).toContain('incoming.url === "/.agentos/ready"'); + expect(server.indexOf("outgoing.flushHeaders?.();")).toBeLessThan( + server.lastIndexOf("for await (const chunk of response.body)"), + ); + + const plainServer = runnerSource({ + entrypoint: "src/index.mjs", + release: "release", + port: 3_080, + maxRequestBytes: 1_024, + maxResponseBytes: 1_024, + usesRivetKit: false, + }); + expect(plainServer).toContain('await import("./src/index.mjs")'); + expect(plainServer).not.toContain('import("rivetkit")'); + expect(plainServer).not.toContain("@rivetkit/rivetkit-wasm"); + expect(plainServer).not.toContain("createRequire"); + + const staticSource = staticRunnerSource({ + root: "dist", + release: "release", + port: 3_080, + }); + expect(staticSource).toContain('join(root, "index.html")'); + expect(staticSource).toContain('"dist"'); + }); + + test("derives stable envoy versions and only exposes loopback engine ports", () => { + expect(releaseEnvoyVersion(`ffffffff${"0".repeat(56)}`)).toBe( + 2_147_483_647, + ); + expect(releaseEnvoyVersion(`00000000${"f".repeat(56)}`)).toBe(1); + expect(runtimeLoopbackPort("http://127.0.0.1:6420")).toBe(6_420); + expect(runtimeLoopbackPort("https://localhost")).toBe(443); + expect(runtimeLoopbackPort("https://engine.example.com")).toBeUndefined(); + }); +}); diff --git a/packages/dynamic-apps/tests/engine-proxy.test.ts b/packages/dynamic-apps/tests/engine-proxy.test.ts new file mode 100644 index 000000000..8188bc6ff --- /dev/null +++ b/packages/dynamic-apps/tests/engine-proxy.test.ts @@ -0,0 +1,239 @@ +import { createServer } from "node:http"; +import { type AddressInfo, connect } from "node:net"; +import { afterEach, describe, expect, test } from "vitest"; +import { + registerGuestEngineProxy, + unregisterGuestEngineProxy, +} from "../src/engine-proxy.js"; + +const owners: string[] = []; +const servers: Array> = []; + +afterEach(async () => { + for (const owner of owners.splice(0)) unregisterGuestEngineProxy(owner); + await Promise.all( + servers.splice(0).map( + (server) => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); +}); + +describe("guest Engine capability proxy", () => { + test("hides credentials and restricts a guest to its namespace and pool", async () => { + const upstreamRequests: Array<{ + url: string; + authorization?: string; + body: string; + }> = []; + const upstream = createServer(async (request, response) => { + let body = ""; + for await (const chunk of request) body += chunk; + upstreamRequests.push({ + url: request.url ?? "", + authorization: request.headers.authorization, + body, + }); + response.setHeader("content-type", "application/json"); + if (request.url?.startsWith("/metadata")) { + response.end( + JSON.stringify({ + clientEndpoint: "http://engine.internal", + clientNamespace: "wrong", + clientToken: "host-secret", + }), + ); + return; + } + if (request.url?.includes("actor_ids=other-actor")) { + response.end( + JSON.stringify({ + actors: [ + { + actor_id: "other-actor", + runner_name_selector: "other-pool", + }, + ], + }), + ); + return; + } + const requestUrl = new URL(request.url ?? "/", "http://upstream"); + if ( + requestUrl.pathname === "/actors" && + requestUrl.searchParams.get("name") === "notes" + ) { + response.end( + JSON.stringify({ + actors: [ + { + actor_id: "app-actor", + runner_name_selector: "app-pool", + }, + ], + }), + ); + return; + } + response.end(JSON.stringify({ actors: [] })); + }); + let upgradeUrl = ""; + let upgradeAuthorization: string | undefined; + let upgradeProtocols: string | undefined; + upstream.on("upgrade", (request, socket) => { + upgradeUrl = request.url ?? ""; + upgradeAuthorization = request.headers.authorization; + upgradeProtocols = request.headers["sec-websocket-protocol"]; + socket.end( + "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Protocol: rivet_token.host-secret\r\n\r\n", + ); + }); + servers.push(upstream); + await new Promise((resolve, reject) => { + upstream.once("error", reject); + upstream.listen(0, "127.0.0.1", resolve); + }); + const upstreamPort = (upstream.address() as AddressInfo).port; + const owner = "replica:test"; + owners.push(owner); + const registration = await registerGuestEngineProxy({ + owner, + upstreamEndpoint: `http://127.0.0.1:${upstreamPort}`, + upstreamToken: "host-secret", + namespace: "app-namespace", + pool: "app-pool", + maxRequestBytes: 1024, + maxResponseBytes: 4096, + }); + + const metadataResponse = await fetch(`${registration.endpoint}/metadata`, { + headers: { authorization: "Bearer guest-controlled" }, + }); + expect(metadataResponse.status).toBe(200); + expect(await metadataResponse.json()).toEqual({ + clientEndpoint: registration.endpoint, + clientNamespace: "app-namespace", + }); + expect(upstreamRequests[0]).toMatchObject({ + authorization: "Bearer host-secret", + }); + expect( + new URL( + upstreamRequests[0]?.url ?? "", + "http://upstream", + ).searchParams.get("namespace"), + ).toBe("app-namespace"); + + const createResponse = await fetch(`${registration.endpoint}/actors`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "notes", + runner_name_selector: "attacker-controlled", + }), + }); + expect(createResponse.status).toBe(200); + expect(JSON.parse(upstreamRequests[1]?.body ?? "{}")).toMatchObject({ + name: "notes", + runner_name_selector: "app-pool", + }); + + expect((await fetch(`${registration.endpoint}/namespaces`)).status).toBe( + 403, + ); + expect((await fetch(`${registration.endpoint}/actors`)).status).toBe(403); + expect(upstreamRequests).toHaveLength(2); + + const gatewayResponse = await fetch( + `${registration.endpoint}/gateway/notes/action/list?rvt-method=getOrCreate&rvt-namespace=evil&rvt-pool=other-pool&rvt-runner=evil`, + { headers: { authorization: "Bearer guest-controlled" } }, + ); + expect(gatewayResponse.status).toBe(200); + expect(upstreamRequests[2]?.authorization).toBeUndefined(); + const gatewayUrl = new URL( + upstreamRequests[2]?.url ?? "", + "http://upstream", + ); + expect(gatewayUrl.searchParams.get("rvt-namespace")).toBe("app-namespace"); + expect(gatewayUrl.searchParams.get("rvt-pool")).toBe("app-pool"); + expect(gatewayUrl.searchParams.get("rvt-runner")).toBe("app-pool"); + const getResponse = await fetch( + `${registration.endpoint}/gateway/notes/action/list?rvt-method=get&rvt-key=a%2Fb,,c%5Cd`, + ); + expect(getResponse.status).toBe(200); + const lookupUrl = new URL( + upstreamRequests.at(-2)?.url ?? "", + "http://upstream", + ); + expect(lookupUrl.searchParams.get("key")).toBe("a\\/b/\\0/c\\\\d"); + const resolvedGatewayUrl = new URL( + upstreamRequests.at(-1)?.url ?? "", + "http://upstream", + ); + expect(resolvedGatewayUrl.pathname).toBe("/gateway/app-actor/action/list"); + expect( + Array.from(resolvedGatewayUrl.searchParams.keys()).some((name) => + name.startsWith("rvt-"), + ), + ).toBe(false); + const directResponse = await fetch( + `${registration.endpoint}/gateway/app-actor/action/list?rvt-namespace=evil&user-query=preserved`, + ); + expect(directResponse.status).toBe(200); + const directGatewayUrl = new URL( + upstreamRequests.at(-1)?.url ?? "", + "http://upstream", + ); + expect(directGatewayUrl.pathname).toBe("/gateway/app-actor/action/list"); + expect(directGatewayUrl.searchParams.get("user-query")).toBe("preserved"); + expect( + Array.from(directGatewayUrl.searchParams.keys()).some((name) => + name.startsWith("rvt-"), + ), + ).toBe(false); + expect( + (await fetch(`${registration.endpoint}/gateway/other-actor/action/list`)) + .status, + ).toBe(403); + expect(upstreamRequests.at(-1)?.authorization).toBe("Bearer host-secret"); + + const proxyUrl = new URL(registration.endpoint); + const upgradeResponse = await new Promise((resolve, reject) => { + const socket = connect(Number(proxyUrl.port), proxyUrl.hostname); + let response = ""; + socket.setEncoding("utf8"); + socket.on("connect", () => { + socket.write( + [ + `GET ${proxyUrl.pathname}/envoys/connect?protocol_version=8&namespace=evil&pool_name=evil HTTP/1.1`, + `Host: ${proxyUrl.host}`, + "Connection: Upgrade", + "Upgrade: websocket", + "Sec-WebSocket-Key: dGVzdC1rZXk=", + "Sec-WebSocket-Version: 13", + "", + "", + ].join("\r\n"), + ); + }); + socket.on("data", (chunk) => { + response += chunk; + }); + socket.on("end", () => resolve(response)); + socket.on("error", reject); + }); + expect(upgradeResponse).toContain("101 Switching Protocols"); + expect(upgradeResponse).not.toContain("rivet_token.host-secret"); + const upgradedUrl = new URL(upgradeUrl, "http://upstream"); + expect(upgradedUrl.searchParams.get("namespace")).toBe("app-namespace"); + expect(upgradedUrl.searchParams.get("pool_name")).toBe("app-pool"); + expect(upgradeAuthorization).toBeUndefined(); + expect(upgradeProtocols).toContain("rivet_token.host-secret"); + + unregisterGuestEngineProxy(owner); + owners.splice(owners.indexOf(owner), 1); + expect((await fetch(`${registration.endpoint}/metadata`)).status).toBe(401); + }); +}); diff --git a/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json b/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json new file mode 100644 index 000000000..1a59108f6 --- /dev/null +++ b/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json @@ -0,0 +1,25 @@ +{ + "sourcePackages": { + "@rivet-dev/agentos-apps": "0.2.15", + "@agentos-software/apps-builder": "0.2.15" + }, + "sourceRevision": "d7026219dec75a886a11d89857c68327a6f9b94a", + "actorKeys": [ + "agentOSAppsApp", + "agentOSAppsScaler", + "agentOSAppsReplica" + ], + "sqliteTables": [ + "agentos_apps_releases", + "agentos_apps_release_files", + "agentos_apps_artifact_chunks" + ], + "filesBase64": { + "index.html": "PGgxPmxlZ2FjeSBmaXh0dXJlPC9oMT4K", + "public/app.js": "AAEC/w==" + }, + "entrypoint": "index.html", + "packagingIdentity": "apps-builder@0.2.15;manifest@1;bundle@2;esbuild-wasm@0.27.4;rivetkit-adapter@6", + "deploymentIdentity": "{\"regions\":[\"us-west\"],\"scaling\":{\"minReplicas\":0,\"maxReplicas\":128,\"targetConcurrency\":8},\"namespace\":\"legacy-fixture\",\"runtime\":{\"endpoint\":\"https://api.rivet.dev\",\"pool\":\"agentos-apps-legacy\"},\"usesRivetKit\":false}", + "releaseId": "6e9dae258355860189a434505e9f0a8a89dacbdb2b8cf62423166d95611aa069" +} diff --git a/packages/dynamic-apps/tsconfig.json b/packages/dynamic-apps/tsconfig.json new file mode 100644 index 000000000..8b743bfdd --- /dev/null +++ b/packages/dynamic-apps/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src", "tests"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..e446b0867 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7187 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@rivet-dev/agentos-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + rivetkit: 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-wasm': 0.0.0-feat-workflows-public-host-apis.0ff6164 + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: 2.4.10 + version: 2.4.10 + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.21.0 + version: 4.23.12 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + + benchmarks/dynamic-apps: + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + examples/apps-ai-builder: + dependencies: + '@ai-sdk/anthropic': + specifier: ^4.0.19 + version: 4.0.39(zod@4.4.3) + '@hono/node-server': + specifier: ^2.0.11 + version: 2.1.1(hono@4.13.3) + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../packages/dynamic-apps + ai: + specifier: ^7.0.37 + version: 7.0.68(zod@4.4.3) + hono: + specifier: ^4.12.9 + version: 4.13.3 + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + examples/apps-hello-world: + dependencies: + '@hono/node-server': + specifier: ^2.0.11 + version: 2.1.1(hono@4.13.3) + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../packages/dynamic-apps + hono: + specifier: ^4.12.9 + version: 4.13.3 + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + examples/apps-multiplayer: + dependencies: + '@hono/node-server': + specifier: ^2.0.11 + version: 2.1.1(hono@4.13.3) + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../packages/dynamic-apps + hono: + specifier: ^4.12.9 + version: 4.13.3 + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + examples/apps-sqlite: + dependencies: + '@hono/node-server': + specifier: ^2.0.11 + version: 2.1.1(hono@4.13.3) + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../packages/dynamic-apps + hono: + specifier: ^4.12.9 + version: 4.13.3 + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + examples/apps-static-website: + dependencies: + '@hono/node-server': + specifier: ^2.0.11 + version: 2.1.1(hono@4.13.3) + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../packages/dynamic-apps + hono: + specifier: ^4.12.9 + version: 4.13.3 + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + examples/apps-workflows: + dependencies: + '@hono/node-server': + specifier: ^2.0.11 + version: 2.1.1(hono@4.13.3) + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../packages/dynamic-apps + hono: + specifier: ^4.12.9 + version: 4.13.3 + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/dynamic-apps: + dependencies: + '@agentos-software/sh': + specifier: 0.2.15 + version: 0.2.15 + '@agentos-software/tar': + specifier: 0.3.5 + version: 0.3.5 + '@rivet-dev/agentos': + specifier: 0.2.15 + version: 0.2.15(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(better-sqlite3@12.11.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(ws@8.21.3) + '@rivet-dev/agentos-core': + specifier: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + version: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3) + '@rivet-dev/agentos-toolchain': + specifier: 0.2.15 + version: 0.2.15 + '@rivet-dev/dynamic-apps-builder': + specifier: workspace:0.2.15 + version: link:../dynamic-apps-builder + hono: + specifier: ^4.7.0 + version: 4.13.3 + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + tsup: + specifier: ^8.4.0 + version: 8.5.1(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.20.1) + + packages/dynamic-apps-builder: + dependencies: + esbuild-wasm: + specifier: 0.27.4 + version: 0.27.4 + devDependencies: + '@rivet-dev/agentos-toolchain': + specifier: 0.2.15 + version: 0.2.15 + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.20.1) + + tests/e2e/dynamic-apps: + dependencies: + '@rivet-dev/dynamic-apps': + specifier: workspace:* + version: link:../../../packages/dynamic-apps + '@rivet-dev/dynamic-apps-builder': + specifier: workspace:* + version: link:../../../packages/dynamic-apps-builder + rivetkit: + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + devDependencies: + '@rivet-dev/agentos-toolchain': + specifier: 0.2.15 + version: 0.2.15 + '@rivetkit/engine-cli': + specifier: 0.0.0-feat-workflows-public-host-apis.0ff6164 + version: 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@types/node': + specifier: ^22.19.15 + version: 22.20.1 + get-port: + specifier: ^7.1.0 + version: 7.2.0 + tsx: + specifier: ^4.20.6 + version: 4.23.12 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + +packages: + + '@agentclientprotocol/sdk@0.16.1': + resolution: {integrity: sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + '@agentos-software/claude-code@0.2.7': + resolution: {integrity: sha512-gXmqqOUWT98QvLkQXHn1UU8OOvqMO8BRSj+0PT99mOL6XNpBlPW6L065FJ9dC4IwJC5xeGzB1XFevjOdP7LwQg==} + hasBin: true + + '@agentos-software/codex-cli@0.3.4': + resolution: {integrity: sha512-SAw3EOTa90dJLgEVVoE7JJIxHwticzdD9cEM9v00gpxhxC9vdLkeBva6rgWIUB9+qD1JEYBvUCyNkIpD2kT5YQ==} + + '@agentos-software/common@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-hTMKv/gtvxlcMuLIdCi1mWVNZcIDdnbltKnNH2EEYyAEcOk7OnAuY6eEclGrLrRfojEbWr4M8JBTzd3tRLtPoA==} + + '@agentos-software/common@0.2.15': + resolution: {integrity: sha512-33roACt3EfAp+C6mciKfthVU7I21XeulM+/yLJRo9tSuXfbMLF18EnkodKhvYAJWjzlLGhbmsxfuSuBF7afrhw==} + + '@agentos-software/coreutils@0.3.4': + resolution: {integrity: sha512-tGd0gQjUjHnm+5KgOBwidwUtlkKnl9biQuD7X2sZl15VOhYqUJpnyLF33h/nF3r9WtOTclSiLj/dc2xCxmAXnw==} + + '@agentos-software/diffutils@0.3.4': + resolution: {integrity: sha512-a5Do+ERMdHPwT0Vrp35fxSgrsJNK8wCsYX8dOAomH9N+xJyV2Hb7/LLyp7XxqGk9BEjMeA6UhbH5ZY/HAlx5sQ==} + + '@agentos-software/findutils@0.3.4': + resolution: {integrity: sha512-wjBWE3lkXe70fRj6da1+2MM/7KlPcBRhr2BycvgvAtglOT/0PtabFU9CrDuX3Vm/8acU/8tw35+WeitPOLi9mg==} + + '@agentos-software/gawk@0.3.4': + resolution: {integrity: sha512-NlU6nGxoqIUc1I2zdBHFwH04gE0tBygP2FcBO12VBptty7lbgq1CNLk909jIcSNFEwlm3VRPHeZNkbdVKZ2Ujg==} + + '@agentos-software/grep@0.3.4': + resolution: {integrity: sha512-Bta2Ljl+kCX/3Bjg06Q9N9LPRcf13S92lHRaYG+CeGVDXabIIgkHLUGIQlI1OKuIr7IRAktjgelUAuJnxGFvvw==} + + '@agentos-software/gzip@0.3.4': + resolution: {integrity: sha512-l7Y/Vwiwsqgna68yYwdNCnUBPGBg5zdmtjEFeG3hnDDFLe/02h17q0gUIqJmDBIAy1q9GoizqcFi7zXO2xygKg==} + + '@agentos-software/manifest@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-pW2lqsiiclos9b0ymoFYKZ/rEvL7wq+QthUeILWH6iYw3FQF+/LE2NSFjf/9ptPNmFYfP+wPO4640cz7NU+i6w==} + + '@agentos-software/opencode@0.2.7': + resolution: {integrity: sha512-lZspCiMgM0+kPAA08CEvyNy8lfBx463wObKpAwbYyaVvc0KfGvbAPS6VmnuZQWmaBmJJuRMtSo14nmb8XCD16g==} + hasBin: true + + '@agentos-software/pi@0.2.7': + resolution: {integrity: sha512-nOdwksByJgTqt92Ya84CzTGxpVFwGI6gkGWaCD/mmvP11JRA++1nshNdbig2mtiaxT7VG3ovWJmK7ZLTTzkhPA==} + hasBin: true + + '@agentos-software/sed@0.3.4': + resolution: {integrity: sha512-J10nZnZmme2SvXK5WMK2unQlOVncMQVUCS20GZB579a2gNoayLJYHGvfJ9a4+42wOHgDKL1u74byWlydCkEyOQ==} + + '@agentos-software/sh@0.2.15': + resolution: {integrity: sha512-ksyZBJb2yiRPPAWvYGBzsauuGU9eTmR5J/xeneS+MlrKFsQol6JOC6Yhc12YwqT4vlpTypJ1i5ZMDfXKzP3aLA==} + + '@agentos-software/tar@0.3.5': + resolution: {integrity: sha512-hSf6PY4q1luIomFSDgVHxVDDIPdAVZxdgwrQFEYH4aIE6TXX9qatVmzR36uYLWpnFGAZTR6srREGoTnmOGV4Lg==} + + '@ai-sdk/anthropic@4.0.39': + resolution: {integrity: sha512-JAMGtYeEuaBzqbsPO4fkho6vQyNoVhsHASM4o59wmJRU6Vh7prjOp490Kmc7YQTY+ioU1/xYzXvWOtxZBup0Xw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@4.0.54': + resolution: {integrity: sha512-x4fAXDqCtYzB/M5vsIQLYcyrzpJuaRgcIwDSw+lpTMMbgH19fU3ds75GSlHNLzfx6Z5yL4Z9+EMr0GJcqVy9QA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.27': + resolution: {integrity: sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@4.0.7': + resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} + engines: {node: '>=22'} + + '@anthropic-ai/claude-agent-sdk@0.2.87': + resolution: {integrity: sha512-WWmgBPxPhBOvNT0ujI8vPTI2lK+w5YEkEZ/y1mH0EDkK/0kBnxVJNhCtG5vnueiAViwLoUOFn66pbkDiivijdA==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^4.0.0 + + '@anthropic-ai/sdk@0.73.0': + resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@anthropic-ai/sdk@0.74.0': + resolution: {integrity: sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@asteasolutions/zod-to-openapi@9.1.0': + resolution: {integrity: sha512-pLMeRgRYS7/vZIgAAkOe2P6+XGTifR1MT9pqDoxZ11EmcJJ+DPmdPqLN8ZZF4U8R9KHCCQCS9c2wtuE8wSrfkw==} + peerDependencies: + zod: ^4.0.0 + + '@aws-sdk/checksums@3.1000.28': + resolution: {integrity: sha512-VCpnmyHQ1IH49ni3LXnQj7DPr7rmcJmzYeiCkYdCcfgNtkvOj38cdcL9lapBWoItZWFACJPFJlymqC7/gem3Gw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-bedrock-runtime@3.1113.0': + resolution: {integrity: sha512-yksECclekh+RPC4t5oS4RnLBGArt8g9if5b/2QQJSr8InX4r3LutS3VPHeEFlEKkwJ+qckKkPZCF2Cmv6hT1wA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1113.0': + resolution: {integrity: sha512-NRqdtohoMRyWkEeeznfG1KPN08dclCbl+HFuLPB2v8qPcgoNmTFlLKl9ELiR6hsGXQ4Ur3qvRnoJVEk8ND74pg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.8': + resolution: {integrity: sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.69': + resolution: {integrity: sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.71': + resolution: {integrity: sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.14': + resolution: {integrity: sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.76': + resolution: {integrity: sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.80': + resolution: {integrity: sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.69': + resolution: {integrity: sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.13': + resolution: {integrity: sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.75': + resolution: {integrity: sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.33': + resolution: {integrity: sha512-1Dd5WyEE2Kb3HvY44u7Ob16ST2W6iutOqsQ8Y2hUmsL2mAH/STlGS1dS9h3IOE6L7Ld3AR2HzKJ6XeCMOw8Peg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.28': + resolution: {integrity: sha512-Z1EDXnS01P7H5jVrUx+/dBqV0m7dta7bSxLclkOuDuS93pNNQm0IcT4YLUbuvWKPYNxbI8aTG0p5Br30GSKDgA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.74': + resolution: {integrity: sha512-2lzoV2z2QO5KJZYGOCnIZ1WVQgzMECvwuzr1xb034a++8QW4U4eGrmC2u4yg1xvNv4TLL/Uv5DLyuAiw0b9z7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.51': + resolution: {integrity: sha512-jdgP3jR5Q96j1jjZ98GGwpGg1CBNFIO2YE+vXg8cg8PvNY4NvgQNYJsqDaRX2PYv5gSUX/+C0D58Fhspj9ELMQ==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.43': + resolution: {integrity: sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.45': + resolution: {integrity: sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1111.0': + resolution: {integrity: sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1113.0': + resolution: {integrity: sha512-E34rqSrdHq0AWi7B8R85GE9UvyzTyBWQLpGbIw5B3BBmJEDbitumoArDCqoeecp1z81EFLoH7KETFOD6CkMedg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.4': + resolution: {integrity: sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.39': + resolution: {integrity: sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.4.10': + resolution: {integrity: sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.10': + resolution: {integrity: sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.10': + resolution: {integrity: sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.10': + resolution: {integrity: sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.4.10': + resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.4.10': + resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.4.10': + resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.4.10': + resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.10': + resolution: {integrity: sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@hono/node-server@2.1.1': + resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + + '@hono/zod-openapi@1.6.0': + resolution: {integrity: sha512-42HXUBIaGmQh+hZGLw3Hy5piOreIXgnBnUSs55mtn2gnW/xWtU9nQwEUtRRCk5to8Vm2XbfO0J83eUCwCf5eTg==} + engines: {node: '>=16.0.0'} + peerDependencies: + hono: '>=4.10.0' + zod: ^4.0.0 + + '@hono/zod-validator@0.9.0': + resolution: {integrity: sha512-n0ZSXmCiHVIp4Y5wlOOyZCeTd/rsawA/qW1cipB8QOYKZ9N8Tk0nZUZCXho9cu374AN4JpDNKioNKBJ/W+LBug==} + peerDependencies: + hono: '>=4.11.2' + zod: ^3.25.0 || ^4.0.0 + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + + '@mariozechner/jiti@2.6.5': + resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} + hasBin: true + + '@mariozechner/pi-agent-core@0.60.0': + resolution: {integrity: sha512-1zQcfFp8r0iwZCxCBQ9/ccFJoagns68cndLPTJJXl1ZqkYirzSld1zBOPxLAgeAKWIz3OX8dB2WQwTJFhmEojQ==} + engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-agent-core instead going forward + + '@mariozechner/pi-ai@0.60.0': + resolution: {integrity: sha512-OiMuXQturnEDPmA+ho7eLe4G8plO2z21yjNMs9niQREauoblWOz7Glv58I66KPzczLED4aZTlQLTRdU6t1rz8A==} + engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-ai instead going forward + hasBin: true + + '@mariozechner/pi-coding-agent@0.60.0': + resolution: {integrity: sha512-IOv7cTU4nbznFNUE5ofi13k2dmSG39coBoGWIBQTVw3iVyl0HxuHbg0NiTx3ktrPIDNtkii+y7tWXzWqwoo4lw==} + engines: {node: '>=20.6.0'} + deprecated: please use @earendil-works/pi-coding-agent instead going forward + hasBin: true + + '@mariozechner/pi-tui@0.60.0': + resolution: {integrity: sha512-ZAK5gxYhGmfJqMjfWcRBjB8glITltDbTrYJXvcDtfengbKTZN0p39p5uO5pvUB8/PiAWKTRS06yaNMhf/LG26g==} + engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-tui instead going forward + + '@mistralai/mistralai@1.14.1': + resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} + + '@modelcontextprotocol/sdk@1.30.0': + resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/cli@2.18.4': + resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} + engines: {node: '>= 10'} + hasBin: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@rivet-dev/agent-os-core@0.1.1': + resolution: {integrity: sha512-Uw5jr+gUXDY7TDUFqlypjGe1BD2KL9kTHPNo/f1iNS1R+l9IWuvT+FF/MXsLOEkc3fB06OPVu2ZvUuNwp9MpLQ==} + + '@rivet-dev/agent-os-posix@0.1.0': + resolution: {integrity: sha512-NIrI7cCb9x6jdmzRPPx7dAeXoTF/YCqf93ydEzYFA2zshIelLW9Rp5KtgP/2hM6fP0ly4+vVnOeavxJW0wYtcA==} + + '@rivet-dev/agent-os-python@0.1.0': + resolution: {integrity: sha512-1tH1beMf1ceSpicQKwN/a6h+NmJrmfuT4GStiRDZmvN/UWfZhkxuy7HR5VPTQpE/feUZJ01FdtBS3Em/Qoxb2Q==} + peerDependencies: + pyodide: '>=0.28.0' + peerDependenciesMeta: + pyodide: + optional: true + + '@rivet-dev/agentos-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-7jqJEu+tUhi/M6qrAKhUaPaP5ZD9jv1V2/tEoaNrKg2j4duekWUpf8p1aNT8v3V7vLhMmImvmLDDLwiC+QigWw==} + + '@rivet-dev/agentos-runtime-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-7QVeEvIoP7Q7hkzeetw9Hl7qeZOjVREWf425OTyX4AwSK44FbUq7kzu8wfH5IJHJ8ha2kcOsazKxNbP6eBnYuQ==} + + '@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-L8NN5RQA/8VZB406gw9uyWUAakNAF15YMpoO2ssX+FabXavEdja77XvRYcRSq4bBPO+tEad7IOJWfBZcmEeoOw==} + engines: {node: '>=20'} + cpu: [arm64] + os: [darwin] + + '@rivet-dev/agentos-runtime-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-vUGoVLr2mQXsrN6R+jASBvc4px/D7FR51dxY/vrBXAdlZRsIqCu8t6Mu5JPt/DiySJxvEawF8pMvhRlUWzg+gA==} + engines: {node: '>=20'} + cpu: [x64] + os: [linux] + + '@rivet-dev/agentos-runtime-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-3eXrQlDQgIfcbmcM1qvVmpNdA8Bew9FZ60kiFAwNhWd4KM5Tc4NmkZ19g3sVwjD35MpbU4pLjwhrpCDTzDoe0g==} + engines: {node: '>=20'} + + '@rivet-dev/agentos-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-2vbHXKR2+Zk/eXI4/kzZqP2ts6bcyyTl4LLnJuU2U1DzIAAJC35uqZu3v5V4moTWciweXHnwiPtSzf+QMQlvrw==} + engines: {node: '>=20'} + cpu: [arm64] + os: [darwin] + + '@rivet-dev/agentos-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-EIQUdVPLIEizhQa4sRBA3maXvEUomBV23dPfSaV21VFTcVD6uF9HrM/fzwPZQ5Eklksv0+EgY8+fuA1opZropg==} + engines: {node: '>=20'} + cpu: [x64] + os: [linux] + + '@rivet-dev/agentos-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + resolution: {integrity: sha512-MCbw4zTgpjCwXP543KtHJG8N/yhe2UvmpE5UV7SjoJGwpC55Ln5Z7J6BhSfK83dShvPr0IhemZ5djWa3fpQW5g==} + engines: {node: '>=20'} + + '@rivet-dev/agentos-toolchain@0.2.15': + resolution: {integrity: sha512-DcjNMIvXijTNGolTVee+9+rf/G//4P7QK+IssTET8cmYqByDBl2/XDSsocRhnu1giFIxSJTFw5AwxtxO7hiAtw==} + hasBin: true + + '@rivet-dev/agentos@0.2.15': + resolution: {integrity: sha512-NLJjFNVD0uP/EzGfGOcDaZ+GMx595P9sps/A2aLn+xb/KPUqpc+mxmZi8VYhCj2cr20QBa6jhfA2Ohf+0r72Mg==} + engines: {node: '>=22.0.0'} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@rivetkit/bare-ts@0.6.2': + resolution: {integrity: sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@rivetkit/engine-cli-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-hb/GOOgEPKEZicdBVhnkjIalYZwIx+x0AL3YORl2nXFpMxqWaUtOCGzQB3E2IveSfnPw9SJMKO2xkBe0Z6pjxg==} + engines: {node: '>= 20.0.0'} + cpu: [arm64] + os: [darwin] + + '@rivetkit/engine-cli-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-qt128grSxT3p2nSK3uudl1ALh79lzXuQ9IbwIkqXQV8i7TFSF7N9jY98U8FvDI2360whr3qq1lvWmFTGDnJ6/A==} + engines: {node: '>= 20.0.0'} + cpu: [x64] + os: [darwin] + + '@rivetkit/engine-cli-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-J9xWBqUvy1D1IWsWpIdkujlg83GqSBdTOTC1DBGPhoQ8It8BSZlRGYvrFLSAFPYqgb1vMv6eFPtipPLPTXecUg==} + engines: {node: '>= 20.0.0'} + cpu: [arm64] + os: [linux] + + '@rivetkit/engine-cli-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-ImXsYTJnmxwrlOa0DSKVy4+2Dp39a2/FVUzi9jgjz4KWVfR/QafGc/gVntZDIkVSvfTUkG/WW6pp8qS7qrz1oA==} + engines: {node: '>= 20.0.0'} + cpu: [x64] + os: [linux] + + '@rivetkit/engine-cli@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-EZE/e48WufHAJFm/0qKzTXY5LKNH0+YAlD26haDhwyucW2OgomdVSdmP2i+1a1wTwqSdtRsQRN7O4DmWSbDRTg==} + engines: {node: '>= 20.0.0'} + + '@rivetkit/engine-envoy-protocol@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-ZEzFU2dtHD+IzM57NlmKH5YXl6sHJTPLYBWYwgfGH1fJsYRdk8zY6yCSLPkzsCcKC8418XIY01mgaymd0vRDPA==} + + '@rivetkit/framework-base@2.3.9': + resolution: {integrity: sha512-ZSxrclYcpmdGsLMiVE2dfWNfUB6diSx+t8k4EQgL4cN02ThzJD3BM5mhU+zQVCCwfNw42eRZVIoxydYDLl/yHw==} + + '@rivetkit/on-change@6.0.1': + resolution: {integrity: sha512-QBN/KRBXLJdCgN4gBTL3XAc/zKm58atSnieXWMOyFSPmo6F1/yIVV/LTRdvAktfCttrGx7W6c32i/lwqCHWnsQ==} + engines: {node: '>=20'} + + '@rivetkit/react@2.3.9': + resolution: {integrity: sha512-j9t82h/yIqqSt17coQZeRu3F9Q5w2FgWPDUC9fCyQUJp9PTjO7ea494PR0lSWvuEiwMRqE5JpgbHdYUQ1woE9w==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + + '@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-XHnxDGrdzZGfNdo7kLBUcXua84bqJ/6v/GhZF1s4sajFO1hkWzBGrAq2R3+t8g3tUuimoLxMGfCtO1ctXUBOlg==} + engines: {node: '>= 20.0.0'} + cpu: [arm64] + os: [darwin] + + '@rivetkit/rivetkit-napi-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-ni79/MJRNR/jO/blbvjW+lPhHGFtp6otDSoWmxW/QkeUvFF6wvwBTeNcvR7Bx+GohgU9JC7Y3BOXJGuYrCRbAA==} + engines: {node: '>= 20.0.0'} + cpu: [x64] + os: [darwin] + + '@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-uUHF7vF5N8pegVtluIzix3xYBTik25cL9izjMhwAzoL2/+2UvQaqMg47hqgO1hwr17ImY7UV/WcwOyNODzEJHg==} + engines: {node: '>= 20.0.0'} + cpu: [arm64] + os: [linux] + + '@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-7/BCrybtr1Tl3GJ1JXyG9MxxsCmfREgpVYDm0H4MZdgu8Lckc7V2CQWWwuXK1bgSewCPcD1QLsVMVj6qhBdxyQ==} + engines: {node: '>= 20.0.0'} + cpu: [arm64] + os: [linux] + + '@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-SjoGcPPM6L4LFEmc9JvdqCJWMUTBMtkaZDquIiu6kBU2bB7V///fqRtMTX06oASdZoU4P7CTSyEGg1DPS2RACg==} + engines: {node: '>= 20.0.0'} + cpu: [x64] + os: [linux] + + '@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-yfVy21Fcw6heDNBbI6vICAm4aFQUbs2jSMUexxEXFh5huHjVCHeTLDh9d6TRBB82iDm94i8URU7q/UYYJRzqBQ==} + engines: {node: '>= 20.0.0'} + cpu: [x64] + os: [linux] + + '@rivetkit/rivetkit-napi@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-JNsbSqJNqNBT4Meh/CaHA5kJjKokAGfDC0k1E4N2faysbt1zn6LYYQoM12irwj/6++Wr5RpNgOiiDT8npB9d2g==} + engines: {node: '>= 20.0.0'} + + '@rivetkit/rivetkit-wasm@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-sajvCywErQwBxeIKDbNKp83hjlfUAe8JsFjGz17AiBV+Cgobc6DZOZBLrnItrccJjBtL3Iq0XdOYd60JAF/LkA==} + + '@rivetkit/traces@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-TRffcxSyfJE6mooduSxVfnY0JP26ealeLcTeB5MnkqhbbiTY3UV2YG4eJ/mbUKmNWTYQCAtddHGwN42geLzIgQ==} + engines: {node: '>=18.0.0'} + + '@rivetkit/virtual-websocket@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-FZBDcOvdg/ywND8KT/lHidfvWsKu1/7RFm1Blc0ULOpp3+788H2NX4zyHvOuF71L8QWfjt9fz1EUm+H87zbhxA==} + + '@rivetkit/workflow-engine@0.0.0-feat-workflows-public-host-apis.0ff6164': + resolution: {integrity: sha512-yjOCVmv67cq66vLeIndsx+jlKo1ct+Tlye1E65O2Xzbc7OcAKVAw/SLOKeBsB4dBMVwTe5Fau4ueuS4xVT3axA==} + engines: {node: '>=18.0.0'} + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@secure-exec/core@0.2.1': + resolution: {integrity: sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg==} + + '@secure-exec/nodejs@0.2.1': + resolution: {integrity: sha512-UJMJqVFxexlHJV0Q9nWURvrz6GElj8673DDOOFln6FHR6JS+9SaSU3eISrN158DuNC3SFi4rgjb/scKnK4YOYQ==} + + '@secure-exec/v8-darwin-arm64@0.2.1': + resolution: {integrity: sha512-gEWhMHzUpLwzuBNAD0lVkZXE8wFlWMLp4IOZ+56FYwOW/C+m07cYxuW4TjHyPqZ+vPm3IkoaMqqH5yT9VhjX/Q==} + cpu: [arm64] + os: [darwin] + + '@secure-exec/v8-darwin-x64@0.2.1': + resolution: {integrity: sha512-H2Z5K+Cq+fn/kxjGvhJzepnNFWG6qNdyhZybVWGr5bAAZoSz/Qkad4WnXcurWU+880tKDtnf19LHBXrg7zewNQ==} + cpu: [x64] + os: [darwin] + + '@secure-exec/v8-linux-arm64-gnu@0.2.1': + resolution: {integrity: sha512-14subGhVV/gW35mYYm7Gv1Keeex7PxIgQfoKji/JH7wYyDuarP6kgaES0nJw+JXVkxEVud52c+kbcIjIggqCEw==} + cpu: [arm64] + os: [linux] + + '@secure-exec/v8-linux-x64-gnu@0.2.1': + resolution: {integrity: sha512-Az4s+vUf+78vWtsC7rTn/jQc6WKJafAdt2YpEjB4Gnu+sX+FFTIst1hRV4gJonbRyJdy6SW+OQ6DZatmwczorQ==} + cpu: [x64] + os: [linux] + + '@secure-exec/v8@0.2.1': + resolution: {integrity: sha512-ye/seCqzvyMGnvyP+AO7RkVMR/lE3x9m0D2PfmiAXA457R78ZmOFmZ6v+JlJG2vv3LM30KsSXTUhwpG+Teh0hw==} + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} + + '@smithy/core@3.33.2': + resolution: {integrity: sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.7.2': + resolution: {integrity: sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.11.2': + resolution: {integrity: sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.7.2': + resolution: {integrity: sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.17.2': + resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + 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 + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/retry@0.12.2': + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + + '@xterm/headless@6.0.0': + resolution: {integrity: sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ai@7.0.68: + resolution: {integrity: sha512-9QuZOT77wzoxxUC0NcueXhCo3HUHA/1pApIJ9VRyE+9/K+3Innkq4kVhrd9aEnwIviJz2Nga063m+UTsPSdOyw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + asn1.js@4.10.1: + resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} + + assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bn.js@4.12.5: + resolution: {integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==} + + bn.js@5.2.5: + resolution: {integrity: sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + browser-resolve@2.0.0: + resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} + + browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + + browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + + browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + + browserify-rsa@4.1.1: + resolution: {integrity: sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==} + engines: {node: '>= 0.10'} + + browserify-sign@4.2.6: + resolution: {integrity: sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==} + engines: {node: '>= 0.10'} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.5: + resolution: {integrity: sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + + constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + croner@10.0.1: + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} + engines: {node: '>=18.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-browserify@3.12.1: + resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} + engines: {node: '>= 0.10'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + + domain-browser@4.22.0: + resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==} + engines: {node: '>=10'} + + drizzle-orm@0.44.7: + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + elliptic@6.6.1: + resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + esbuild-wasm@0.27.4: + resolution: {integrity: sha512-3xhVMcJ8Odvb1QjlWnjBGSYVYESsi3/oJYwLyVvbHOb2CiV4mFtD6x8Lk6JFnRxwEE3fUeVuJLbIxyVQWa867g==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.6.2: + resolution: {integrity: sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdb-tuple@1.0.0: + resolution: {integrity: sha512-8jSvKPCYCgTpi9Pt87qlfTk6griyMx4Gk3Xv31Dp72Qp8b6XgIyFsMm8KzPmFJ9iJ8K4pGvRxvOS8D0XGnrkjw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-port@7.2.0: + resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} + engines: {node: '>=16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@144.0.0: + resolution: {integrity: sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw==} + engines: {node: '>=14.0.0'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-base@3.0.5: + resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==} + engines: {node: '>= 0.10'} + + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + hono@4.13.3: + resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==} + engines: {node: '>=16.9.0'} + + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@10.5.0: + resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + + is-network-error@1.3.2: + resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} + engines: {node: '>=16'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isolated-vm@6.2.0: + resolution: {integrity: sha512-UuSlxSHWt2QuJ5WvBhzlIJx2VVZN/a44SqBbEZFKNdvuSyhOvhmyDo8SQ+njVbhnh/njoL/aW0bUTiFYlpweGQ==} + engines: {node: '>=22.0.0'} + + isomorphic-timers-promises@1.0.1: + resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} + engines: {node: '>=10'} + + jose@6.2.9: + resolution: {integrity: sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + koffi@2.16.3: + resolution: {integrity: sha512-E9y1AsgYGlaxMhcZzHr8y96QF2U5XzA12GGVAfbWqIubTwPNMXQarfBzePNXHe0xtIEtNd6ifAv3GAKYGUeBAQ==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-stdlib-browser@1.3.1: + resolution: {integrity: sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==} + engines: {node: '>=10'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + openapi3-ts@4.6.1: + resolution: {integrity: sha512-XW9MOldkhoICNeXVzzmXzmOW5G73ppOEGmh7fLCqHjgfdEYCGGN+00MlVCeUZgovjjfC56j9tvtDt1zGabNjjA==} + + os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-retry@6.2.1: + resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} + engines: {node: '>=16.17'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse-asn1@5.1.9: + resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} + engines: {node: '>= 0.10'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + pbkdf2@3.1.6: + resolution: {integrity: sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==} + engines: {node: '>= 0.10'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-dir@5.0.0: + resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} + engines: {node: '>=10'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + + rivetkit@0.0.0-feat-workflows-public-host-apis.0ff6164: + resolution: {integrity: sha512-oFSxfTtFdvOiW4ufuGr3liEzy3gXtx08VIqL6LpcbsLdKmg4ivfr48GUGTRpgt+PcmqtmJ/2L90ucHxRK/r83Q==} + engines: {node: '>=22.0.0'} + peerDependencies: + drizzle-kit: ^0.31.2 + eventsource: ^4.0.0 + ws: ^8.0.0 + peerDependenciesMeta: + drizzle-kit: + optional: true + eventsource: + optional: true + ws: + optional: true + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + secure-exec@0.2.1: + resolution: {integrity: sha512-oaQDzTPDSCOckYC8G0PimIqzEVxY6sYEvcx0fMGsRR/Wl4wkFVHaZgQ3kc2DHWysV6WHWt5g1AXc/6seafO2XQ==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + + stream-http@3.2.0: + resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@3.2.0: + resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==} + + timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + tty-browserify@0.0.1: + resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@12.0.1: + resolution: {integrity: sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vbare@0.0.4: + resolution: {integrity: sha512-QsxSVw76NqYUWYPVcQmOnQPX8buIVjgn+yqldTHlWISulBTB9TJ9rnzZceDu+GZmycOtzsmuPbPN1YNxvK12fg==} + engines: {node: '>=18.0.0'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-streams-polyfill@4.3.0: + resolution: {integrity: sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@agentclientprotocol/sdk@0.16.1(zod@4.4.3)': + dependencies: + zod: 4.4.3 + + '@agentos-software/claude-code@0.2.7': + dependencies: + '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) + '@anthropic-ai/claude-agent-sdk': 0.2.87(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + + '@agentos-software/codex-cli@0.3.4': {} + + '@agentos-software/common@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + dependencies: + '@agentos-software/coreutils': 0.3.4 + '@agentos-software/diffutils': 0.3.4 + '@agentos-software/findutils': 0.3.4 + '@agentos-software/gawk': 0.3.4 + '@agentos-software/grep': 0.3.4 + '@agentos-software/gzip': 0.3.4 + '@agentos-software/sed': 0.3.4 + '@agentos-software/tar': 0.3.5 + + '@agentos-software/common@0.2.15': + dependencies: + '@agentos-software/coreutils': 0.3.4 + '@agentos-software/diffutils': 0.3.4 + '@agentos-software/findutils': 0.3.4 + '@agentos-software/gawk': 0.3.4 + '@agentos-software/grep': 0.3.4 + '@agentos-software/gzip': 0.3.4 + '@agentos-software/sed': 0.3.4 + '@agentos-software/tar': 0.3.5 + + '@agentos-software/coreutils@0.3.4': {} + + '@agentos-software/diffutils@0.3.4': {} + + '@agentos-software/findutils@0.3.4': {} + + '@agentos-software/gawk@0.3.4': {} + + '@agentos-software/grep@0.3.4': {} + + '@agentos-software/gzip@0.3.4': {} + + '@agentos-software/manifest@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': {} + + '@agentos-software/opencode@0.2.7': {} + + '@agentos-software/pi@0.2.7(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@agentos-software/sed@0.3.4': {} + + '@agentos-software/sh@0.2.15': {} + + '@agentos-software/tar@0.3.5': {} + + '@ai-sdk/anthropic@4.0.39(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.27(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/gateway@4.0.54(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.27(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@5.0.27(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.7 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.1 + undici: 7.29.0 + zod: 4.4.3 + + '@ai-sdk/provider@4.0.7': + dependencies: + json-schema: 0.4.0 + + '@anthropic-ai/claude-agent-sdk@0.2.87(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.74.0(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3) + zod: 4.4.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + + '@anthropic-ai/sdk@0.73.0(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + + '@anthropic-ai/sdk@0.74.0(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + + '@asteasolutions/zod-to-openapi@9.1.0(zod@4.4.3)': + dependencies: + openapi3-ts: 4.6.1 + zod: 4.4.3 + + '@aws-sdk/checksums@3.1000.28': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1113.0': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/credential-provider-node': 3.972.80 + '@aws-sdk/eventstream-handler-node': 3.972.33 + '@aws-sdk/middleware-eventstream': 3.972.28 + '@aws-sdk/middleware-websocket': 3.972.51 + '@aws-sdk/token-providers': 3.1113.0 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1113.0': + dependencies: + '@aws-sdk/checksums': 3.1000.28 + '@aws-sdk/core': 3.977.8 + '@aws-sdk/credential-provider-node': 3.972.80 + '@aws-sdk/middleware-sdk-s3': 3.972.74 + '@aws-sdk/signature-v4-multi-region': 3.996.45 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/signature-v4': 5.7.2 + '@smithy/types': 4.17.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.71': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.33': + dependencies: + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.28': + dependencies: + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.74': + 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.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.51': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/signature-v4': 5.7.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.45': + dependencies: + '@aws-sdk/types': 3.974.4 + '@smithy/signature-v4': 5.7.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@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.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1113.0': + dependencies: + '@aws-sdk/core': 3.977.8 + '@aws-sdk/nested-clients': 3.997.43 + '@aws-sdk/types': 3.974.4 + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.4': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.39': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@babel/runtime@7.29.7': {} + + '@biomejs/biome@2.4.10': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.10 + '@biomejs/cli-darwin-x64': 2.4.10 + '@biomejs/cli-linux-arm64': 2.4.10 + '@biomejs/cli-linux-arm64-musl': 2.4.10 + '@biomejs/cli-linux-x64': 2.4.10 + '@biomejs/cli-linux-x64-musl': 2.4.10 + '@biomejs/cli-win32-arm64': 2.4.10 + '@biomejs/cli-win32-x64': 2.4.10 + + '@biomejs/cli-darwin-arm64@2.4.10': + optional: true + + '@biomejs/cli-darwin-x64@2.4.10': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.10': + optional: true + + '@biomejs/cli-linux-arm64@2.4.10': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.10': + optional: true + + '@biomejs/cli-linux-x64@2.4.10': + optional: true + + '@biomejs/cli-win32-arm64@2.4.10': + optional: true + + '@biomejs/cli-win32-x64@2.4.10': + optional: true + + '@borewit/text-codec@0.2.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))': + dependencies: + google-auth-library: 10.9.1 + p-retry: 4.6.2 + protobufjs: 7.6.5 + ws: 8.21.3 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@hono/node-server@2.1.1(hono@4.13.3)': + dependencies: + hono: 4.13.3 + + '@hono/zod-openapi@1.6.0(hono@4.13.3)(zod@4.4.3)': + dependencies: + '@asteasolutions/zod-to-openapi': 9.1.0(zod@4.4.3) + '@hono/zod-validator': 0.9.0(hono@4.13.3)(zod@4.4.3) + hono: 4.13.3 + openapi3-ts: 4.6.1 + zod: 4.4.3 + + '@hono/zod-validator@0.9.0(hono@4.13.3)(zod@4.4.3)': + dependencies: + hono: 4.13.3 + zod: 4.4.3 + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + + '@mariozechner/jiti@2.6.5': + dependencies: + std-env: 3.10.0 + yoctocolors: 2.2.0 + + '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.73.0(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1113.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)) + '@mistralai/mistralai': 1.14.1 + '@sinclair/typebox': 0.34.52 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + chalk: 5.6.2 + openai: 6.26.0(ws@8.21.3)(zod@4.4.3) + partial-json: 0.1.7 + proxy-agent: 6.5.0 + undici: 7.29.0 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)': + dependencies: + '@mariozechner/jiti': 2.6.5 + '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@mariozechner/pi-tui': 0.60.0 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cli-highlight: 2.1.11 + diff: 8.0.4 + extract-zip: 2.0.1 + file-type: 21.3.4 + glob: 13.0.6 + hosted-git-info: 9.0.3 + ignore: 7.0.6 + marked: 15.0.12 + minimatch: 10.2.6 + proper-lockfile: 4.1.2 + strip-ansi: 7.2.0 + undici: 7.29.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-tui@0.60.0': + dependencies: + '@types/mime-types': 2.1.4 + chalk: 5.6.2 + get-east-asian-width: 1.6.0 + marked: 15.0.12 + mime-types: 3.0.2 + optionalDependencies: + koffi: 2.16.3 + + '@mistralai/mistralai@1.14.1': + dependencies: + ws: 8.21.3 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 2.1.1(hono@4.13.3) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + express: 5.2.1 + express-rate-limit: 8.6.2(express@5.2.1) + hono: 4.13.3 + jose: 6.2.9 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@napi-rs/cli@2.18.4': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@pinojs/redact@0.4.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@rivet-dev/agent-os-core@0.1.1': + dependencies: + '@rivet-dev/agent-os-posix': 0.1.0 + '@rivet-dev/agent-os-python': 0.1.0 + '@secure-exec/core': 0.2.1 + '@secure-exec/nodejs': 0.2.1 + '@secure-exec/v8': 0.2.1 + croner: 10.0.1 + long-timeout: 0.1.1 + secure-exec: 0.2.1 + transitivePeerDependencies: + - pyodide + + '@rivet-dev/agent-os-posix@0.1.0': + dependencies: + '@secure-exec/core': 0.2.1 + + '@rivet-dev/agent-os-python@0.1.0': + dependencies: + '@secure-exec/core': 0.2.1 + + '@rivet-dev/agentos-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)': + dependencies: + '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) + '@agentos-software/claude-code': 0.2.7 + '@agentos-software/codex-cli': 0.3.4 + '@agentos-software/common': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@agentos-software/manifest': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@agentos-software/opencode': 0.2.7 + '@agentos-software/pi': 0.2.7(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3) + '@aws-sdk/client-s3': 3.1113.0 + '@rivet-dev/agentos-runtime-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@rivet-dev/agentos-sidecar': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@rivetkit/bare-ts': 0.6.2 + '@xterm/headless': 6.0.0 + better-sqlite3: 12.11.1 + croner: 10.0.1 + googleapis: 144.0.0 + isolated-vm: 6.2.0 + long-timeout: 0.1.1 + minimatch: 10.2.6 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@modelcontextprotocol/sdk' + - bufferutil + - encoding + - supports-color + - utf-8-validate + - ws + + '@rivet-dev/agentos-runtime-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + dependencies: + '@rivet-dev/agentos-runtime-sidecar': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@rivetkit/bare-ts': 0.6.2 + zod: 4.4.3 + + '@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + optional: true + + '@rivet-dev/agentos-runtime-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + optional: true + + '@rivet-dev/agentos-runtime-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + optionalDependencies: + '@rivet-dev/agentos-runtime-sidecar-darwin-arm64': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@rivet-dev/agentos-runtime-sidecar-linux-x64-gnu': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + + '@rivet-dev/agentos-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + optional: true + + '@rivet-dev/agentos-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + optional: true + + '@rivet-dev/agentos-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': + optionalDependencies: + '@rivet-dev/agentos-sidecar-darwin-arm64': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + '@rivet-dev/agentos-sidecar-linux-x64-gnu': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + + '@rivet-dev/agentos-toolchain@0.2.15': + dependencies: + '@rivetkit/bare-ts': 0.6.2 + + '@rivet-dev/agentos@0.2.15(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(better-sqlite3@12.11.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(ws@8.21.3)': + dependencies: + '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3) + '@agentos-software/common': 0.2.15 + '@rivet-dev/agentos-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3) + '@rivetkit/react': 2.3.9(better-sqlite3@12.11.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(ws@8.21.3) + rivetkit: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + zod: 4.4.3 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cfworker/json-schema' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@modelcontextprotocol/sdk' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bufferutil + - bun-types + - drizzle-kit + - encoding + - eventsource + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - pyodide + - sql.js + - sqlite3 + - supports-color + - utf-8-validate + - ws + + '@rivetkit/bare-ts@0.6.2': {} + + '@rivetkit/engine-cli-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/engine-cli-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/engine-cli-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/engine-cli-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/engine-cli@0.0.0-feat-workflows-public-host-apis.0ff6164': + optionalDependencies: + '@rivetkit/engine-cli-darwin-arm64': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-cli-darwin-x64': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-cli-linux-arm64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-cli-linux-x64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 + + '@rivetkit/engine-envoy-protocol@0.0.0-feat-workflows-public-host-apis.0ff6164': + dependencies: + '@rivetkit/bare-ts': 0.6.2 + + '@rivetkit/framework-base@2.3.9(better-sqlite3@12.11.1)(ws@8.21.3)': + dependencies: + '@tanstack/store': 0.7.7 + fast-deep-equal: 3.1.3 + rivetkit: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - drizzle-kit + - eventsource + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - pyodide + - sql.js + - sqlite3 + - ws + + '@rivetkit/on-change@6.0.1': {} + + '@rivetkit/react@2.3.9(better-sqlite3@12.11.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(ws@8.21.3)': + dependencies: + '@rivetkit/framework-base': 2.3.9(better-sqlite3@12.11.1)(ws@8.21.3) + '@tanstack/react-store': 0.7.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + rivetkit: 0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3) + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - drizzle-kit + - eventsource + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - pyodide + - sql.js + - sqlite3 + - ws + + '@rivetkit/rivetkit-napi-darwin-arm64@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/rivetkit-napi-darwin-x64@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/rivetkit-napi-linux-arm64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/rivetkit-napi-linux-arm64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/rivetkit-napi-linux-x64-gnu@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/rivetkit-napi-linux-x64-musl@0.0.0-feat-workflows-public-host-apis.0ff6164': + optional: true + + '@rivetkit/rivetkit-napi@0.0.0-feat-workflows-public-host-apis.0ff6164': + dependencies: + '@napi-rs/cli': 2.18.4 + '@rivetkit/engine-envoy-protocol': 0.0.0-feat-workflows-public-host-apis.0ff6164 + optionalDependencies: + '@rivetkit/rivetkit-napi-darwin-arm64': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi-darwin-x64': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi-linux-arm64-gnu': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi-linux-arm64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi-linux-x64-gnu': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-napi-linux-x64-musl': 0.0.0-feat-workflows-public-host-apis.0ff6164 + + '@rivetkit/rivetkit-wasm@0.0.0-feat-workflows-public-host-apis.0ff6164': {} + + '@rivetkit/traces@0.0.0-feat-workflows-public-host-apis.0ff6164': + dependencies: + '@rivetkit/bare-ts': 0.6.2 + cbor-x: 1.6.5 + fdb-tuple: 1.0.0 + vbare: 0.0.4 + + '@rivetkit/virtual-websocket@0.0.0-feat-workflows-public-host-apis.0ff6164': {} + + '@rivetkit/workflow-engine@0.0.0-feat-workflows-public-host-apis.0ff6164': + dependencies: + '@rivetkit/bare-ts': 0.6.2 + cbor-x: 1.6.5 + fdb-tuple: 1.0.0 + pino: 9.14.0 + vbare: 0.0.4 + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@secure-exec/core@0.2.1': + dependencies: + better-sqlite3: 12.11.1 + + '@secure-exec/nodejs@0.2.1': + dependencies: + '@secure-exec/core': 0.2.1 + '@secure-exec/v8': 0.2.1 + cbor-x: 1.6.5 + cjs-module-lexer: 2.2.1 + es-module-lexer: 1.7.0 + esbuild: 0.27.7 + node-stdlib-browser: 1.3.1 + web-streams-polyfill: 4.3.0 + + '@secure-exec/v8-darwin-arm64@0.2.1': + optional: true + + '@secure-exec/v8-darwin-x64@0.2.1': + optional: true + + '@secure-exec/v8-linux-arm64-gnu@0.2.1': + optional: true + + '@secure-exec/v8-linux-x64-gnu@0.2.1': + optional: true + + '@secure-exec/v8@0.2.1': + dependencies: + cbor-x: 1.6.5 + optionalDependencies: + '@secure-exec/v8-darwin-arm64': 0.2.1 + '@secure-exec/v8-darwin-x64': 0.2.1 + '@secure-exec/v8-linux-arm64-gnu': 0.2.1 + '@secure-exec/v8-linux-x64-gnu': 0.2.1 + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@sinclair/typebox@0.34.52': {} + + '@smithy/core@3.33.2': + dependencies: + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.5.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.7.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.11.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.7.2': + dependencies: + '@smithy/core': 3.33.2 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + + '@smithy/types@4.17.2': + dependencies: + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@tanstack/react-store@0.7.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/store': 0.7.7 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + + '@tanstack/store@0.7.7': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/estree@1.0.9': {} + + '@types/mime-types@2.1.4': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/retry@0.12.0': {} + + '@types/retry@0.12.2': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.20.1 + optional: true + + '@vercel/oidc@3.2.0': {} + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.20.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@workflow/serde@4.1.0': {} + + '@xterm/headless@6.0.0': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@8.18.0: {} + + agent-base@7.1.4: {} + + ai@7.0.68(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 4.0.54(zod@4.4.3) + '@ai-sdk/provider': 4.0.7 + '@ai-sdk/provider-utils': 5.0.27(zod@4.4.3) + zod: 4.4.3 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + asn1.js@4.10.1: + dependencies: + bn.js: 4.12.5 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + assert@2.1.0: + dependencies: + call-bind: 1.0.9 + is-nan: 1.3.2 + object-is: 1.1.6 + object.assign: 4.1.7 + util: 0.12.5 + + assertion-error@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + atomic-sleep@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + basic-ftp@5.3.1: {} + + better-sqlite3@12.11.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bn.js@4.12.5: {} + + bn.js@5.2.5: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bowser@2.14.1: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + brorand@1.1.0: {} + + browser-resolve@2.0.0: + dependencies: + resolve: 1.22.12 + + browserify-aes@1.2.0: + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.7 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + browserify-cipher@1.0.1: + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + + browserify-des@1.0.2: + dependencies: + cipher-base: 1.0.7 + des.js: 1.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + browserify-rsa@4.1.1: + dependencies: + bn.js: 5.2.5 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + browserify-sign@4.2.6: + dependencies: + bn.js: 5.2.5 + browserify-rsa: 4.1.1 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.6.1 + inherits: 2.0.4 + parse-asn1: 5.1.9 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-xor@1.0.3: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-status-codes@3.0.0: {} + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.5: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + cjs-module-lexer@2.2.1: {} + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.2 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + console-browserify@1.2.0: {} + + constants-browserify@1.0.0: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-ecdh@4.0.4: + dependencies: + bn.js: 4.12.5 + elliptic: 6.6.1 + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.7 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + + create-require@1.1.1: {} + + croner@10.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-browserify@3.12.1: + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.6 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + hash-base: 3.0.5 + inherits: 2.0.4 + pbkdf2: 3.1.6 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + depd@2.0.0: {} + + des.js@1.1.0: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + diffie-hellman@5.0.3: + dependencies: + bn.js: 4.12.5 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + + domain-browser@4.22.0: {} + + drizzle-orm@0.44.7(better-sqlite3@12.11.1): + optionalDependencies: + better-sqlite3: 12.11.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + elliptic@6.6.1: + dependencies: + bn.js: 4.12.5 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + esbuild-wasm@0.27.4: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + 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 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + eventsource-parser@3.1.1: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.1 + + evp_bytestokey@1.0.3: + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + + expand-template@2.0.3: {} + + expect-type@1.4.0: {} + + express-rate-limit@8.6.2(express@5.2.1): + dependencies: + debug: 4.4.3 + express: 5.2.1 + ip-address: 10.5.0 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.5: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdb-tuple@1.0.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + file-uri-to-path@1.0.0: {} + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.62.4 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gaxios@7.3.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.1 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + generator-function@2.0.1: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-port@7.2.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + github-from-package@0.0.0: {} + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + google-logging-utils@1.1.3: {} + + googleapis-common@7.2.0: + dependencies: + extend: 3.0.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + qs: 6.15.3 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + googleapis@144.0.0: + dependencies: + google-auth-library: 9.15.1 + googleapis-common: 7.2.0 + transitivePeerDependencies: + - encoding + - supports-color + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-base@3.0.5: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + highlight.js@10.7.3: {} + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + hono@4.13.3: {} + + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-browserify@1.0.0: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@7.0.6: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@10.5.0: {} + + ipaddr.js@1.9.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-nan@1.3.2: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + is-network-error@1.3.2: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isolated-vm@6.2.0: + dependencies: + node-gyp-build: 4.8.4 + + isomorphic-timers-promises@1.0.1: {} + + jose@6.2.9: {} + + joycon@3.1.1: {} + + js-tokens@4.0.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-schema@0.4.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + koffi@2.16.3: + optional: true + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + long-timeout@0.1.1: {} + + long@5.3.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@11.5.2: {} + + lru-cache@7.18.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@15.0.12: {} + + math-intrinsics@1.1.0: {} + + md5.js@1.3.5: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + miller-rabin@4.0.1: + dependencies: + bn.js: 4.12.5 + brorand: 1.1.0 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-response@3.1.0: {} + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.18: {} + + napi-build-utils@2.0.0: {} + + negotiator@1.0.0: {} + + netmask@2.1.1: {} + + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-gyp-build@4.8.4: {} + + node-stdlib-browser@1.3.1: + dependencies: + assert: 2.1.0 + browser-resolve: 2.0.0 + browserify-zlib: 0.2.0 + buffer: 5.7.1 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + create-require: 1.1.1 + crypto-browserify: 3.12.1 + domain-browser: 4.22.0 + events: 3.3.0 + https-browserify: 1.0.0 + isomorphic-timers-promises: 1.0.1 + os-browserify: 0.3.0 + path-browserify: 1.0.1 + pkg-dir: 5.0.0 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 3.6.2 + stream-browserify: 3.0.0 + stream-http: 3.2.0 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.1 + url: 0.11.4 + util: 0.12.5 + vm-browserify: 1.1.2 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@6.26.0(ws@8.21.3)(zod@4.4.3): + optionalDependencies: + ws: 8.21.3 + zod: 4.4.3 + + openapi3-ts@4.6.1: + dependencies: + yaml: 2.9.0 + + os-browserify@0.3.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-retry@6.2.1: + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.3.2 + retry: 0.13.1 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + + pako@1.0.11: {} + + parse-asn1@5.1.9: + dependencies: + asn1.js: 4.10.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.6 + safe-buffer: 5.2.1 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + parseurl@1.3.3: {} + + partial-json@0.1.7: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + pbkdf2@3.1.6: + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + to-buffer: 1.2.2 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.2.0 + + pirates@4.0.7: {} + + pkce-challenge@5.0.1: {} + + pkg-dir@5.0.0: + dependencies: + find-up: 5.0.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@6.0.1(postcss@8.5.26)(tsx@4.23.12)(yaml@2.9.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.26 + tsx: 4.23.12 + yaml: 2.9.0 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.94.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + process-nextick-args@2.0.1: {} + + process-warning@5.1.0: {} + + process@0.11.10: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.20.1 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + public-encrypt@4.0.3: + dependencies: + bn.js: 4.12.5 + browserify-rsa: 4.1.1 + create-hash: 1.2.0 + parse-asn1: 5.1.9 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@1.4.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + querystring-es3@0.2.1: {} + + quick-format-unescaped@4.0.4: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + randomfill@1.0.4: + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react@19.2.8: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + real-require@0.2.0: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry@0.12.0: {} + + retry@0.13.1: {} + + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + + rivetkit@0.0.0-feat-workflows-public-host-apis.0ff6164(better-sqlite3@12.11.1)(ws@8.21.3): + dependencies: + '@hono/zod-openapi': 1.6.0(hono@4.13.3)(zod@4.4.3) + '@rivet-dev/agent-os-core': 0.1.1 + '@rivetkit/bare-ts': 0.6.2 + '@rivetkit/engine-cli': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/engine-envoy-protocol': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/on-change': 6.0.1 + '@rivetkit/rivetkit-napi': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-wasm': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/traces': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/virtual-websocket': 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/workflow-engine': 0.0.0-feat-workflows-public-host-apis.0ff6164 + cbor-x: 1.6.5 + drizzle-orm: 0.44.7(better-sqlite3@12.11.1) + hono: 4.13.3 + invariant: 2.2.4 + p-retry: 6.2.1 + pino: 9.14.0 + uuid: 12.0.1 + vbare: 0.0.4 + zod: 4.4.3 + optionalDependencies: + ws: 8.21.3 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - pyodide + - sql.js + - sqlite3 + + 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.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + secure-exec@0.2.1: + dependencies: + '@secure-exec/core': 0.2.1 + '@secure-exec/nodejs': 0.2.1 + + semver@7.8.5: {} + + 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.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.5.0 + smart-buffer: 4.2.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + source-map@0.7.6: {} + + split2@4.2.0: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + stream-http@3.2.0: + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + xtend: 4.0.2 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-json-comments@2.0.1: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@3.2.0: + dependencies: + real-require: 0.2.0 + + timers-browserify@2.0.12: + dependencies: + setimmediate: 1.0.5 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + ts-algebra@2.0.0: {} + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + tsup@8.5.1(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.26)(tsx@4.23.12)(yaml@2.9.0) + resolve-from: 5.0.0 + rollup: 4.62.4 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.26 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + tty-browserify@0.0.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + undici@7.29.0: {} + + unpipe@1.0.0: {} + + url-template@2.0.8: {} + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.15.3 + + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.22 + + uuid@12.0.1: {} + + uuid@9.0.1: {} + + vary@1.1.2: {} + + vbare@0.0.4: {} + + vite-node@2.1.9(@types/node@22.20.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.20.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.20.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.26 + rollup: 4.62.4 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.20.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.20.1) + vite-node: 2.1.9(@types/node@22.20.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vm-browserify@1.1.2: {} + + web-streams-polyfill@3.3.3: {} + + web-streams-polyfill@4.3.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.21.3: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yaml@2.9.0: {} + + yargs-parser@20.2.9: {} + + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + yoctocolors@2.2.0: {} + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..9b76a9c43 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,20 @@ +packages: + - packages/* + - examples/apps-* + - benchmarks/dynamic-apps + - tests/e2e/dynamic-apps + +onlyBuiltDependencies: + - '@biomejs/biome' + - better-sqlite3 + - cbor-extract + - esbuild + +overrides: + '@rivet-dev/agentos-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c + rivetkit: 0.0.0-feat-workflows-public-host-apis.0ff6164 + '@rivetkit/rivetkit-wasm': 0.0.0-feat-workflows-public-host-apis.0ff6164 + +catalogs: + rivetkit: + rivetkit: 0.0.0-feat-workflows-public-host-apis.0ff6164 diff --git a/scripts/check-boundaries.mjs b/scripts/check-boundaries.mjs new file mode 100644 index 000000000..10885ebca --- /dev/null +++ b/scripts/check-boundaries.mjs @@ -0,0 +1,94 @@ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; + +const root = new URL("../", import.meta.url); + +async function read(path) { + return readFile(new URL(path, root), "utf8"); +} + +async function walk(path) { + const directory = new URL(path, root); + const entries = await readdir(directory, { withFileTypes: true }); + return ( + await Promise.all( + entries.map(async (entry) => { + const child = join(path, entry.name); + return entry.isDirectory() ? walk(`${child}/`) : [child]; + }), + ) + ).flat(); +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const mainPackage = JSON.parse( + await read("packages/dynamic-apps/package.json"), +); +assert( + mainPackage.dependencies?.["@rivet-dev/agentos"] === "0.2.15", + "agentOS must remain a pinned implementation dependency", +); +assert( + !mainPackage.peerDependencies?.["@rivet-dev/agentos"], + "agentOS must not be a peer dependency", +); +assert( + mainPackage.dependencies?.["@rivet-dev/dynamic-apps-builder"] === + `workspace:${mainPackage.version}` || + mainPackage.dependencies?.["@rivet-dev/dynamic-apps-builder"] === + mainPackage.version, + "the builder must use the exact matching workspace version", +); + +for (const path of await walk("examples/")) { + if (!path.endsWith(".ts") && !path.endsWith("package.json")) continue; + const source = await read(path); + assert( + !source.includes('from "@rivet-dev/agentos"') && + !source.includes('"@rivet-dev/agentos":'), + `${path} exposes agentOS instead of the Dynamic Apps setup wrapper`, + ); +} + +const actors = await read("packages/dynamic-apps/src/actors.ts"); +for (const identity of [ + 'const APP_ACTOR_NAME = "agentOSAppsApp"', + 'const SCALER_ACTOR_NAME = "agentOSAppsScaler"', + 'const REPLICA_ACTOR_NAME = "agentOSAppsReplica"', + '"/opt/agentos/bin/apps-builder"', +]) { + assert( + actors.includes(identity), + `compatibility identity changed: ${identity}`, + ); +} + +const runtime = await read("packages/dynamic-apps/src/runtime.ts"); +assert( + runtime.includes('hash.update("agentos-apps-release-v15\\0")'), + "release hash domain changed", +); + +const builderManifest = JSON.parse( + await read("packages/dynamic-apps-builder/agentos-package.json"), +); +assert( + builderManifest.name === "apps-builder", + "the VM package identity must remain apps-builder", +); + +const declarations = (await walk("packages/dynamic-apps/dist/")) + .filter((path) => path.endsWith(".d.ts")) + .map((path) => read(path)); +for (const [index, source] of (await Promise.all(declarations)).entries()) { + assert( + !source.includes('from "@rivet-dev/agentos') && + !source.includes('from "@agentos-software/manifest"'), + `public declaration ${index + 1} leaks an implementation package`, + ); +} + +console.log("Dynamic Apps package boundaries are valid."); diff --git a/scripts/resolve-release.mjs b/scripts/resolve-release.mjs new file mode 100644 index 000000000..e0b7bd6aa --- /dev/null +++ b/scripts/resolve-release.mjs @@ -0,0 +1,62 @@ +import { execFile } from "node:child_process"; +import { appendFile } from "node:fs/promises"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const args = Object.fromEntries( + process.argv.slice(2).map((argument) => { + const [key, ...value] = argument.replace(/^--/, "").split("="); + return [key, value.join("=")]; + }), +); + +let version = args.version; +if (version === "legacy") { + const [{ stdout: main }, { stdout: builder }] = await Promise.all([ + execFileAsync("npm", ["view", "@rivet-dev/agentos-apps@latest", "version"]), + execFileAsync("npm", [ + "view", + "@agentos-software/apps-builder@latest", + "version", + ]), + ]); + if (main.trim() !== builder.trim()) { + throw new Error( + `legacy package versions differ: apps=${main.trim()} builder=${builder.trim()}`, + ); + } + version = main.trim(); +} + +if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { + throw new Error(`invalid release version: ${version ?? ""}`); +} + +const sanitize = (value) => + value + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^[._-]+|[._-]+$/g, "") + .slice(0, 64); + +let tag = args.tag ?? "auto"; +if (tag === "auto") { + const prerelease = version.split("-")[1]?.split(".")[0]; + tag = prerelease === "rc" || prerelease === "next" ? prerelease : "latest"; +} else if (tag === "preview") { + tag = `preview-${sanitize(args.branch ?? "branch")}`; +} +if (!/^[a-z0-9][a-z0-9._-]{0,127}$/.test(tag)) { + throw new Error(`invalid npm dist-tag: ${tag}`); +} + +const output = [ + `version=${version}`, + `npm_tag=${tag}`, + `real_release=${!tag.startsWith("preview-")}`, +]; +if (process.env.GITHUB_OUTPUT) { + await appendFile(process.env.GITHUB_OUTPUT, `${output.join("\n")}\n`); +} else { + console.log(output.join("\n")); +} diff --git a/scripts/set-release-version.mjs b/scripts/set-release-version.mjs new file mode 100644 index 000000000..3660716c3 --- /dev/null +++ b/scripts/set-release-version.mjs @@ -0,0 +1,18 @@ +import { readFile, writeFile } from "node:fs/promises"; + +const version = process.argv[2]; +if (!version) throw new Error("usage: set-release-version.mjs "); + +async function update(path, transform) { + const value = JSON.parse(await readFile(path, "utf8")); + transform(value); + await writeFile(path, `${JSON.stringify(value, null, "\t")}\n`); +} + +await update("packages/dynamic-apps-builder/package.json", (value) => { + value.version = version; +}); +await update("packages/dynamic-apps/package.json", (value) => { + value.version = version; + value.dependencies["@rivet-dev/dynamic-apps-builder"] = version; +}); diff --git a/scripts/test-packed.mjs b/scripts/test-packed.mjs new file mode 100644 index 000000000..bf736ed43 --- /dev/null +++ b/scripts/test-packed.mjs @@ -0,0 +1,144 @@ +import { execFile } from "node:child_process"; +import { + access, + mkdir, + mkdtemp, + readdir, + readFile, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const repositoryRoot = resolve(new URL("../", import.meta.url).pathname); +const packDirectory = join(repositoryRoot, ".pack"); + +await rm(packDirectory, { recursive: true, force: true }); +await mkdir(packDirectory, { recursive: true }); +for (const packagePath of [ + "packages/dynamic-apps-builder", + "packages/dynamic-apps", +]) { + await execFileAsync( + "pnpm", + ["--dir", packagePath, "pack", "--pack-destination", packDirectory], + { cwd: repositoryRoot }, + ); +} + +const tarballs = await readdir(packDirectory); +const builderTarball = join( + packDirectory, + tarballs.find((name) => name.includes("dynamic-apps-builder")) ?? "missing", +); +const mainTarball = join( + packDirectory, + tarballs.find( + (name) => name.includes("dynamic-apps-") && !name.includes("builder"), + ) ?? "missing", +); +await access(builderTarball); +await access(mainTarball); + +const fixture = await mkdtemp(join(tmpdir(), "dynamic-apps-packed-")); +await writeFile( + join(fixture, "package.json"), + JSON.stringify({ + private: true, + type: "module", + dependencies: { + "@rivet-dev/dynamic-apps": `file:${mainTarball}`, + "@rivet-dev/dynamic-apps-builder": `file:${builderTarball}`, + }, + }), +); +await execFileAsync( + "npm", + [ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--loglevel=error", + ], + { cwd: fixture }, +); + +const builderRoot = join( + fixture, + "node_modules/@rivet-dev/dynamic-apps-builder", +); +const mainRoot = join(fixture, "node_modules/@rivet-dev/dynamic-apps"); +const builder = await import(pathToFileURL(join(builderRoot, "dist/index.js"))); +if (basename(builder.default.packagePath) !== "package.aospkg") { + throw new Error("packed builder did not export package.aospkg"); +} +await access(builder.default.packagePath); +await access(join(mainRoot, "assets/inspector/deployment/index.html")); +await access(join(mainRoot, "assets/inspector/scaler/index.html")); +await access(join(mainRoot, "assets/inspector/replica/index.html")); + +const main = await import(pathToFileURL(join(mainRoot, "dist/index.js"))); +for (const name of ["setup", "setupApps", "deployApp", "appsRouter"]) { + if (!(name in main)) + throw new Error(`packed main package is missing ${name}`); +} + +for (const packageRoot of [builderRoot, mainRoot]) { + const manifest = JSON.parse( + await readFile(join(packageRoot, "package.json"), "utf8"), + ); + const serialized = JSON.stringify(manifest); + if (serialized.includes("workspace:") || serialized.includes("catalog:")) { + throw new Error( + `${manifest.name} contains an unpublished dependency specifier`, + ); + } +} + +const declaration = await readFile(join(mainRoot, "dist/index.d.ts"), "utf8"); +if (/from ["']@rivet-dev\/agentos/.test(declaration)) { + throw new Error( + "packed public declarations expose agentOS implementation types", + ); +} + +const workspace = join(fixture, "builder-smoke"); +const release = join(fixture, "builder-release"); +await mkdir(workspace, { recursive: true }); +await writeFile( + join(workspace, "entry.ts"), + 'export default "packed builder";\n', +); +await writeFile( + join(workspace, "package.json"), + JSON.stringify({ private: true, type: "module" }), +); +const configPath = join(fixture, "builder-config.json"); +await writeFile( + configPath, + JSON.stringify({ + workspace, + release, + entrypoint: "entry.ts", + version: "packed-smoke", + sourceFiles: ["entry.ts"], + usesRivetKit: false, + maxOutputBytes: 1024 * 1024, + maxOutputFiles: 16, + maxFileBytes: 512 * 1024, + }), +); +await execFileAsync(process.execPath, [ + join(builderRoot, "cli/apps-builder.mjs"), + configPath, +]); +await access(join(release, "main.mjs")); + +console.log( + `Verified ${basename(builderTarball)} and ${basename(mainTarball)}.`, +); diff --git a/tests/e2e/dynamic-apps/README.md b/tests/e2e/dynamic-apps/README.md new file mode 100644 index 000000000..c5c01c3e1 --- /dev/null +++ b/tests/e2e/dynamic-apps/README.md @@ -0,0 +1,36 @@ +# Dynamic Apps end-to-end verification + +This expensive check starts a local Rivet engine, installs the real `rivetkit` +npm dependency inside an agentOS build VM, serves the packed app, calls a guest +actor through DirectActor, replaces the execution replica, and verifies both +SQLite state recovery and failed-build rollback. + +```sh +pnpm --filter @rivet-dev/dynamic-apps-e2e test:e2e +``` + +For routing and host-runtime iteration, cache only the content-addressed +`.aospkg` build artifact: + +```sh +pnpm --filter @rivet-dev/dynamic-apps-e2e test:e2e:fast +``` + +The first fast run performs the full package build. Later runs stop after HTTP +and DirectActor checks and reuse `/tmp/agentos-apps-artifact-cache-v16`. Every +run still starts with a fresh Rivet database, uploads the artifact into actor +SQLite, materializes it in a replica, and exercises the real routing path. +Remove that cache directory to force a clean build. The normal `test:e2e` +command never reads the cache and remains the required final gate. + +To test an unpublished local RivetKit package without checking a tarball into +this repository: + +```sh +AGENTOS_APPS_RIVETKIT_TARBALL=/absolute/path/to/rivetkit.tgz \ + pnpm --filter @rivet-dev/dynamic-apps-e2e test:e2e:fast +``` + +The test uploads that tarball as `vendor/rivetkit.tgz` and installs it through +`file:./vendor/rivetkit.tgz`. The guest installs the matching +`@rivetkit/rivetkit-wasm` version directly and omits optional dependencies. diff --git a/tests/e2e/dynamic-apps/package.json b/tests/e2e/dynamic-apps/package.json new file mode 100644 index 000000000..14ec04af6 --- /dev/null +++ b/tests/e2e/dynamic-apps/package.json @@ -0,0 +1,25 @@ +{ + "name": "@rivet-dev/dynamic-apps-e2e", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "test:e2e": "node --import tsx src/run.ts", + "test:e2e:build": "node --import tsx src/run.ts --build-only", + "test:e2e:fast": "node --import tsx src/run.ts --fast", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@rivet-dev/dynamic-apps": "workspace:*", + "@rivet-dev/dynamic-apps-builder": "workspace:*", + "rivetkit": "0.0.0-feat-workflows-public-host-apis.0ff6164" + }, + "devDependencies": { + "@rivet-dev/agentos-toolchain": "0.2.15", + "@rivetkit/engine-cli": "0.0.0-feat-workflows-public-host-apis.0ff6164", + "@types/node": "^22.19.15", + "get-port": "^7.1.0", + "tsx": "^4.20.6", + "typescript": "^5.7.3" + } +} diff --git a/tests/e2e/dynamic-apps/src/run.ts b/tests/e2e/dynamic-apps/src/run.ts new file mode 100644 index 000000000..f0e2726b4 --- /dev/null +++ b/tests/e2e/dynamic-apps/src/run.ts @@ -0,0 +1,104 @@ +import { spawn } from "node:child_process"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { getEnginePath } from "@rivetkit/engine-cli"; +import getPort from "get-port"; + +const fast = process.argv.includes("--fast"); +const buildOnly = process.argv.includes("--build-only"); +const root = await mkdtemp(join(tmpdir(), "agentos-apps-e2e-")); +const databasePath = join(root, "db"); +await mkdir(databasePath, { recursive: true }); +const guardPort = await getPort(); +const peerPort = await getPort({ exclude: [guardPort] }); +const metricsPort = await getPort({ exclude: [guardPort, peerPort] }); +const endpoint = `http://127.0.0.1:${guardPort}`; +const peerEndpoint = `http://127.0.0.1:${peerPort}`; +const configPath = join(root, "engine.json"); +await writeFile( + configPath, + JSON.stringify({ + file_system: { path: databasePath }, + guard: { host: "127.0.0.1", port: guardPort }, + api_peer: { host: "127.0.0.1", port: peerPort }, + metrics: { host: "127.0.0.1", port: metricsPort }, + topology: { + datacenter_label: 1, + datacenters: { + default: { + datacenter_label: 1, + is_leader: true, + public_url: endpoint, + peer_url: peerEndpoint, + proxy_url: null, + }, + }, + }, + telemetry: { enabled: false }, + runtime: { allow_version_rollback: true }, + }), +); + +const engine = spawn(getEnginePath(), ["--config", configPath, "start"], { + stdio: ["ignore", "inherit", "inherit"], +}); + +try { + await waitUntilHealthy(endpoint, engine); + process.env.RIVET_ENDPOINT = endpoint; + process.env.AGENTOS_APPS_E2E_FAST = fast ? "1" : "0"; + process.env.AGENTOS_APPS_E2E_BUILD_ONLY = buildOnly ? "1" : "0"; + if (fast) { + process.env.AGENTOS_APPS_E2E_ARTIFACT_CACHE = join( + tmpdir(), + "agentos-apps-artifact-cache-v16", + ); + } else { + delete process.env.AGENTOS_APPS_E2E_ARTIFACT_CACHE; + } + delete process.env.RIVET_ENGINE; + delete process.env.RIVET_RUN_ENGINE; + await import("./verify.js"); +} finally { + await stopEngine(engine); + await rm(root, { recursive: true, force: true }); +} +process.exit(0); + +async function stopEngine(process: ReturnType): Promise { + if (process.exitCode !== null) return; + process.kill("SIGTERM"); + const stopped = await Promise.race([ + new Promise((resolve) => process.once("exit", () => resolve(true))), + new Promise((resolve) => + setTimeout(() => resolve(false), fast ? 1_000 : 10_000), + ), + ]); + if (stopped) return; + console.warn("Rivet Engine exceeded the E2E shutdown limit; sending SIGKILL"); + process.kill("SIGKILL"); + await new Promise((resolve) => process.once("exit", () => resolve())); +} + +async function waitUntilHealthy( + endpoint: string, + process: ReturnType, +): Promise { + const deadline = Date.now() + 30_000; + let lastError: unknown; + while (Date.now() < deadline) { + if (process.exitCode !== null) { + throw new Error(`Rivet Engine exited with code ${process.exitCode}`); + } + try { + const response = await fetch(`${endpoint}/health`); + if (response.ok) return; + lastError = new Error(`health returned ${response.status}`); + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error("Rivet Engine did not become healthy", { cause: lastError }); +} diff --git a/tests/e2e/dynamic-apps/src/verify.ts b/tests/e2e/dynamic-apps/src/verify.ts new file mode 100644 index 000000000..ecc81cded --- /dev/null +++ b/tests/e2e/dynamic-apps/src/verify.ts @@ -0,0 +1,604 @@ +import { execFile } from "node:child_process"; +import { + mkdir, + mkdtemp, + readFile, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { packAospkgFromTarBytes } from "@rivet-dev/agentos-toolchain"; +import { appsRouter, deployApp, setup } from "@rivet-dev/dynamic-apps"; +import { appsBuilderVersion } from "@rivet-dev/dynamic-apps-builder"; +import { createClient } from "rivetkit/client"; +import { runLoadTest } from "../../../../benchmarks/dynamic-apps/src/load.js"; +import { + createAppsActors, + normalizeScaling, +} from "../../../../packages/dynamic-apps/src/actors.js"; +import { provisionAppNamespace } from "../../../../packages/dynamic-apps/src/control-plane.js"; +import { + appRunnerPool, + canonicalDeploymentHash, + runnerSource, +} from "../../../../packages/dynamic-apps/src/runtime.js"; + +const execFileAsync = promisify(execFile); + +const fast = process.env.AGENTOS_APPS_E2E_FAST === "1"; +const buildOnly = process.env.AGENTOS_APPS_E2E_BUILD_ONLY === "1"; +const artifactCacheDirectory = process.env.AGENTOS_APPS_E2E_ARTIFACT_CACHE; +const appsActors = createAppsActors({ + artifactCache: artifactCacheDirectory + ? { + async get(release) { + try { + return await readFile( + join(artifactCacheDirectory, `${release}.aospkg`), + ); + } catch (error) { + if ( + typeof error === "object" && + error !== null && + "code" in error && + error.code === "ENOENT" + ) { + return undefined; + } + throw error; + } + }, + async put(release, artifact) { + await mkdir(artifactCacheDirectory, { + recursive: true, + }); + const target = join(artifactCacheDirectory, `${release}.aospkg`); + const temporary = `${target}.${process.pid}.tmp`; + await writeFile(temporary, artifact); + await rename(temporary, target); + }, + } + : undefined, +}); + +export const registry = setup({ + use: { + ...appsActors, + }, +}); + +registry.start(); + +await verify(); + +async function verify(): Promise { + try { + if (buildOnly) { + await verifyBuildScriptDiagnosticsAndRollback(); + return; + } + const localRivetKitTarball = process.env.AGENTOS_APPS_RIVETKIT_TARBALL; + const hello = await deployApp({ + appId: "agentos-apps-hello-e2e", + source: new URL( + "../../../../examples/apps-hello-world/fixtures/app/", + import.meta.url, + ), + }); + const helloResponse = await appsRouter.request("/agentos-apps-hello-e2e/"); + if ( + !helloResponse.ok || + !(await helloResponse.text()).includes("Hello from Dynamic Apps") + ) { + throw new Error( + "dependency-free hello-world example returned the wrong body", + ); + } + if (fast) { + const simple = await deployApp({ + appId: "agentos-apps-simple-e2e", + files: { + "index.html": "

Dynamic Apps packaging works

", + }, + }); + const simpleResponse = await appsRouter.request( + "/agentos-apps-simple-e2e/", + ); + if ( + !simpleResponse.ok || + !(await simpleResponse.text()).includes("works") + ) { + throw new Error( + "minimal build-VM bundle did not serve its static asset", + ); + } + console.log( + JSON.stringify({ + simpleBundle: true, + simpleRelease: simple.release, + }), + ); + } + + const rivetKitFiles = { + "package.json": localRivetKitTarball + ? JSON.stringify({ + name: "sqlite-notes-app", + version: "0.0.0", + private: true, + type: "module", + main: "src/index.ts", + dependencies: { + rivetkit: "file:./vendor/rivetkit.tgz", + "@rivetkit/rivetkit-wasm": + "0.0.0-feat-workflows-public-host-apis.0ff6164", + }, + }) + : await readFile( + new URL( + "../../../../examples/apps-sqlite/fixtures/app/package.json", + import.meta.url, + ), + ), + "src/index.ts": await readFile( + new URL( + "../../../../examples/apps-sqlite/fixtures/app/src/index.ts", + import.meta.url, + ), + ), + ...(localRivetKitTarball + ? { "vendor/rivetkit.tgz": await readFile(localRivetKitTarball) } + : {}), + }; + if (fast && artifactCacheDirectory) { + const runtime = await provisionAppNamespace("agentos-apps-e2e"); + const release = canonicalDeploymentHash({ + files: encodeFiles(rivetKitFiles), + entrypoint: "src/index.ts", + build: false, + packagingIdentity: `apps-builder@${appsBuilderVersion};manifest@1;bundle@2;esbuild-wasm@0.27.4;rivetkit-adapter@6`, + deploymentIdentity: JSON.stringify({ + regions: ["default"], + scaling: normalizeScaling({ + maxReplicas: 2, + targetConcurrency: 2, + }), + namespace: runtime.namespace, + runtime: { + endpoint: runtime.endpoint, + pool: appRunnerPool("agentos-apps-e2e"), + }, + usesRivetKit: true, + }), + }); + await cacheRivetKitArtifact( + rivetKitFiles, + artifactCacheDirectory, + release, + ); + } + const deployment = await deployApp({ + appId: "agentos-apps-e2e", + createNamespace: true, + files: rivetKitFiles, + scaling: { + maxReplicas: 2, + targetConcurrency: 2, + }, + }); + const guest = createClient({ + namespace: deployment.namespace, + poolName: deployment.pool, + }) as any; + const notes = guest.notes.getOrCreate(["shared"]); + await notes.add("first"); + const first = (await notes.list()) as unknown[]; + await notes.add("second"); + const second = (await notes.list()) as unknown[]; + if (second.length !== first.length + 1) { + throw new Error( + `DirectActor SQLite state did not advance: ${first.length} -> ${second.length}`, + ); + } + const firstResponse = await appsRouter.request("/agentos-apps-e2e/"); + if (!firstResponse.ok) { + throw new Error(`first HTTP request failed with ${firstResponse.status}`); + } + const firstBody = (await firstResponse.json()) as { + app?: unknown; + message?: unknown; + }; + if ( + firstBody.app !== "sqlite-notes" || + typeof firstBody.message !== "string" + ) { + throw new Error( + "the packed RivetKit SQLite application returned the wrong body", + ); + } + const load = await runLoadTest( + { + target: "http://agentos-apps.test/agentos-apps-e2e", + concurrency: 8, + durationSeconds: 30, + timeoutMs: 10_000, + maxRequests: 64, + maxSamples: 64, + maxResponseBytes: 1_024, + maxReplicaSeries: 128, + minSuccessRate: 1, + }, + async () => appsRouter.request("/agentos-apps-e2e/"), + ); + if ( + load.completed !== 64 || + load.successRate !== 1 || + load.replicaHeaderCoverage !== 1 + ) { + throw new Error(`bounded load test failed: ${JSON.stringify(load)}`); + } + // These are stable implementation actor actions, not part of the public + // Dynamic Apps client surface. + const control = createClient() as any; + const resolution = await control.agentOSAppsApp + .getOrCreate(["agentos-apps-e2e"]) + .resolveDeployment(); + const scaler = control.agentOSAppsScaler.getOrCreate(resolution.scalerKey); + const scaleDeadline = Date.now() + 30_000; + let scalerState = await scaler.inspect(); + while (scalerState.readyReplicas.length < 2 && Date.now() < scaleDeadline) { + await new Promise((resolve) => setTimeout(resolve, 100)); + scalerState = await scaler.inspect(); + } + if (scalerState.readyReplicas.length < 2) { + throw new Error( + `autoscaler did not produce a second ready replica: ${JSON.stringify(scalerState)}`, + ); + } + const scaledLoad = await runLoadTest( + { + target: "http://agentos-apps.test/agentos-apps-e2e", + concurrency: 4, + durationSeconds: 10, + timeoutMs: 10_000, + maxRequests: 16, + maxSamples: 16, + maxResponseBytes: 1_024, + maxReplicaSeries: 2, + minSuccessRate: 1, + }, + async () => appsRouter.request("/agentos-apps-e2e/"), + ); + if ( + scaledLoad.completed !== 16 || + scaledLoad.successRate !== 1 || + scaledLoad.maximumReplicaCount < 2 || + Object.keys(scaledLoad.replicas).length < 2 + ) { + throw new Error( + `scaled routing test failed: ${JSON.stringify(scaledLoad)}`, + ); + } + if (fast) { + console.log( + JSON.stringify( + { + hello, + deployment, + realRivetKitPackage: true, + localRivetKitTarball: Boolean(localRivetKitTarball), + directActorRowCounts: [first.length, second.length], + load, + scaledLoad, + fast: true, + }, + null, + 2, + ), + ); + return; + } + + scalerState = await scaler.inspect(); + const oldReplica = scalerState.readyReplicas[0]?.key; + if (!oldReplica) throw new Error("regional scaler has no ready replica"); + await scaler.drainReplica(oldReplica); + + const coldResponse = await appsRouter.request("/agentos-apps-e2e/"); + if (!coldResponse.ok) { + throw new Error(`cold HTTP request failed with ${coldResponse.status}`); + } + await coldResponse.arrayBuffer(); + const third = (await notes.list()) as unknown[]; + if (third.length !== second.length) { + throw new Error( + `DirectActor SQLite state was lost across replica replacement: ${second.length} -> ${third.length}`, + ); + } + + let failedBuild = false; + try { + await deployApp({ + appId: "agentos-apps-e2e", + createNamespace: true, + files: { + "package.json": JSON.stringify({ + private: true, + type: "module", + main: "dist/index.js", + scripts: { build: "tsc" }, + devDependencies: { typescript: "5.7.3" }, + }), + "tsconfig.json": JSON.stringify({ + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + outDir: "dist", + strict: true, + }, + include: ["src"], + }), + "src/index.ts": + "const invalid: string = 42; export default () => new Response(invalid);", + }, + }); + } catch (error) { + failedBuild = true; + assertTypeScriptDiagnostic(error); + } + if (!failedBuild) throw new Error("the invalid TypeScript build succeeded"); + + const rollbackResponse = await appsRouter.request("/agentos-apps-e2e/"); + if (!rollbackResponse.ok) { + throw new Error( + `active release was lost after failed build: ${rollbackResponse.status}`, + ); + } + + console.log( + JSON.stringify( + { + hello, + deployment, + realRivetKitPackage: true, + localRivetKitTarball: Boolean(localRivetKitTarball), + directActorRowCounts: [first.length, second.length, third.length], + load, + replacedReplica: oldReplica.join("/"), + coldStart: + coldResponse.headers.get("x-agentos-app-cold-start") === "1", + failedBuildPreservedActiveRelease: true, + }, + null, + 2, + ), + ); + } finally { + // The parent test harness owns Engine teardown. A graceful registry drain + // waits on RivetKit's intentionally long-lived serverless /start stream. + } +} + +async function verifyBuildScriptDiagnosticsAndRollback(): Promise { + const appId = "agentos-apps-build-e2e"; + await deployApp({ + appId, + files: { + "index.html": "

previous release

", + }, + }); + const before = await appsRouter.request(`/${appId}/`); + if (!before.ok || !(await before.text()).includes("previous release")) { + throw new Error("initial release did not become active"); + } + + let diagnostic = ""; + try { + await deployApp({ + appId, + files: { + "package.json": JSON.stringify({ + private: true, + type: "module", + main: "dist/index.js", + scripts: { build: "tsc" }, + devDependencies: { typescript: "5.7.3" }, + }), + "tsconfig.json": JSON.stringify({ + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + outDir: "dist", + strict: true, + }, + include: ["src"], + }), + "src/index.ts": + "const invalid: string = 42; export default () => new Response(invalid);", + }, + }); + } catch (error) { + diagnostic = assertTypeScriptDiagnostic(error); + } + if (!diagnostic) throw new Error("the invalid TypeScript build succeeded"); + + const after = await appsRouter.request(`/${appId}/`); + if (!after.ok || !(await after.text()).includes("previous release")) { + throw new Error("failed build replaced the previous active release"); + } + console.log( + JSON.stringify({ + buildScriptExecuted: true, + typeScriptDiagnostic: "TS2322", + failedBuildPreservedActiveRelease: true, + }), + ); +} + +function assertTypeScriptDiagnostic(error: unknown): string { + const diagnostic = + error instanceof Error + ? `${error.message}\n${JSON.stringify(error)}` + : JSON.stringify(error); + if (diagnostic.length > 64 * 1024) { + throw new Error("TypeScript diagnostics were not bounded"); + } + if (!diagnostic.includes("TS2322")) { + throw new Error( + `build failed before TypeScript produced its diagnostic: ${diagnostic}`, + ); + } + return diagnostic; +} + +async function cacheRivetKitArtifact( + files: Record, + cacheDirectory: string, + release: string, +): Promise<{ release: string; bytes: number }> { + const encodedFiles = encodeFiles(files); + const target = join(cacheDirectory, `${release}.aospkg`); + try { + const cached = await readFile(target); + return { release, bytes: cached.byteLength }; + } catch (error) { + if ( + typeof error !== "object" || + error === null || + !("code" in error) || + error.code !== "ENOENT" + ) { + throw error; + } + } + + const root = await mkdtemp(join(tmpdir(), "agentos-apps-host-bundle-")); + const workspace = join(root, "workspace"); + const releaseDirectory = join(root, "release"); + try { + for (const [path, content] of Object.entries(encodedFiles)) { + const destination = join(workspace, path); + await mkdir(dirname(destination), { recursive: true }); + await writeFile(destination, content); + } + const packageJson = JSON.parse( + await readFile(join(workspace, "package.json"), "utf8"), + ); + packageJson.dependencies = { + ...(packageJson.dependencies ?? {}), + "@rivetkit/rivetkit-wasm": + packageJson.dependencies?.["@rivetkit/rivetkit-wasm"] ?? + "0.0.0-feat-workflows-public-host-apis.0ff6164", + }; + packageJson.overrides = { + ...(packageJson.overrides ?? {}), + "@rivet-dev/agent-os-core": "npm:empty-npm-package@1.0.0", + "@rivetkit/engine-cli": "npm:empty-npm-package@1.0.0", + "@rivetkit/rivetkit-napi": "npm:empty-npm-package@1.0.0", + }; + await writeFile( + join(workspace, "package.json"), + JSON.stringify(packageJson), + ); + await writeFile( + join(workspace, "runner.mjs"), + runnerSource({ + entrypoint: "src/index.ts", + release, + port: 3080, + maxRequestBytes: 1024 * 1024, + maxResponseBytes: 4 * 1024 * 1024, + usesRivetKit: true, + }), + ); + await execFileAsync( + "npm", + [ + "install", + "--install-strategy=shallow", + "--omit=optional", + "--omit=peer", + "--legacy-peer-deps", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--loglevel=error", + ], + { cwd: workspace }, + ); + const configPath = join(root, "bundle.json"); + await writeFile( + configPath, + JSON.stringify({ + version: release, + workspace, + release: releaseDirectory, + entrypoint: "runner.mjs", + sourceFiles: Object.keys(files), + usesRivetKit: true, + maxOutputBytes: 64 * 1024 * 1024, + maxOutputFiles: 4096, + maxFileBytes: 32 * 1024 * 1024, + }), + ); + const builder = fileURLToPath( + new URL( + "../../../../packages/dynamic-apps-builder/cli/apps-builder.mjs", + import.meta.url, + ), + ); + await execFileAsync(process.execPath, [builder, configPath]); + const sourceTarPath = join(root, "release.tar"); + await execFileAsync( + "tar", + [ + "--sort=name", + "--mtime=@0", + "--owner=0", + "--group=0", + "--numeric-owner", + "-cf", + sourceTarPath, + ".", + ], + { cwd: releaseDirectory }, + ); + const packed = packAospkgFromTarBytes(await readFile(sourceTarPath)).bytes; + if (packed.byteLength >= 8 * 1024 * 1024) { + throw new Error( + `RivetKit App Bundle regressed to ${packed.byteLength} bytes`, + ); + } + await mkdir(cacheDirectory, { recursive: true }); + const temporary = `${target}.${process.pid}.tmp`; + await writeFile(temporary, packed); + await rename(temporary, target); + console.log( + JSON.stringify({ + rivetKitBundle: true, + release, + bytes: packed.byteLength, + }), + ); + return { release, bytes: packed.byteLength }; + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +function encodeFiles( + files: Record, +): Record { + return Object.fromEntries( + Object.entries(files).map(([path, content]) => [ + path, + typeof content === "string" ? new TextEncoder().encode(content) : content, + ]), + ); +} diff --git a/tests/e2e/dynamic-apps/tsconfig.json b/tests/e2e/dynamic-apps/tsconfig.json new file mode 100644 index 000000000..af7c847b4 --- /dev/null +++ b/tests/e2e/dynamic-apps/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..ac25577e0 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "strict": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "stripInternal": true, + "moduleResolution": "bundler", + "downlevelIteration": true, + "lib": ["ESNext"], + "types": ["node"] + } +} From adbc1c99b8983064f60d2a58a0c61b4c28742148 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 19 Aug 2026 19:24:23 -0700 Subject: [PATCH 2/7] chore: remove copied internal docs --- .../design/dynamic-apps-api-simplification.md | 835 ------------------ .../design/dynamic-apps-packaging.md | 406 --------- 2 files changed, 1241 deletions(-) delete mode 100644 docs-internal/design/dynamic-apps-api-simplification.md delete mode 100644 docs-internal/design/dynamic-apps-packaging.md diff --git a/docs-internal/design/dynamic-apps-api-simplification.md b/docs-internal/design/dynamic-apps-api-simplification.md deleted file mode 100644 index 3026b5076..000000000 --- a/docs-internal/design/dynamic-apps-api-simplification.md +++ /dev/null @@ -1,835 +0,0 @@ -# Dynamic Apps API Simplification - -Status: implemented and end-to-end validated proof of concept; production -credential plumbing and repo-wide release gates remain open. - -This document records the implemented Dynamic Apps public API and tracks the -remaining work required to take the proof of concept to production. The runtime -moves platform plumbing out of the user-facing surface while preserving -ordinary RivetKit clients and DirectActor calls. - -## Goals - -- Keep `setup()` and `createClient()` as ordinary RivetKit APIs. -- Make `setupApps()` responsible only for creating Dynamic Apps actor - definitions. -- Give every infrastructure actor a stable `agentOSApps*` registry name. -- Deploy a directory or generated in-memory file tree with one function. -- Mount all application HTTP routes on a Hono server without manual path - parsing or request forwarding. -- Use the ordinary RivetKit client defaults. Examples must not read or forward - `RIVET_*` environment variables. -- Store submitted files and immutable releases durably in the application - actor's SQLite database. -- Keep local files disposable. No persistent artifact directory may be required - for recovery or replica placement. -- Preserve the direct RivetKit actor path. Dynamic Apps must never proxy or - reinterpret DirectActor calls. - -## Target API - -### Actor setup - -`setupApps()` creates actor definitions and nothing else: - -```ts -import { setup, setupApps } from "@rivet-dev/dynamic-apps"; - -const { appsActors } = setupApps(); - -export const registry = setup({ - use: { - ...appsActors, - }, -}); - -registry.start(); -``` - -The exported map must use explicit, collision-resistant registry keys: - -```ts -const appsActors = { - agentOSAppsApp, - agentOSAppsScaler, - agentOSAppsReplica, -}; -``` - -`setupApps()` must not: - -- call `setup()` or start a registry; -- create or wrap a RivetKit client; -- construct an HTTP router; -- read Rivet endpoint, token, namespace, or pool environment variables; -- create directories or perform other import-time I/O. - -The initial common API should require no options: - -```ts -const { appsActors } = setupApps(); -``` - -Actor implementation overrides may be added later under an explicitly advanced -surface. They must not make the default example configure VM permissions, -runtime connection details, namespace provisioning, or artifact storage. - -### Deploying an application - -`deployApp()` is independent from the value returned by `setupApps()`: - -```ts -import { deployApp } from "@rivet-dev/dynamic-apps"; - -const deployment = await deployApp({ - appId: "hello-world", - source: new URL("../fixtures/app/", import.meta.url), -}); -``` - -It lazily creates an ordinary `createClient()` when no client is supplied. -RivetKit resolves the ordinary request client's endpoint, token, namespace, and -pool defaults. The proof of concept still duplicates the standard connection -variables for its internal namespace control-plane calls; removing that -duplication requires the RivetKit primitive tracked below. - -An existing ordinary client can be supplied without creating an Dynamic Apps -client or wrapper: - -```ts -await deployApp({ - appId: "hello-world", - source: new URL("../fixtures/app/", import.meta.url), -}, { client }); -``` - -The input supports a local directory for checked examples and an in-memory file -tree for generated applications: - -```ts -type DeployAppInput = - | { - appId: string; - source: URL; - createNamespace?: boolean; - regions?: string[]; - scaling?: AppScaling; - } - | { - appId: string; - files: Record; - createNamespace?: boolean; - regions?: string[]; - scaling?: AppScaling; - }; -``` - -The result should contain only stable application information: - -```ts -interface Deployment { - appId: string; - release: string; - namespace: string; - pool: string; - regions: string[]; -} -``` - -`appId` is always a required property. There is no positional application -identifier and no generated default. Public and internal implementation -identifiers must consistently use `appId`; remove ambiguous identifier names -such as `name`, `app`, and `appKey`. - -The common-path defaults are: - -| Setting | Default | -| --- | --- | -| `regions` | The stable application actor's current Rivet region | -| `scaling.minReplicas` | `0`; active releases scale to zero when idle | -| `scaling.maxReplicas` | `128` replicas per deployed region | -| `scaling.targetConcurrency` | `8` admitted requests per replica | -| Excess replica warm retention | Five minutes | -| RivetKit client | Lazily create the ordinary default client | -| Rivet namespace | Reuse the namespace configured for the ordinary Rivet connection | -| Dependency installation | `npm ci` with a lockfile; otherwise bounded `npm install` | -| Build | Run `npm run build` when the package defines a build script | -| Entrypoint | Infer from `exports`, then `main`, then the documented default | -| Release activation | Boot and verify at least one replica in every requested region before activation, even when `minReplicas` is `0` | - -`source` and `files` are mutually exclusive and exactly one is required. -`warmIdleTimeout` and infrastructure limits remain bounded internal or advanced -settings; callers should not need them for a normal deployment. - -### Hono routing - -HTTP routing is also independent from `setupApps()`: - -```ts -import { appsRouter } from "@rivet-dev/dynamic-apps"; -import { Hono } from "hono"; - -const server = new Hono(); - -server.route("/apps", appsRouter); -``` - -The router uses a lazy ordinary RivetKit client and handles: - -- `/:appId` and `/:appId/*`; -- bounded `appId` parsing; -- removal of the mounted application prefix; -- region selection; -- scaler admission and renewable leases; -- bounded request buffering and response streaming; -- cancellation and backpressure; -- hop-by-hop header removal and repeated response headers; -- typed mapping of expected routing errors to HTTP responses. - -For a custom client, an advanced adapter may construct the same router without -coupling it to `setupApps()`: - -```ts -import { createAppsRouter } from "@rivet-dev/dynamic-apps/advanced"; - -server.route("/apps", createAppsRouter({ client })); -``` - -There is no public `routeAppRequest()` in the target common API. - -### Complete server example - -The intended example server is: - -```ts -import { serve } from "@hono/node-server"; -import { - appsRouter, - deployApp, - setup, - setupApps, -} from "@rivet-dev/dynamic-apps"; -import { Hono } from "hono"; - -const { appsActors } = setupApps(); - -export const registry = setup({ - use: { - ...appsActors, - }, -}); - -registry.start(); - -await deployApp({ - appId: "hello-world", - source: new URL("../fixtures/app/", import.meta.url), -}); - -const server = new Hono(); - -server.route("/apps", appsRouter); - -serve({ - fetch: server.fetch, - port: 3000, -}); -``` - -Runner registration races must be handled inside Dynamic Apps with a bounded -retry for known readiness errors. This retry does not belong in examples. - -## Durable Storage - -### SQLite is the source of truth - -The stable application actor owns the submitted source and built release in its -SQLite database. Actor state should retain only small coordination fields; file -content and artifacts belong in explicit tables. - -Proposed logical schema: - -```text -app_releases - release_id - created_at - status - entrypoint - artifact_hash - artifact_bytes - build_error - -app_release_files - release_id - path - content - byte_length - -app_release_artifact_chunks - release_id - chunk_index - content - byte_length -``` - -All tables and operations must be bounded: - -- maximum releases retained per application; -- maximum files and source bytes per release; -- maximum path length; -- maximum individual and aggregate artifact bytes; -- fixed artifact chunk size and maximum chunk count; -- maximum build stdout and stderr retained; -- bounded transactions and batch sizes; -- typed errors naming the violated limit and its configuration. - -Deployment must be transactional from the caller's perspective: - -1. Validate and normalize every submitted path. -2. Compute the canonical release hash. -3. Insert the release and source files as a non-active building release. -4. Build in a short-lived agentOS VM. -5. Persist and verify the immutable artifact chunks. -6. Warm the required regional replicas. -7. Atomically make the release active. -8. Leave the previous release active on any failure. - -Failed release records may retain bounded diagnostics, but partial artifact -chunks must be removed. - -### Local files are disposable - -agentOS currently requires a host path when mounting a packed `.aospkg`. A -replica may therefore materialize an artifact from SQLite into a bounded -temporary file: - -```text -application actor SQLite - | - | bounded, checksummed chunks - v -replica-owned temporary .aospkg - | - v -agentOS VM read-only package mount -``` - -The temporary file: - -- is not the durable source of truth; -- is scoped to a replica or bounded cache entry; -- is recreated after process or host restart; -- remains present for the VM's entire lifetime because package reads may be - lazy; -- is cleaned after the VM and all lazy mount readers are finished; -- must never require a user-configured artifact directory. - -Delete `localArtifacts()` and the `.data/apps-artifacts` example directory. -There should be no `.agentos/apps/artifacts` persistent requirement either. - -### Replica wake, warm retention, and cleanup - -The replica lifecycle is: - -```text -wake - -> stream the active release from application SQLite - -> write a fresh replica-scoped temporary .aospkg - -> verify its size and content hash - -> boot the agentOS VM - -> report ready to the scaler - -retire, sleep, destroy, or startup failure - -> stop accepting new leases - -> drain bounded in-flight requests - -> stop and dispose the VM - -> delete the temporary .aospkg and its directory -``` - -Cleanup must run in `finally` on every terminal path. A failed cleanup must be -logged and retried or returned as a typed error; it must not be swallowed. A -subsequent wake always creates a new temporary path and never trusts a leftover -file from a previous VM. - -Warm retention and actor sleep are separate policies: - -- `warmIdleTimeout` controls how long the scaler keeps an excess replica hot - after its last lease; -- the configured minimum replica count remains hot indefinitely; -- the actor sleep grace period only controls the actor lifecycle and cleanup - window. It is not the warm-pool autoscaling policy. - -The current 30-second scale-down delay is too aggressive for a VM that must -rehydrate an npm application, initialize V8, and boot its HTTP server. Use a -five-minute default `warmIdleTimeout` for excess replicas, while keeping it an -advanced bounded setting rather than common setup configuration. - -`minReplicas` defaults to `0`. A deployment still boots and health-checks one -replica in every requested region before activating the release. That verified -replica remains warm until the normal idle timeout and can then retire, leaving -the active release at zero replicas. The next request rehydrates the artifact -from SQLite and cold-starts a replica. - -Replicas currently opt out of automatic actor sleep, so the scaler must -explicitly retire and destroy excess replicas after the warm idle timeout. -This preserves an accurate distinction between ready, warm replicas and -nonexistent replicas. If replicas later use engine-driven sleep, a sleeping -replica must first be removed from the scaler's ready set and must complete the -full wake-and-readiness sequence before receiving another request. - -### Scaler capacity warning - -Each regional scaler must emit a host-visible warning when its provisioned -replica count transitions from at or below 50% to above 50% of -`scaling.maxReplicas`. Count both ready and warming replicas so concurrent -scale-up cannot hide approaching capacity. - -For the default `maxReplicas: 128`, the warning is emitted when the count first -reaches `65`. It is transition-based rather than request-based: latch the -warning while usage remains above 50%, clear the latch after usage returns to -50% or below, and warn again only after a later upward crossing. - -The structured warning must include `appId`, release, region, ready replicas, -warming replicas, `maxReplicas`, and the utilization percentage. It must name -the limit and explain how to raise it. Reaching the warning threshold does not -reject traffic or force another scale-up by itself. - -### Retention and garbage collection - -When an inactive release exceeds the configured retention count: - -1. Drain its regional scalers and replicas. -2. Confirm no replica still references its artifact. -3. Delete its artifact chunks. -4. Delete its source files. -5. Delete its release metadata. - -Cleanup failures must be logged and retried. They must not be silently ignored. - -## Namespace and Runtime Plumbing - -The common path reuses the namespace configured for the ordinary Rivet -connection and makes no namespace-management request. Callers may set -`createNamespace: true` to idempotently create a stable, isolated namespace -for `appId` within the configured host namespace. - -Dynamic Apps must internally: - -1. Resolve the namespace from the ordinary Rivet connection by default. -2. When opted in, derive and idempotently create a namespace deterministic for - `appId` within the configured host namespace. -3. Configure a stable Dynamic Apps guest runner pool derived from `appId`. -4. Mint or resolve credentials scoped to that namespace and runner connection. -5. Inject only the namespace, endpoint, pool, scoped credential, and monotonic - release version into the guest process. -6. Keep management credentials out of actor state, SQLite, artifacts, logs, and - guest-visible environment variables. - -Remove these common API concepts: - -- `rivetNamespaceProvisioner()`; -- the `provision` callback; -- the `runtime()` callback; -- `AppRuntimeConfig`; -- manual endpoint, token, namespace, and pool configuration. - -If current RivetKit APIs cannot resolve default client configuration or create a -scoped runner credential without duplicating environment parsing, add the -necessary primitive to RivetKit. Do not keep the callback-based public API as a -workaround. - -## Source Build and Package Conventions - -For `{ source: URL }`, recursively load the directory with these rules: - -- accept only a `file:` directory URL; -- reject symlinks, devices, sockets, and paths escaping the root; -- enforce file-count, individual-file, total-byte, and path-length bounds while - reading; -- preserve empty files and binary static assets; -- sort normalized paths before hashing and upload; -- ignore only a documented fixed set of local build artifacts; -- never follow a user `.gitignore` implicitly. - -The in-memory API accepts byte values for static assets: - -```ts -files: Record -``` - -Dependency installation and compilation happen once per immutable release in a -short-lived agentOS build VM. They never run in the trusted host process and -never run independently on every serving replica: - -```text -deployApp() - -> validate and normalize the submitted source - -> persist the source in the application actor's SQLite - -> start an isolated, bounded agentOS build VM - -> materialize the source into the build VM workspace - -> install dependencies - -> run the build, if present - -> resolve and smoke-test the HTTP entrypoint or static output - -> prune build-only dependencies - -> pack source, output, and runtime node_modules into one .aospkg - -> stream checksummed artifact chunks into application SQLite - -> destroy the build VM and its temporary filesystem -``` - -Package behavior is inferred from `package.json`: - -1. Use `npm ci` when `package-lock.json` exists. -2. Otherwise use bounded `npm install` and retain the generated lockfile with - the immutable release. -3. Run lifecycle scripts only inside the untrusted build VM. The VM receives no - host secrets and has bounded CPU, memory, filesystem, process, output, - network, and wall-clock limits. -4. Run `npm run build` when a build script is present. -5. Resolve a server entrypoint from `package.json.exports`, then - `package.json.main`, then the documented source default. -6. If there is no server entrypoint but the build produced `dist/index.html`, - package `dist/` with the Dynamic Apps static HTTP entrypoint. -7. If there is no `package.json` and the submitted root contains `index.html`, - package the submitted tree as a static website without installing modules. -8. Fail with a typed error when the server/static mode or entrypoint is - ambiguous. - -After a server build, remove development-only dependencies while retaining -runtime dependencies. The resulting `.aospkg` contains the application and its -runtime `node_modules`, so ordinary Node package resolution works inside every -replica without another install. This is also how a guest application imports -the published `rivetkit` npm package. - -Native Node addons are not silently accepted when the agentOS JavaScript runtime -cannot load them. Installation or the smoke test must return a typed unsupported -module error naming the package. Pure JavaScript and WebAssembly packages use -normal package resolution. - -The first implementation should not add a shared mutable `node_modules` cache. -An identical immutable release may reuse its verified artifact; otherwise each -release receives a clean build VM. Build logs, artifact size, dependency count, -process count, network destinations, and build duration are all bounded and -reported through typed deployment errors. - -The common deployment API does not require `entrypoint`, `buildCommand`, -artifact paths, install commands, or VM options. Advanced explicit overrides -can be considered only when real applications prove these conventions -insufficient. - -## Actor Changes - -- Rename the stable application actor registry key to `agentOSAppsApp`. -- Rename the regional scaler actor registry key to `agentOSAppsScaler`. -- Rename the execution replica actor registry key to `agentOSAppsReplica`. -- Return those definitions from `setupApps()` as `appsActors`. -- Keep the actors infrastructure-only; users do not call them for guest actor - actions. -- Replace artifact-path metadata with SQLite release and artifact references. -- Add bounded artifact chunk read actions used only by execution replicas. -- Preserve renewable admission leases and scale-to-zero behavior. -- Preserve monotonic serverless runner versions across releases. -- Keep rollout preparation idempotent across actor retries and process restarts. -- Ensure failed new releases retire every partially created scaler and replica. - -## Client and DirectActor Behavior - -Dynamic Apps does not export a client and does not wrap `createClient()`. - -Guest actors are called through ordinary RivetKit: - -```ts -import { createClient } from "rivetkit/client"; - -const deployment = await deployApp({ appId: "hello-world", source }); -const client = createClient({ - namespace: deployment.namespace, - poolName: deployment.pool, -}); -``` - -The guest application namespace returned by `deployApp()` is used with the -ordinary DirectActor API when an explicit namespace is required. Calls travel: - -```text -RivetKit client - -> Rivet Engine - -> serverless callback through agentOSAppsApp - -> regional scaler admission - -> guest RivetKit registry in a warm agentOS VM - -> guest actor -``` - -This remains the ordinary DirectActor protocol: Dynamic Apps does not wrap the -client or reinterpret actions. Rivet's serverless callback travels through -`agentOSAppsApp` and the regional scaler so actor demand can wake a replica -from zero. It does not require the user-facing Hono router. - -## Public API Changes - -Target common exports: - -```ts -export { - AgentOSAppsError, - appsRouter, - deployApp, - setupApps, - type AppScaling, - type DeployAppInput, - type Deployment, -}; -``` - -Remove from the common public surface: - -```text -agentOSApps -localArtifacts -rivetNamespaceProvisioner -routeAppRequest -AgentOSAppsRoutingClient -AppRuntimeConfig -ArtifactStore -LocalArtifactsOptions -``` - -This repository does not guarantee protocol or API backward compatibility. -Update examples and documentation directly rather than carrying two competing -public APIs. - -## Flat Examples - -Replace the combined `examples/apps/` project with standalone directories: - -```text -examples/ - apps-hello-world/ - apps-sqlite/ - apps-workflows/ - apps-multiplayer/ - apps-static-website/ - apps-ai-builder/ -``` - -Each example uses: - -```text -package.json -tsconfig.json -src/ - server.ts -fixtures/ - app/ -``` - -Rules: - -- trusted host and server code lives in `src/`; -- uploaded application code and static assets live in `fixtures/`; -- examples export `const registry = setup(...)` and call `registry.start()` on a - later statement; -- examples spread `{ ...appsActors }` into `use`; -- examples do not read `RIVET_*` variables; -- examples do not contain local runner-readiness retry loops; -- examples do not contain persistent artifact directories; -- directory examples do not need `files.ts`. - -Move comprehensive validation and load tooling out of beginner examples: - -```text -tests/e2e/dynamic-apps/ -benchmarks/dynamic-apps/ -``` - -### Hello World - -Demonstrate only actor setup, directory deployment, Hono mounting, and one -response. - -### SQLite - -Demonstrate application data persisted through RivetKit actor SQLite. Verify -that data survives request routing to a different Dynamic Apps execution -replica. - -### Workflows - -Demonstrate a durable RivetKit workflow defined inside the deployed application, -including starting it over HTTP, observing progress, and resuming after an -execution replica is replaced. - -### Multiplayer - -Demonstrate a RivetKit multiplayer actor with multiple connected clients, -shared state, reconnect behavior, and execution-replica replacement. Keep the -example focused on the application API rather than load-generation machinery. - -### Static Website - -Demonstrate deploying HTML, CSS, JavaScript, and binary assets without a -`package.json`. Also document the `dist/index.html` convention for a built -static site. - -### AI App Builder - -Tie the complete flow together with the Vercel AI SDK: generate a bounded source -tree, deploy it, feed bounded TypeScript/build diagnostics back to the agent, -repair it, and activate only a successful release. - -Use the Vercel AI SDK on the trusted host: - -```text -prompt - -> coding agent edits bounded in-memory files - -> deployApp() runs the real TypeScript build - -> bounded diagnostics return to the agent - -> agent repairs the files - -> successful immutable release activates -``` - -The host, not the model, decides whether the workflow is complete. Require a -successful deployment, cap model steps and repair attempts, limit editable -paths, and retain the previous valid release during failed iterations. - -## Implementation Order - -### 1. Lock the public contract - -- [x] Add type-level tests for the exact `setupApps()` example. -- [x] Add type-level tests for directory and in-memory `deployApp()` calls. -- [x] Change `deployApp()` to one object input with a required `appId`; remove - the positional application identifier. -- [x] Rename every application identifier field, variable, actor input, route - parameter, error, example, and result to `appId`; remove identifier uses - of `name`, `app`, and `appKey`. -- [x] Add tests for all `deployApp()` defaults and partial scaling overrides. -- [x] Change the default `scaling.minReplicas` to `0` and test idle - scale-to-zero followed by a successful cold wake. -- [x] Change the default `scaling.maxReplicas` to `128` per region and replace - the current hard maximum of `64` with a bounded platform limit that - permits at least the default. -- [x] Add a latched structured warning when a regional scaler crosses above - 50% of `scaling.maxReplicas`, counting ready and warming replicas. -- [x] Test that the capacity warning fires once per upward crossing, rearms - after returning to 50% or below, and includes the required metadata. -- [x] Add a Hono mounting test for `appsRouter`. -- [x] Add a test proving the registry keys are exactly `agentOSAppsApp`, - `agentOSAppsScaler`, and `agentOSAppsReplica`. -- [x] Add a test proving `setupApps()` performs no I/O or client creation. - -### 2. Move releases into actor SQLite - -- [x] Design and migrate the release, file, and artifact-chunk tables. -- [x] Persist normalized source files before starting a build. -- [x] Stream build artifacts into bounded SQLite chunks. -- [x] Verify artifact length and content hash before marking a build ready. -- [x] Rehydrate an artifact into a replica-owned temporary file. -- [x] Keep the temporary artifact for the VM lifetime, then delete it after VM - disposal on retire, sleep, destroy, startup failure, and runtime error. -- [ ] Make cleanup retryable and observable; never reuse an unverified leftover - artifact on wake. -- [x] Replace the 30-second scale-down delay with a five-minute - `warmIdleTimeout` default for excess replicas. -- [x] Prove configured minimum replicas remain warm while excess replicas - retire after the configured timeout. -- [x] Prove deployment verifies one replica per region before activation even - when the configured minimum is zero. -- [x] Prove a cold start succeeds after deleting all local temporary data. -- [x] Implement release retention and garbage collection retried by later - deployments. -- [x] Remove the local artifact store implementation and configuration. - -### 3. Internalize namespace and runner setup - -- [x] Replace provisioning callbacks with internal idempotent namespace setup. -- [x] Make namespace creation opt-in and reuse the configured namespace by - default. -- [ ] Reuse RivetKit's default connection configuration. -- [ ] Add a RivetKit primitive if default config is not safely reusable. -- [x] Configure a stable per-app guest runner pool automatically. -- [ ] Create namespace-scoped guest credentials without exposing management - credentials. -- [x] Move runner-readiness retries into bounded internal deployment logic. -- [x] Delete the public runtime and provisioning APIs. - -### 4. Implement the simple deployment facade - -- [x] Add bounded directory loading. -- [x] Support binary in-memory files. -- [x] Run dependency installation, lifecycle scripts, builds, pruning, and - entrypoint smoke tests only inside a bounded short-lived build VM. -- [x] Infer install, build, server entrypoint, and static output behavior from - the submitted tree and `package.json`. -- [x] Pack runtime dependencies into the immutable artifact so replicas never - install modules on wake. -- [x] Return a typed error for unsupported native Node addons. -- [x] Support package-free static trees rooted at `index.html` and built static - output rooted at `dist/index.html`. -- [x] Preserve content-addressed, deterministic release hashing. -- [x] Let callers optionally pass an ordinary RivetKit client. -- [x] Lazily create the default client without import-time side effects. -- [x] Return stable deployment information and typed build errors. - -### 5. Implement the Hono router - -- [x] Add the `/:appId` and `/:appId/*` routes. -- [x] Strip the mount prefix correctly. -- [x] Preserve response streaming, cancellation, backpressure, and repeated - headers. Request bodies remain bounded and buffered. -- [x] Select regions without maintaining edge-local placement state. -- [x] Use a lazy default client. -- [x] Provide custom-client construction only in the advanced surface. -- [x] Remove public `routeAppRequest()`. - -### 6. Rewrite examples and documentation - -- [x] Replace `examples/apps/` with the flat examples. -- [x] Add focused Hello World, SQLite, Workflows, Multiplayer, and Static - Website examples and corresponding documentation. -- [x] Move E2E verification to `tests/e2e/dynamic-apps/`. -- [x] Move the load driver to `benchmarks/dynamic-apps/`. -- [x] Add the AI SDK generate, type-check, repair, and deploy example. -- [x] Rewrite the package README around the target API. -- [x] Rewrite the website Apps page in this order: product overview, checked - Hello World quick start, application structure, deployment, builds and - dependencies, HTTP routing, SQLite and RivetKit persistence, scaling and - cold starts, regions and isolation, examples, API reference, and current - limitations. -- [x] Lead the website page with the deployable user API; keep scaler, - namespace, artifact, and runner internals after the quick start. -- [x] Source every runnable website snippet from the checked flat examples - through the docs theme `` mechanism. -- [x] Include the deployment defaults table, build pipeline, disposable-replica - versus durable-SQLite diagram, request routing diagram, scale-to-zero - lifecycle, and 50% scaler-capacity warning. -- [x] Link and briefly describe the Hello World, SQLite, Workflows, Multiplayer, - Static Website, and AI App Builder examples without duplicating their - complete READMEs. -- [x] Document the ordinary DirectActor path without an Dynamic Apps client - proxy. -- [x] Update the main Dynamic Apps design wherever the old artifact-store and - routing APIs appear. -- [x] Remove obsolete environment-variable and artifact-directory guidance. - -### 7. Validate the complete behavior - -- [x] Run package unit tests and type checks. -- [x] Run `cargo check --workspace`. -- [ ] Run `pnpm build` and `pnpm check-types`. -- [x] Run fixed-version and publish-helper checks. -- [x] Build the website. -- [x] Run a real RivetKit guest from a packed npm dependency tree. -- [x] Verify DirectActor state survives replica replacement. -- [x] Verify deployment recovery with an empty local filesystem. -- [x] Verify failed TypeScript builds return bounded diagnostics and do not - replace the active release. -- [x] Verify abandoned HTTP requests recover through admission lease expiry. -- [x] Run the bounded load test and record cold-start and warm-request latency. - -## Completion Criteria - -The simplification is complete when a new user can understand the hello-world -server without learning about artifacts, runtime callbacks, namespace -provisioners, route forwarding, or Rivet environment variables; the same -implementation must still recover every deployed release from actor SQLite and -run real RivetKit actors through the ordinary DirectActor API. diff --git a/docs-internal/design/dynamic-apps-packaging.md b/docs-internal/design/dynamic-apps-packaging.md deleted file mode 100644 index f5f15e50c..000000000 --- a/docs-internal/design/dynamic-apps-packaging.md +++ /dev/null @@ -1,406 +0,0 @@ -# Dynamic Apps Packaging - -Status: implemented and validated on 2026-07-24. - -The production builder is `@rivet-dev/dynamic-apps-builder`. Turbo builds its -generated `dist/package.aospkg` before `@rivet-dev/dynamic-apps`; the artifact -is gitignored but included in the published builder npm package. The current -builder package is 14.2 MB uncompressed and 3.6 MB inside its npm tarball. -The focused shell package is 2.9 MB uncompressed and 1.0 MiB inside its npm -tarball; the build VM no longer mounts the 67.3 MB coreutils package. - -Validation reduced the real RivetKit 2.3.9 fixture from 40,967,331 bytes to -4,889,457 bytes. End-to-end tests cover a package-free app, RivetKit HTTP, -DirectActor state, two-replica autoscaling and routing, and bounded load. The -builder tests also inspect the isolated release rather than resolving -dependencies from the repository. - -This document records the agreed packaging model for Dynamic Apps. The central -rule is: - -> Tenant dependencies exist only in a disposable build VM. Execution replicas -> receive a minimal, immutable App Bundle and never install or build anything. - -The public `deployApp()` API does not expose the bundler, artifact format, or -build-VM configuration. - -## Public API - -Packaging remains an implementation detail behind the existing API: - -```ts -await deployApp({ - appId: "hello", - source: new URL("../fixtures/app", import.meta.url), -}); -``` - -Generated applications continue to use the in-memory file form: - -```ts -await deployApp({ - appId: "hello", - files, -}); -``` - -The common API must not add `bundler`, `runtime`, `artifact`, `minify`, or -builder-package options. Dynamic Apps owns the build conventions and their sane -defaults. - -## Packaging Flow - -```text -deployApp({ appId, source/files }) - | - v -source stored in the stable app actor's SQLite - | - v -temporary agentOS build VM - - mounts platform-owned apps-builder.aospkg - - mounts the platform POSIX shell for package build scripts - - writes tenant source into /workspace - - runs npm ci/install for tenant dependencies - - runs npm run build when defined - - generates the agentOS HTTP runner - - bundles runner + server code + JavaScript dependencies - - emits imported WASM/binary modules separately - - collects static assets - | - v -minimal /release directory - main.mjs - modules/* - public/* - manifest.json - | - v -pack /release as an immutable .aospkg - | - v -store checksummed .aospkg chunks in app actor SQLite - | - v -replica rehydrates and mounts .aospkg at /app - | - v -node /app/main.mjs -``` - -This is equivalent to a multi-stage Docker build: the agentOS build VM is the -builder stage, and the release `.aospkg` is the minimal final image. - -## Platform Build Package - -The build tool must be platform-owned and automatically available. Tenants must -not install it through their own `package.json`. - -Add a software package with a name such as: - -```text -@rivet-dev/dynamic-apps-builder -``` - -`@rivet-dev/dynamic-apps` depends on that package in the same way it currently -depends on `@agentos-software/tar`. Package scripts use the focused -`@agentos-software/sh` package instead of mounting the full coreutils command -set. Each package exports a `SoftwarePackageRef`, and only the Dynamic Apps -build VM includes them in `software`: - -```ts -const buildVmOptions = { - defaultSoftware: false, - software: [sh, tar, appsBuilder], - // Existing bounded permissions and limits. -}; -``` - -The packed software payload contains the pinned build program and everything it -needs: - -```text -apps-builder.aospkg - build-app.mjs - esbuild-wasm JavaScript support - esbuild.wasm - package metadata -``` - -The exact projected package path should come from package resolution rather than -being duplicated as an arbitrary versioned string. The VM invokes the JavaScript -entrypoint with its existing Node runtime. - -Important lifecycle properties: - -- The host installs the Apps builder transitively with Dynamic Apps. -- Every build VM mounts the same immutable, read-only software package. -- No deployment downloads the platform bundler into the tenant workspace. -- Serving replicas do not mount the builder package. -- The release `.aospkg` never contains the builder. -- The builder version and configuration participate in the release hash. -- Updating the builder invalidates build-cache keys. -- Do not add the builder to the global agentOS base layer; ordinary VMs and - serving replicas do not need its relatively large WASM compiler. - -## Build Tool - -Use the mainstream esbuild build model rather than creating a new compiler or -framework build system: - -- Native isolated Linux builders may use ordinary native `esbuild`. -- The current agentOS build VM should use `esbuild-wasm`, because the normal - `esbuild` npm package launches a platform-specific native executable. -- Both implementations must produce the same App Bundle contract. -- The choice between native and WASM esbuild remains internal and can change - without changing `deployApp()` or the execution replicas. - -The direct `esbuild-wasm` API is validated in a real agentOS build VM. The -builder uses esbuild's in-process browser service with `worker: false`; its -Node entrypoint launches a child-process service that is unnecessary inside -the VM and previously made failed builds stall. - -The platform-owned build program should roughly: - -1. Accept a generated runner entrypoint, workspace root, release output - directory, and bounded build settings. -2. Bundle for the agentOS Node runtime as ESM. -3. Set `NODE_ENV` to `production` for dead-code elimination. -4. Enable tree shaking and minification. -5. Emit an external source map for diagnostics, but keep it outside the runtime - package. -6. Emit recognized WASM and binary imports as separate files. -7. Return a metafile describing every generated input and output. -8. Hash the output files and write the App Bundle manifest. - -The generated agentOS runner, rather than the tenant entrypoint alone, is the -bundle entrypoint. This ensures the HTTP adapter and the tenant's imports share -one module graph and one RivetKit module identity. - -## App Bundle - -The logical runtime output is: - -```text -/release - main.mjs - modules/ - -.wasm - -.bin - public/ - index.html - assets/* - manifest.json -``` - -Not every release needs every directory. A server-only release may contain only -`main.mjs`, while a static site may contain a small generated runner and -`public/`. - -The internal manifest should be versioned and simple: - -```ts -interface AppBundleManifest { - version: 1; - mainModule: string; - modules: Array<{ - path: string; - type: "esm" | "wasm" | "text" | "data"; - size: number; - hash: string; - }>; - assets: Array<{ - path: string; - size: number; - hash: string; - }>; -} -``` - -This manifest is internal. Users do not construct or upload it directly in the -initial API. - -The release package must not contain: - -```text -node_modules/ -src/ -package-lock.json -tsconfig.json -platform build tools -unused package files -``` - -SQLite continues to retain the submitted source separately for release history -and rebuilding. The execution artifact contains only runtime outputs. - -## Module And Asset Discovery - -Do not recursively scan `node_modules` for files with interesting extensions. -Packages often ship browser, debug, test, and architecture-specific payloads -that are not used at runtime. - -Use three bounded rules instead. - -### Imported modules - -The bundler follows statically analyzable imports: - -```ts -import dependency from "dependency"; -import schema from "./schema.json"; -import query from "./query.sql"; -import wasmPath from "./engine.wasm"; -``` - -JavaScript, TypeScript, and JSON join the bundle. Recognized WASM and binary -imports become hashed files under `modules/`. Small text-like module types may -be inlined. - -Literal dynamic imports are supported. Computed imports and opaque filesystem -paths are not generally discoverable by any bundler. - -### Static assets - -Use conventional static output directories rather than guessing arbitrary -files: - -- a package-free root static site; -- `dist/` when a frontend build produces `dist/index.html`; -- `public/` for explicitly public application assets. - -Static paths, sizes, and hashes are recorded in the manifest. Initially, include -the required bytes in the release `.aospkg`. The manifest permits a future -content-addressed asset store without changing the user API. - -Do not implement a multi-step asset upload session or JWT protocol for the -proof of concept. The stable app actor already receives and durably owns the -complete file tree. - -### Non-analyzable runtime files - -Computed imports and arbitrary `fs.readFile(runtimeValue)` cannot be packaged -reliably without an explicit convention. The initial behavior should fail with -a bounded, typed build error that identifies the unresolved dependency. - -An advanced module-rule escape hatch may be added when a concrete application -requires it. It is not part of the initial common API. - -## RivetKit - -RivetKit receives a built-in packaging adapter because it is a first-class -Dynamic Apps use case. - -The target is: - -```text -main.mjs bundled app + RivetKit JavaScript -modules/rivetkit-.wasm RivetKit runtime -``` - -Do not retain the RivetKit npm package tree, NAPI bindings, Engine CLI, or -agentOS host integrations in the release. - -Prefer an upstream RivetKit surface that makes its WASM import statically -analyzable or accepts preloaded WASM bindings. Until that is available, the -Apps builder may explicitly resolve and emit the one known RivetKit WASM module. -This is a narrow platform adapter, not a generic `node_modules` scan. - -The generated runner initializes the emitted WASM bytes before importing or -starting the guest registry. `RIVETKIT_RUNTIME=wasm` and serverless runtime mode -remain enforced by the replica. - -## Storage And Replica Lifecycle - -The stable app actor's SQLite database remains the durable source of truth: - -```text -submitted source BLOBs -release metadata -immutable checksummed .aospkg chunks -``` - -A replica: - -1. Downloads the immutable artifact chunks. -2. Validates total bytes and SHA-256. -3. Writes a replica-scoped temporary `.aospkg`. -4. Mounts it read-only at `/app`. -5. Starts `node /app/main.mjs`. -6. Keeps the temporary package while lazy mount readers may exist. -7. Removes it after the VM is disposed. - -Replicas never run npm, a framework build, or the platform bundler. - -## Release Identity - -The release hash must cover: - -- normalized submitted source; -- selected tenant entrypoint and static root; -- tenant build configuration already used by the platform; -- generated runner semantics; -- App Bundle manifest version; -- Apps builder package version; -- bundler version and material options; -- RivetKit packaging-adapter version. - -Changing packaging semantics must not reuse an artifact built under older -semantics. - -## Security And Limits - -Tenant source, dependencies, build scripts, and bundler inputs remain untrusted. -They execute inside the bounded build VM, not in the trusted app actor process. - -Keep or add explicit limits for: - -- source files and bytes; -- dependency count; -- build duration; -- process count and open file descriptors; -- V8 heap; -- build filesystem bytes; -- bundler input and output bytes; -- emitted module and asset counts; -- individual emitted file size; -- total App Bundle size; -- captured diagnostics and source-map size. - -Threshold warnings and typed errors must identify the configured limit and how -to raise it. Build failures must leave the previous active release unchanged. - -## Acceptance Criteria - -The packaging change is complete when tests prove: - -1. A plain JavaScript HTTP app bundles and serves without runtime - `node_modules`. -2. A TypeScript app runs its build, bundles the output, and reports bounded - compiler diagnostics on failure. -3. A real RivetKit app serves HTTP and DirectActor calls using its emitted WASM - module. -4. RivetKit actor state survives replica replacement because state remains in - Rivet, not the release filesystem. -5. A static website serves HTML, JavaScript, CSS, and binary assets. -6. An imported WASM fixture is emitted as a separate hashed runtime module. -7. The release archive contains only the manifest, bundle outputs, and required - assets. -8. The release archive contains no tenant `node_modules`, source tree, lockfile, - or Apps builder. -9. A cold replica rehydrates the minimal artifact from SQLite and becomes - healthy without npm or network access. -10. Repeating a deployment with the same source and builder version reuses the - same release identity. -11. Changing the builder or manifest version invalidates the release identity. -12. Unsupported computed imports or opaque runtime files fail with a clear - typed error. -13. Artifact size is recorded in tests so the RivetKit fixture cannot silently - regress back to shipping its production dependency tree. - -## References - -- [Cloudflare Wrangler bundling](https://developers.cloudflare.com/workers/wrangler/bundling/) -- [Cloudflare multipart Worker upload metadata](https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/) -- [Cloudflare Workers for Platforms static assets](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/static-assets/) -- [Cloudflare Workers versions and deployments](https://developers.cloudflare.com/workers/versions-and-deployments/) From 4794a5e15356ba394e24421347ab12605d46da68 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 19 Aug 2026 19:33:06 -0700 Subject: [PATCH 3/7] chore: add standalone release recipes --- justfile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 justfile diff --git a/justfile b/justfile new file mode 100644 index 000000000..67e6a4a13 --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +[group('release')] +release VERSION TAG='auto' REF='main': + gh workflow run .github/workflows/publish.yml \ + --ref "{{ REF }}" \ + --field version="{{ VERSION }}" \ + --field dist_tag="{{ TAG }}" + +[group('release')] +preview-publish VERSION REF: + just release "{{ VERSION }}" preview "{{ REF }}" + +[group('release')] +release-preview VERSION REF: + just preview-publish "{{ VERSION }}" "{{ REF }}" From 1921ee5209df10b97ee974d6f8082e2a304278c0 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 19 Aug 2026 19:37:17 -0700 Subject: [PATCH 4/7] docs: consolidate documentation locations --- packages/dynamic-apps/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/dynamic-apps/README.md b/packages/dynamic-apps/README.md index 19a4dd4e8..914a88da7 100644 --- a/packages/dynamic-apps/README.md +++ b/packages/dynamic-apps/README.md @@ -3,6 +3,8 @@ Dynamic Apps deploys user-generated JavaScript and static sites into isolated VMs and routes HTTP through Rivet Actors. +[Documentation](https://rivet.dev/dynamic-apps/docs) + Install Dynamic Apps in a Node.js 22 or newer project: ```sh From 5a93ef0a155abaebbe46687e0180fdd88a5f7332 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 19 Aug 2026 19:41:47 -0700 Subject: [PATCH 5/7] docs: remove unused markdown files --- benchmarks/dynamic-apps/README.md | 31 -------- docs/CLAUDE.md | 125 ------------------------------ docs/content/use-cases/index.mdx | 9 --- tests/e2e/dynamic-apps/README.md | 36 --------- 4 files changed, 201 deletions(-) delete mode 100644 benchmarks/dynamic-apps/README.md delete mode 100644 docs/CLAUDE.md delete mode 100644 docs/content/use-cases/index.mdx delete mode 100644 tests/e2e/dynamic-apps/README.md diff --git a/benchmarks/dynamic-apps/README.md b/benchmarks/dynamic-apps/README.md deleted file mode 100644 index deb1cfd7b..000000000 --- a/benchmarks/dynamic-apps/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Dynamic Apps load test - -Start `examples/apps-hello-world`, then run: - -```sh -pnpm --filter @rivet-dev/dynamic-apps-benchmarks load -``` - -The bounded driver reports p50, p89, p95, and p99 latency for all, cold, and -warm requests, plus queue delay, replica distribution, throughput, and status -counts. - -The defaults run 16 concurrent clients for 10 seconds, with hard limits of -100,000 requests, 100,000 latency samples, 1 MiB per response, 1,024 replica -series, and 10 seconds per request. Configure them with: - -| Variable | Default | -| --- | ---: | -| `LOAD_TEST_URL` | `http://127.0.0.1:3000/apps/hello-world` | -| `LOAD_TEST_CONCURRENCY` | `16` | -| `LOAD_TEST_DURATION_SECONDS` | `10` | -| `LOAD_TEST_TIMEOUT_MS` | `10000` | -| `LOAD_TEST_MAX_REQUESTS` | `100000` | -| `LOAD_TEST_MAX_SAMPLES` | `100000` | -| `LOAD_TEST_MAX_RESPONSE_BYTES` | `1048576` | -| `LOAD_TEST_MAX_REPLICA_SERIES` | `1024` | - -Optional `LOAD_TEST_MAX_P95_MS` and `LOAD_TEST_MIN_SUCCESS_RATE` (from `0` to -`1`) turn the run into a failing performance gate. A run can take up to one -request timeout beyond its configured duration while the final in-flight -requests finish. diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 100644 index 2d6ae04c2..000000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1,125 +0,0 @@ -# Docs Bundle CLAUDE.md - -Rules for the docs in this repo. These pages are **not** rendered here — they are -published on [rivet.dev](https://rivet.dev) by the -[rivet-website](https://github.com/rivet-dev/website) repo, which symlinks -this directory in. Everything below exists so a page written here renders -correctly there. - -## Layout - -``` -docs/ - sidebar.json navigation for the two tabs - content/ - docs/**.mdx -> /{product}/docs/... - tutorials/**.mdx -> /{product}/tutorials/... -``` - -The website links `docs/content` into its content collection, so **only real -pages belong under `content/`**. Anything else (scripts, fixtures, notes) goes -elsewhere in the repo or it will be published as a docs page. - -## Frontmatter - -Every page needs `title` and `description`. Both are used for SEO and the -sidebar falls back to `title` when a sidebar entry omits one. - -```mdx ---- -title: "In-Memory State" -description: "Actors store state in memory for instant reads and writes." ---- -``` - -## sidebar.json - -Navigation for this product's Documentation and Tutorials tabs. Icons travel as -Font Awesome **export names**, not objects, so this repo needs no dependency on -the website's icon package. - -```json -{ - "docs": [ - { "title": "General", "pages": [ - { "title": "Introduction", "href": "/actors/docs", "icon": "faSquareInfo" } - ]} - ], - "tutorials": [] -} -``` - -- `href` is the full site path, including the product segment. -- Adding a page to `content/` does not add it to the nav. Add it here too. -- The Self-Host tab is **not** in this file. It is generated by the website. - -## Code - -- **Never inline a fenced TypeScript block.** Real examples live in `examples/` - and are embedded with ``, so they are type-checked and cannot rot. - A snippet that fails to compile fails the website build. -- Snippet paths are relative to **this repo's root**, so the same path works both - here and on rivet.dev: - ```mdx - - ``` -- Embed part of a file with `region="name"`, delimited in the source by - `// docs:start name` / `// docs:end name`. -- Shell commands, YAML, Dockerfiles, and terminal output **may** be inline fenced - blocks. The no-inline rule exists for type checking, which only applies to - TypeScript. -- Every TypeScript snippet must include its imports and define everything it - references. Use `@nocheck` only for API that does not exist on this branch yet. -- Use `` for examples spanning multiple files, with each - file as its own ``. - -## What does not belong here - -- **Marketing pages.** They live in the website repo. -- **Deploy and self-hosting guides.** They are written once in the website repo - and templated across every product. Do not write a per-product copy. -- **Website components.** Do not import from the website by relative path or - alias; a page must render from the components the site already provides. - -## Terminology - -Applies to everything published on the website. - -- The service that routes, schedules, and persists is the **control plane**. - Never "engine", "server", or "orchestrator". -- A process running user code with the Rivet SDK is a **worker**. Never "envoy", - "runner", "node", "compute", or "data plane". -- **Never use "agent" as a deployment noun.** Rivet ships agentOS and Actors is - "where agents live"; the collision is unrecoverable. -- **"envoy" never appears in docs.** Envoy Proxy is a top-tier CNCF project. - Internal code keeps its own names. -- **"Rivet Compute" is retired.** Where prose must name the managed offering it - is **Rivet Cloud**, and it links to . -- Spell the product `agentOS`, never `AgentOS`. Capitalize **Rivet Actor** as a - proper noun, lowercase generic "actor". -- Always `rivet.dev`, never `rivet.gg`. - -## Writing - -- Write comments and prose as complete sentences. **Never use em dashes**; use - periods instead. -- Do not document deltas. A reader who never saw the old version gains nothing - from "this was renamed". - -## Previewing locally - -Clone the website next to this repo and run it. It detects the sibling -automatically and serves this directory's pages live: - -```sh -git clone https://github.com/rivet-dev/website -cd rivet-website && pnpm install && pnpm dev -``` - -`pnpm assemble` prints which checkout each product resolved to. To point at a -different checkout, repoint the symlink; it is gitignored and assemble leaves an -existing one alone: - -```sh -ln -sfn /path/to/this/repo/docs/content src/content/docs/ -``` diff --git a/docs/content/use-cases/index.mdx b/docs/content/use-cases/index.mdx deleted file mode 100644 index c09d0d853..000000000 --- a/docs/content/use-cases/index.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: "Use Cases" -description: "What people build with Dynamic Apps." ---- - - -**TODO.** Overview of what Dynamic Apps is used for, one section per use case, each -linking to the guide that shows how to build it. - diff --git a/tests/e2e/dynamic-apps/README.md b/tests/e2e/dynamic-apps/README.md deleted file mode 100644 index c5c01c3e1..000000000 --- a/tests/e2e/dynamic-apps/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Dynamic Apps end-to-end verification - -This expensive check starts a local Rivet engine, installs the real `rivetkit` -npm dependency inside an agentOS build VM, serves the packed app, calls a guest -actor through DirectActor, replaces the execution replica, and verifies both -SQLite state recovery and failed-build rollback. - -```sh -pnpm --filter @rivet-dev/dynamic-apps-e2e test:e2e -``` - -For routing and host-runtime iteration, cache only the content-addressed -`.aospkg` build artifact: - -```sh -pnpm --filter @rivet-dev/dynamic-apps-e2e test:e2e:fast -``` - -The first fast run performs the full package build. Later runs stop after HTTP -and DirectActor checks and reuse `/tmp/agentos-apps-artifact-cache-v16`. Every -run still starts with a fresh Rivet database, uploads the artifact into actor -SQLite, materializes it in a replica, and exercises the real routing path. -Remove that cache directory to force a clean build. The normal `test:e2e` -command never reads the cache and remains the required final gate. - -To test an unpublished local RivetKit package without checking a tarball into -this repository: - -```sh -AGENTOS_APPS_RIVETKIT_TARBALL=/absolute/path/to/rivetkit.tgz \ - pnpm --filter @rivet-dev/dynamic-apps-e2e test:e2e:fast -``` - -The test uploads that tarball as `vendor/rivetkit.tgz` and installs it through -`file:./vendor/rivetkit.tgz`. The guest installs the matching -`@rivetkit/rivetkit-wasm` version directly and omits optional dependencies. From aaac1bf4536c8c91ba6d63f6fc69014d26fcb7ba Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 19 Aug 2026 19:49:02 -0700 Subject: [PATCH 6/7] ci(release): migrate npm publishing to oidc --- .github/workflows/publish.yml | 10 +++------- packages/dynamic-apps-builder/package.json | 5 +++++ packages/dynamic-apps/package.json | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92d1349a9..eddfd1f71 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,6 +38,8 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml registry-url: https://registry.npmjs.org + - name: Install OIDC-capable npm + run: npm install --global npm@11.16.0 - run: pnpm install --frozen-lockfile - id: release run: >- @@ -46,11 +48,9 @@ jobs: --tag=${{ inputs.dist_tag }} --branch=${{ github.ref_name }} - name: Verify npm authentication and unused versions - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | set -euo pipefail - npm whoami + npm ping for package in @rivet-dev/dynamic-apps-builder @rivet-dev/dynamic-apps; do if npm view "$package@${{ steps.release.outputs.version }}" version >/dev/null 2>&1; then echo "$package@${{ steps.release.outputs.version }} already exists" >&2 @@ -65,8 +65,6 @@ jobs: - run: pnpm lint - run: pnpm test:packed - name: Publish builder - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: >- npm publish .pack/rivet-dev-dynamic-apps-builder-${{ steps.release.outputs.version }}.tgz @@ -85,8 +83,6 @@ jobs: echo "builder version did not become visible" >&2 exit 1 - name: Publish main package - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: >- npm publish .pack/rivet-dev-dynamic-apps-${{ steps.release.outputs.version }}.tgz diff --git a/packages/dynamic-apps-builder/package.json b/packages/dynamic-apps-builder/package.json index 8396ea922..218667aea 100644 --- a/packages/dynamic-apps-builder/package.json +++ b/packages/dynamic-apps-builder/package.json @@ -4,6 +4,11 @@ "type": "module", "license": "Apache-2.0", "description": "Platform-owned Dynamic Apps release bundler", + "repository": { + "type": "git", + "url": "https://github.com/rivet-dev/dynamic-apps.git", + "directory": "packages/dynamic-apps-builder" + }, "main": "./dist/index.js", "types": "./dist/index.d.ts", "bin": { diff --git a/packages/dynamic-apps/package.json b/packages/dynamic-apps/package.json index fe69a0125..974f3bde9 100644 --- a/packages/dynamic-apps/package.json +++ b/packages/dynamic-apps/package.json @@ -3,6 +3,11 @@ "version": "0.2.15", "description": "Run and scale user-generated HTTP applications with Rivet Actors.", "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/rivet-dev/dynamic-apps.git", + "directory": "packages/dynamic-apps" + }, "type": "module", "sideEffects": false, "files": [ From 27c3949471ad6d55d95c2de881803539bf49d66c Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 19 Aug 2026 19:49:02 -0700 Subject: [PATCH 7/7] docs: remove unnecessary markdown files --- docs/AGENTS.md | 1 - examples/apps-ai-builder/README.md | 20 ----- examples/apps-hello-world/README.md | 21 ----- examples/apps-multiplayer/README.md | 19 ----- examples/apps-sqlite/README.md | 19 ----- examples/apps-static-website/README.md | 18 ----- examples/apps-workflows/README.md | 18 ----- packages/dynamic-apps/README.md | 102 ------------------------- packages/dynamic-apps/package.json | 1 - 9 files changed, 219 deletions(-) delete mode 120000 docs/AGENTS.md delete mode 100644 examples/apps-ai-builder/README.md delete mode 100644 examples/apps-hello-world/README.md delete mode 100644 examples/apps-multiplayer/README.md delete mode 100644 examples/apps-sqlite/README.md delete mode 100644 examples/apps-static-website/README.md delete mode 100644 examples/apps-workflows/README.md delete mode 100644 packages/dynamic-apps/README.md diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 120000 index 681311eb9..000000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/examples/apps-ai-builder/README.md b/examples/apps-ai-builder/README.md deleted file mode 100644 index 232a636ba..000000000 --- a/examples/apps-ai-builder/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Dynamic Apps: AI App Builder - -This trusted host uses the Vercel AI SDK to generate a bounded three-file -RivetKit application. `deployApp()` runs the real TypeScript build in an -isolated VM. Bounded diagnostics are fed back to the model for at most three -repairs, and a failed build never replaces the previous active release. - -Run the checked workspace example with Node.js 22 or newer: - -```sh -ANTHROPIC_API_KEY=... pnpm --dir examples/apps-ai-builder start -# In another terminal: -curl -X POST http://localhost:3000/deploy/ai-generated-app \ - -H 'content-type: application/json' \ - -d '{"prompt":"Build a collaborative counter"}' -``` - -RivetKit starts its local Engine automatically. Against an existing Rivet -deployment, use the standard Rivet connection variables instead. The successful app is mounted at -`http://localhost:3000/apps/ai-generated-app`. diff --git a/examples/apps-hello-world/README.md b/examples/apps-hello-world/README.md deleted file mode 100644 index ed2376cfc..000000000 --- a/examples/apps-hello-world/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Dynamic Apps: Hello World - -This smallest example registers the three infrastructure actors, mounts the -Apps router, and deploys generated files from a separate script. The app runs at -`http://localhost:3000/apps/hello-world/`. - -The uploaded fixture serves an HTML page at `/` and a JSON endpoint at -`/api/hello`. It does not use RivetKit because it has no durable or coordinated -state; the SQLite, workflows, and multiplayer examples add RivetKit while still -serving ordinary HTTP requests. - -Run the checked workspace example with Node.js 22 or newer: - -```sh -pnpm --dir examples/apps-hello-world start -# In another terminal: -pnpm --dir examples/apps-hello-world deploy -``` - -Dynamic Apps starts its local Rivet Engine automatically. Against an existing -Rivet deployment, use the standard Rivet connection variables instead. diff --git a/examples/apps-multiplayer/README.md b/examples/apps-multiplayer/README.md deleted file mode 100644 index e85b98055..000000000 --- a/examples/apps-multiplayer/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Dynamic Apps: Multiplayer - -The deployed server defines keyed room actors and an HTTP handler. The separate -`src/client.ts` deploys it, connects with the returned namespace and pool, then -joins and moves through DirectActor. - -Run the checked workspace example with Node.js 22 or newer: - -```sh -pnpm --dir examples/apps-multiplayer start -# In another terminal: -pnpm --dir examples/apps-multiplayer client -``` - -RivetKit starts its local Engine automatically. Against an existing Rivet -deployment, use the standard Rivet connection variables instead. - -The deployed HTTP handler is available at -`http://localhost:3000/apps/multiplayer-room`. diff --git a/examples/apps-sqlite/README.md b/examples/apps-sqlite/README.md deleted file mode 100644 index 2b7f41fca..000000000 --- a/examples/apps-sqlite/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Dynamic Apps: SQLite - -The deployed server defines a Rivet Actor backed by SQLite and still serves -HTTP. The separate `src/client.ts` deploys it, connects with the returned -namespace and pool, and adds and lists notes through DirectActor. - -Run the checked workspace example with Node.js 22 or newer: - -```sh -pnpm --dir examples/apps-sqlite start -# In another terminal: -pnpm --dir examples/apps-sqlite client -``` - -RivetKit starts its local Engine automatically. Against an existing Rivet -deployment, use the standard Rivet connection variables instead. - -The deployed HTTP handler is available at -`http://localhost:3000/apps/sqlite-notes`. diff --git a/examples/apps-static-website/README.md b/examples/apps-static-website/README.md deleted file mode 100644 index 12b9c9bb2..000000000 --- a/examples/apps-static-website/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Dynamic Apps: Static Website - -A directory with `index.html` and no `package.json` is served directly. CSS, -JavaScript, SVG, and other byte assets are included in the immutable release. -A package with a build script is treated as a built static site when it emits -`dist/index.html`. - -Run the checked workspace example with Node.js 22 or newer: - -```sh -pnpm --dir examples/apps-static-website start -# In another terminal: -curl -X POST http://localhost:3000/deploy/static-website -``` - -RivetKit starts its local Engine automatically. Against an existing Rivet -deployment, use the standard Rivet connection variables instead. Open -`http://localhost:3000/apps/static-website/`. diff --git a/examples/apps-workflows/README.md b/examples/apps-workflows/README.md deleted file mode 100644 index ac2134903..000000000 --- a/examples/apps-workflows/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Dynamic Apps: Workflows - -The deployed server defines a keyed `job` actor and an HTTP handler. The -separate `src/client.ts` deploys it, connects with the returned namespace and -pool, and creates a durable workflow through DirectActor. - -Run the checked workspace example with Node.js 22 or newer: - -```sh -pnpm --dir examples/apps-workflows start -# In another terminal: -pnpm --dir examples/apps-workflows client -``` - -RivetKit starts its local Engine automatically. Against an existing Rivet -deployment, use the standard Rivet connection variables instead. The deployed -HTTP handler is available at -`http://localhost:3000/apps/durable-workflow`. diff --git a/packages/dynamic-apps/README.md b/packages/dynamic-apps/README.md deleted file mode 100644 index 914a88da7..000000000 --- a/packages/dynamic-apps/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Dynamic Apps - -Dynamic Apps deploys user-generated JavaScript and static sites into isolated -VMs and routes HTTP through Rivet Actors. - -[Documentation](https://rivet.dev/dynamic-apps/docs) - -Install Dynamic Apps in a Node.js 22 or newer project: - -```sh -npm add @rivet-dev/dynamic-apps -npm add @hono/node-server hono -npm add --save-dev tsx -npm pkg set type=module -``` - -RivetKit starts a local Engine automatically. For an existing Rivet deployment, -use its standard Rivet connection variables and credentials. - -`src/actors.ts`: - -```ts -import { setup, setupApps } from "@rivet-dev/dynamic-apps"; - -const { appsActors } = setupApps(); - -export const registry = setup({ - use: { - ...appsActors, - }, -}); -``` - -`src/server.ts`: - -```ts -import { serve } from "@hono/node-server"; -import { appsRouter } from "@rivet-dev/dynamic-apps"; -import { Hono } from "hono"; -import { registry } from "./actors.js"; - -registry.start(); - -const server = new Hono(); -server.route("/apps", appsRouter); - -serve({ fetch: server.fetch, port: 3000 }); -``` - -`src/deploy.ts`: - -```ts -import { deployApp } from "@rivet-dev/dynamic-apps"; - -await deployApp({ - appId: "hello-world", - files: { - "index.html": "

Hello from Dynamic Apps

", - }, -}); -``` - -The common API has three primary entry points: - -- `setupApps()` returns the three stable internal actor definitions used for - deployments, scaling, and replicas. -- `deployApp({ appId, source | files })` builds and activates an immutable - release. It lazily uses an ordinary RivetKit client. -- `appsRouter` routes `/:appId` and `/:appId/*` to deployed applications. - -It also exports the deployment input, result, scaling, and typed error types. -Supplying a custom ordinary client remains an option on `deployApp()` rather -than a separate Apps client abstraction. - -Submitted source and packed release chunks are stored in the stable app actor's -SQLite database. Serving replicas materialize a verified temporary `.aospkg` -for the VM lifetime and delete it after VM disposal. No durable local artifact -directory is required. - -Dependencies and build tools exist only inside a disposable build VM. The -platform-owned Apps builder emits a minimal release containing bundled -JavaScript, imported WASM modules, and static assets; serving replicas never -install packages and releases do not contain tenant `node_modules`. - -Deployments use the ordinary Rivet connection's configured namespace by -default and do not require namespace-management permission. Set -`createNamespace: true` on `deployApp()` to idempotently create a stable, -isolated namespace for that `appId` within the configured host namespace. Every -deployment returns its stable `pool` along with its `namespace` for ordinary -DirectActor clients. - -The `scaling` options default to `minReplicas: 0`, `maxReplicas: 128`, and -`targetConcurrency: 8`. - -Guest Rivet Actors use the ordinary DirectActor API from `rivetkit/client`. -Dynamic Apps does not export or wrap a RivetKit client. Host management tokens -are never exposed inside guest VMs. Each VM receives an opaque, app-scoped -Engine capability that fixes the namespace and runner pool and rejects -management routes. Rivet Engine callbacks use a random per-app credential that -the trusted app actor validates and strips before forwarding. - -See `examples/apps-hello-world` for the smallest runnable server. diff --git a/packages/dynamic-apps/package.json b/packages/dynamic-apps/package.json index 974f3bde9..665a26b89 100644 --- a/packages/dynamic-apps/package.json +++ b/packages/dynamic-apps/package.json @@ -13,7 +13,6 @@ "files": [ "dist", "assets", - "README.md", "package.json" ], "exports": {