diff --git a/.agents/skills/wrdn-effect-atom-optimistic/SKILL.md b/.agents/skills/wrdn-effect-atom-optimistic/SKILL.md
index 44b6ef488..5f121ccc2 100644
--- a/.agents/skills/wrdn-effect-atom-optimistic/SKILL.md
+++ b/.agents/skills/wrdn-effect-atom-optimistic/SKILL.md
@@ -41,7 +41,7 @@ When the trace cannot resolve with the files at hand, drop the finding.
- `useState` for a "busy" / "submitting" boolean used to disable a button while the mutation runs. That is not optimistic state.
- `setTimeout` / `setInterval` based debouncing or rate-limiting around a mutation. Different concern.
- Toast / error-message state. UI feedback, not optimistic data.
-- Server-only code (`apps/cloud`, `apps/local`, `packages/core/**`). This skill is React-specific; do not flag backend handlers, plugin storage, or test helpers.
+- Server-only code (`legacy/cloud`, `legacy/local`, `packages/core/**`). This skill is React-specific; do not flag backend handlers, plugin storage, or test helpers.
- Storybook files, test files, and example-only code. The pattern matters in shipped UI; not in fixtures.
## Severity ladder
diff --git a/.changeset/README.md b/.changeset/README.md
deleted file mode 100644
index 7c9773cf4..000000000
--- a/.changeset/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Changesets
-
-This repo uses Changesets to drive releases for the published `executor` CLI.
-
-## What to put in a changeset
-
-Only `executor` is managed directly by Changesets.
-
-Release PRs should only mention the published CLI package directly. Changesets
-will still version the fixed release group and dependent public packages as
-needed, and will update each affected package's `CHANGELOG.md`.
-
-Write the changeset body as the package changelog entry you want to appear in
-the Version Packages PR and in the affected package changelogs. Keep broader
-user-facing launch notes in `apps/cli/release-notes/next.md`; those are used for
-the GitHub Release body.
-
-## Beta releases
-
-Use prerelease mode for beta trains:
-
-- `bun run release:beta:start`
-- merge release PRs while prerelease mode is active
-- `bun run release:beta:stop` when you want to return to stable releases
diff --git a/.changeset/config.json b/.changeset/config.json
deleted file mode 100644
index 56e35abe8..000000000
--- a/.changeset/config.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
- "changelog": ["@changesets/changelog-github", { "repo": "RhysSullivan/executor" }],
- "commit": false,
- "fixed": [
- [
- "@executor-js/sdk",
- "@executor-js/codemode-core",
- "@executor-js/runtime-quickjs",
- "@executor-js/execution",
- "@executor-js/config",
- "executor",
- "@executor-js/plugin-file-secrets",
- "@executor-js/plugin-graphql",
- "@executor-js/plugin-keychain",
- "@executor-js/plugin-mcp",
- "@executor-js/plugin-onepassword",
- "@executor-js/plugin-openapi",
- "@executor-js/plugin-example",
- "@executor-js/plugin-desktop-settings",
- "@executor-js/desktop"
- ]
- ],
- "linked": [],
- "access": "public",
- "baseBranch": "main",
- "updateInternalDependencies": "patch",
- "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
- "onlyUpdatePeerDependentsWhenOutOfRange": true
- },
- "ignore": [
- "@executor-js/local",
- "@executor-js/host-mcp",
- "@executor-js/ir",
- "@executor-js/runtime-deno-subprocess",
- "@executor-js/marketing",
- "@executor-js/runtime-dynamic-worker",
- "@executor-js/plugin-workos-vault",
- "@executor-js/example-promise-sdk",
- "@executor-js/app"
- ]
-}
diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml
index a068f80ef..85231bc81 100644
--- a/.codex/environments/environment.toml
+++ b/.codex/environments/environment.toml
@@ -67,9 +67,9 @@ icon = "run"
command = "bun i"
[[actions]]
-name = "Run Local Server"
+name = "Run Legacy Local Server"
icon = "tool"
command = '''
-cd apps/local
+cd legacy/local
bun dev
'''
diff --git a/.dockerignore b/.dockerignore
index 792e93cda..fb1787aca 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -8,3 +8,6 @@
**/.next
**/*.log
.claude
+secrets
+**/secret.key
+**/master.key
diff --git a/.github/ISSUE_TEMPLATE/desktop-crash.yml b/.github/ISSUE_TEMPLATE/desktop-crash.yml
deleted file mode 100644
index 401f9d7e0..000000000
--- a/.github/ISSUE_TEMPLATE/desktop-crash.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Desktop app crash
-description: The Executor desktop app crashes, won't start, or shows a blank window.
-title: "[desktop] "
-labels: ["desktop", "crash"]
-body:
- - type: markdown
- attributes:
- value: |
- Before filing, please try the quick fixes in the pinned
- **[Desktop app crashing? Start here](https://github.com/RhysSullivan/executor/issues?q=is%3Aissue+label%3Acrash-triage)**
- guide — most crashes are fixed by updating to the latest version.
- If you're still stuck, the details below help us pin it down fast.
- - type: input
- id: version
- attributes:
- label: Executor version
- description: Shown in the About menu (or Settings page). Please update to the latest first if you can.
- placeholder: "1.5.4"
- validations:
- required: true
- - type: dropdown
- id: os
- attributes:
- label: Operating system
- options:
- - macOS (Apple Silicon)
- - macOS (Intel)
- - Windows
- - Linux
- validations:
- required: true
- - type: dropdown
- id: when
- attributes:
- label: When does it crash?
- options:
- - On launch — the app never opens
- - Shortly after launch
- - While doing something specific (describe below)
- - Randomly during use
- validations:
- required: true
- - type: textarea
- id: what-happened
- attributes:
- label: What happened
- description: What were you doing when it crashed? Does it happen every time?
- validations:
- required: true
- - type: textarea
- id: logs
- attributes:
- label: Diagnostics / logs
- description: |
- If the app opens far enough to show a menu, open the **Executor** menu → **Export Diagnostics…**
- (or **Report a Problem…**) and attach the zip — it has everything we need and no secrets.
-
- Otherwise, attach the log file directly:
- - macOS: `~/Library/Logs/Executor/main.log`
- - Windows: `%APPDATA%\Executor\logs\main.log`
- - Linux: `~/.config/Executor/logs/main.log`
-
- (Please don't attach anything from `~/.executor` — that's where your data and secrets live.)
- placeholder: Drag the diagnostics zip or main.log here.
- - type: input
- id: run-id
- attributes:
- label: Run ID (optional)
- description: If a crash report was sent, the Run ID from the diagnostics manifest helps us find it.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 937936e3b..5707a2397 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,18 +6,107 @@ on:
branches:
- main
+permissions:
+ contents: read
+
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
+ rust:
+ name: Rust
+ runs-on: ubuntu-22.04
+ timeout-minutes: 30
+ env:
+ EXECUTOR_WEB_ASSETS_DIR: tests/fixtures/web-assets
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Install Rust toolchain
+ shell: bash
+ run: |
+ set -euo pipefail
+ rustup toolchain install 1.96.0 \
+ --profile minimal \
+ --component clippy,rustfmt
+ rustup override set 1.96.0
+
+ - name: Cache Cargo
+ uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 # v2.8.2
+ with:
+ cache-on-failure: false
+ shared-key: rust-1.96.0
+
+ - name: Check formatting
+ run: cargo fmt --all -- --check
+
+ - name: Check all targets and features
+ run: cargo check --locked --all-targets --all-features
+
+ - name: Lint all targets and features
+ run: cargo clippy --locked --all-targets --all-features -- -D warnings
+
+ - name: Set up Bun for emulator-backed Rust tests
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
+ with:
+ bun-version: 1.3.11
+
+ - name: Install locked emulator dependency
+ run: bun install --frozen-lockfile --ignore-scripts
+
+ - name: Verify OAuth test emulator
+ run: test -x e2e/node_modules/.bin/emulate
+
+ - name: Test all targets and features
+ run: cargo test --locked --all-targets --all-features
+
+ web:
+ name: Svelte web
+ runs-on: ubuntu-22.04
+ timeout-minutes: 20
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
+ with:
+ bun-version: 1.3.11
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Check formatting
+ run: bun run format:check
+ working-directory: web
+
+ - name: Lint
+ run: bun run lint
+ working-directory: web
+
+ - name: Typecheck
+ run: bun run typecheck
+ working-directory: web
+
+ - name: Test
+ run: bun run test
+ working-directory: web
+
format:
name: Format
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- - uses: oven-sh/setup-bun@v2
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
@@ -29,9 +118,11 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- - uses: oven-sh/setup-bun@v2
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
@@ -43,9 +134,11 @@ jobs:
name: Typecheck
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- - uses: oven-sh/setup-bun@v2
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
@@ -57,61 +150,135 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
-
- - uses: oven-sh/setup-bun@v2
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- bun-version: 1.3.11
+ persist-credentials: false
- # apps/cloud's test script invokes `node` directly; undici 8.x (pulled
- # in by @cloudflare/vitest-pool-workers) calls webidl.markAsUncloneable
- # which only exists in Node 22.10+. Pin a known-good runtime.
- - uses: actions/setup-node@v4
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
- node-version: 22
+ bun-version: 1.3.11
- run: bun install --frozen-lockfile
- run: bun run test
- desktop-smoke:
- name: Desktop smoke build
- runs-on: ubuntu-latest
+ - name: Test release workflow and Docker contracts
+ run: bun run test:release:static
+
+ - name: Test release archive installer
+ run: bun run test:release:installer
+
+ - name: Test systemd master-key lifecycle
+ run: bun run test:release:systemd
+
+ - name: Check release shell scripts
+ run: bun run test:release:shell
+
+ local-selfhost-e2e:
+ name: Local self-host e2e
+ runs-on: ubuntu-22.04
+ timeout-minutes: 45
steps:
- - uses: actions/checkout@v4
+ - name: Checkout source
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- - uses: oven-sh/setup-bun@v2
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
- - run: bun install --frozen-lockfile
+ - name: Install Rust toolchain
+ shell: bash
+ run: |
+ set -euo pipefail
+ rustup toolchain install 1.96.0 --profile minimal
+ rustup override set 1.96.0
+
+ - name: Cache Cargo
+ uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 # v2.8.2
+ with:
+ cache-on-failure: false
+ shared-key: rust-1.96.0-local-selfhost-e2e
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
- - name: Build web app
- run: bun run --filter @executor-js/local build
+ - name: Install Chromium
+ run: bun run --cwd e2e playwright install --with-deps chromium
- - name: Build bundled executor
- env:
- BUN_TARGET: bun-linux-x64
- run: bun ./scripts/build-sidecar.ts
- working-directory: apps/desktop
+ - name: Run local self-host e2e
+ run: bun run test:e2e
- - name: Build Electron main/preload/renderer
- run: bunx --bun electron-vite build
- working-directory: apps/desktop
+ - name: Upload e2e artifacts
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: local-selfhost-e2e
+ path: e2e/runs/
+ if-no-files-found: error
+ retention-days: 14
selfhost-docker-smoke:
- name: Self-host Docker image
- runs-on: ubuntu-latest
+ name: Rust self-host Docker image
+ runs-on: ubuntu-22.04
+ timeout-minutes: 45
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build self-host image
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
- file: apps/host-selfhost/Dockerfile
+ file: Dockerfile
+ load: true
push: false
tags: executor-selfhost:ci
+
+ - name: Smoke test container health and shutdown
+ shell: bash
+ run: |
+ set -euo pipefail
+ docker volume create executor-ci-data >/dev/null
+ docker run --detach \
+ --name executor-ci \
+ --init \
+ --read-only \
+ --cap-drop ALL \
+ --security-opt no-new-privileges \
+ --memory 2g \
+ --pids-limit 512 \
+ --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
+ --mount source=executor-ci-data,target=/var/lib/executor \
+ executor-selfhost:ci >/dev/null
+
+ status="starting"
+ for _ in {1..60}; do
+ status="$(docker inspect --format '{{.State.Health.Status}}' executor-ci)"
+ if [[ "$status" == "healthy" ]]; then
+ break
+ fi
+ if [[ "$(docker inspect --format '{{.State.Status}}' executor-ci)" == "exited" ]]; then
+ break
+ fi
+ sleep 1
+ done
+ if [[ "$status" != "healthy" ]]; then
+ docker logs executor-ci
+ exit 1
+ fi
+
+ docker stop --time 120 executor-ci >/dev/null
+ test "$(docker inspect --format '{{.State.ExitCode}}' executor-ci)" = "0"
+
+ - name: Clean up container smoke test
+ if: always()
+ run: |
+ docker rm --force executor-ci >/dev/null 2>&1 || true
+ docker volume rm --force executor-ci-data >/dev/null 2>&1 || true
diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml
deleted file mode 100644
index cb9cf0544..000000000
--- a/.github/workflows/pkg-pr-new.yml
+++ /dev/null
@@ -1,117 +0,0 @@
-name: Publish (pkg.pr.new)
-
-on:
- pull_request:
-
-permissions:
- contents: read
-
-jobs:
- # Per-platform matrix: build the executor binary, tar it, upload to R2.
- # The wrapper npm package is built later by the `publish` job which just
- # needs to know which platforms made it this far.
- build-preview-binary:
- name: Build preview binary (${{ matrix.target }})
- # Fork PRs don't receive repo secrets, so the R2 upload step can't
- # authenticate. Skip the whole preview pipeline (publish `needs:` this
- # job and cascades to skipped) rather than failing the check.
- if: github.event.pull_request.head.repo.full_name == github.repository
- strategy:
- fail-fast: false
- matrix:
- include:
- - runner: ubuntu-latest
- target: executor-linux-x64
- - runner: macos-14
- target: executor-darwin-arm64
- runs-on: ${{ matrix.runner }}
- steps:
- - uses: actions/checkout@v4
- with:
- persist-credentials: false
-
- - uses: oven-sh/setup-bun@v2
- with:
- bun-version: 1.3.11
-
- - run: bun install --frozen-lockfile
-
- - name: Build executor preview tarball
- env:
- EXECUTOR_PREVIEW_CDN_URL: ${{ vars.EXECUTOR_PREVIEW_CDN_URL }}
- EXECUTOR_PREVIEW_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
- run: bun run --filter=executor build:preview:tarball
-
- - name: Upload preview binary to R2
- # curl --aws-sigv4 handles large uploads directly against R2's S3
- # endpoint. Simpler than aws-cli (endpoint validation quirks) or
- # Bun.s3 (ConnectionRefused on ubuntu-latest for reasons unclear).
- env:
- AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
- R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
- SHA: ${{ github.event.pull_request.head.sha || github.sha }}
- TARGET: ${{ matrix.target }}
- run: |
- tarball="apps/cli/dist/previews/${TARGET}.tar.gz"
- echo "Uploading ${TARGET}.tar.gz → r2://executor-previews/$SHA/"
- curl --fail-with-body --silent --show-error \
- -X PUT "$R2_ENDPOINT/executor-previews/$SHA/${TARGET}.tar.gz" \
- --upload-file "$tarball" \
- --aws-sigv4 "aws:amz:auto:s3" \
- --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
-
- publish:
- name: Publish
- needs: build-preview-binary
- runs-on: ubuntu-latest
- permissions:
- contents: read
- pull-requests: write
- steps:
- - uses: actions/checkout@v4
- with:
- persist-credentials: false
-
- - uses: oven-sh/setup-bun@v2
- with:
- bun-version: 1.3.11
-
- - run: bun install --frozen-lockfile
-
- - name: Build executor preview wrapper
- env:
- EXECUTOR_PREVIEW_CDN_URL: ${{ vars.EXECUTOR_PREVIEW_CDN_URL }}
- EXECUTOR_PREVIEW_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
- EXECUTOR_PREVIEW_TARGETS: executor-linux-x64,executor-darwin-arm64
- run: bun run --filter=executor build:preview:wrapper
-
- # Apply the same `publishConfig.exports` promotion and `workspace:*`
- # resolution the real npm release does. Runs `build:packages`
- # internally. Without this, pkg-pr-new ships unresolved
- # `workspace:*` references and the dev-time `src/index.ts` exports.
- - run: bun run release:publish:packages:prepare
-
- # Explicit list (not `plugins/*`) — must match PUBLIC_PACKAGE_DIRS
- # in scripts/publish-packages.ts. Globbing would pick up unprepared
- # private packages that would publish with broken refs.
- #
- # No --compact: it requires every package already exist on npm to
- # generate short URLs. New packages aren't published yet, and
- # --compact aborts the whole run when it can't resolve them.
- - run: >
- npx pkg-pr-new publish --bun
- './apps/cli/dist/executor'
- './packages/core/storage-core'
- './packages/core/sdk'
- './packages/core/config'
- './packages/core/execution'
- './packages/core/cli'
- './packages/kernel/core'
- './packages/kernel/runtime-quickjs'
- './packages/plugins/file-secrets'
- './packages/plugins/graphql'
- './packages/plugins/keychain'
- './packages/plugins/mcp'
- './packages/plugins/onepassword'
- './packages/plugins/openapi'
diff --git a/.github/workflows/preview-sweep.yml b/.github/workflows/preview-sweep.yml
index 977ba31c9..b194e238c 100644
--- a/.github/workflows/preview-sweep.yml
+++ b/.github/workflows/preview-sweep.yml
@@ -6,6 +6,7 @@ name: Preview sweep
# open. A second pass (sweep-e2e) cleans up after the e2e harness on the same
# account: crashed suite runs leak executor-e2e-* stacks, and synthetic OTP
# logins permanently occupy Zero Trust seats.
+# The repository variable ENABLE_CLOUDFLARE_PREVIEWS must be exactly "true".
on:
schedule:
@@ -19,11 +20,14 @@ permissions:
jobs:
sweep:
name: Destroy previews for closed PRs
+ if: vars.ENABLE_CLOUDFLARE_PREVIEWS == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- - uses: oven-sh/setup-bun@v2
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
@@ -34,13 +38,13 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
- for pr in $(bun apps/host-cloudflare/scripts/preview.ts list | jq -r '.[].pr'); do
+ for pr in $(bun legacy/host-cloudflare/scripts/preview.ts list | jq -r '.[].pr'); do
state="$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json state -q .state 2>/dev/null || echo UNKNOWN)"
if [ "$state" != "OPEN" ]; then
- echo "PR #$pr is $state — destroying its preview"
- bun apps/host-cloudflare/scripts/preview.ts destroy --pr "$pr"
+ echo "PR #$pr is $state, destroying its preview"
+ bun legacy/host-cloudflare/scripts/preview.ts destroy --pr "$pr"
else
- echo "PR #$pr is open — keeping its preview"
+ echo "PR #$pr is open, keeping its preview"
fi
done
@@ -48,4 +52,4 @@ jobs:
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_PREVIEW_ACCOUNT_ID }}
- run: bun apps/host-cloudflare/scripts/preview.ts sweep-e2e
+ run: bun legacy/host-cloudflare/scripts/preview.ts sweep-e2e
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index 3d3bef13f..87e76df4a 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -3,13 +3,14 @@ name: Preview
# Per-PR preview deploys of the Cloudflare host to a dedicated preview account.
# Every same-repo PR gets its own isolated stack (Worker + D1 + Access app)
# behind Cloudflare Access; it is torn down when the PR closes. Fork PRs are
-# skipped — they must not run with the deploy token.
+# skipped because they must not run with the deploy token. The repository
+# variable ENABLE_CLOUDFLARE_PREVIEWS must also be exactly "true".
#
# Required repo configuration:
-# secret CLOUDFLARE_PREVIEW_API_TOKEN — scoped token (Workers/D1/R2/Access edit)
-# var CLOUDFLARE_PREVIEW_ACCOUNT_ID — the preview Cloudflare account
-# var PREVIEW_ACCESS_TEAM_DOMAIN — Zero Trust team domain
-# var PREVIEW_ACCESS_EMAILS — comma-separated emails allowed through Access
+# secret CLOUDFLARE_PREVIEW_API_TOKEN: scoped token (Workers/D1/R2/Access edit)
+# var CLOUDFLARE_PREVIEW_ACCOUNT_ID: the preview Cloudflare account
+# var PREVIEW_ACCESS_TEAM_DOMAIN: Zero Trust team domain
+# var PREVIEW_ACCESS_EMAILS: comma-separated emails allowed through Access
on:
pull_request:
@@ -17,7 +18,6 @@ on:
permissions:
contents: read
- pull-requests: write
concurrency:
group: preview-${{ github.event.pull_request.number }}
@@ -26,12 +26,24 @@ concurrency:
jobs:
deploy:
name: Deploy preview
- if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
+ if: vars.ENABLE_CLOUDFLARE_PREVIEWS == 'true' && github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- - uses: actions/checkout@v4
+ - id: checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ persist-credentials: false
+
+ - name: Verify preview commit
+ env:
+ EXPECTED_SHA: ${{ github.event.pull_request.head.sha }}
+ run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
- - uses: oven-sh/setup-bun@v2
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
@@ -39,7 +51,7 @@ jobs:
- name: Deploy
id: deploy
- working-directory: apps/host-cloudflare
+ working-directory: legacy/host-cloudflare
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_PREVIEW_ACCOUNT_ID }}
@@ -48,9 +60,10 @@ jobs:
run: bun scripts/preview.ts deploy --pr ${{ github.event.pull_request.number }}
- name: Comment preview URL
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
PREVIEW_URL: ${{ steps.deploy.outputs.url }}
+ PREVIEW_SHA: ${{ steps.checkout.outputs.commit }}
with:
script: |
const marker = "";
@@ -62,10 +75,10 @@ jobs:
`|---|---|`,
`| Console | ${process.env.PREVIEW_URL} |`,
`| MCP | \`${process.env.PREVIEW_URL}/mcp\` |`,
- `| Deployed commit | ${context.payload.pull_request.head.sha} |`,
+ `| Deployed commit | ${process.env.PREVIEW_SHA} |`,
"",
"Sign-in is Cloudflare Access (one-time PIN to an allowed email). " +
- "The preview has its own database and encryption key; it is destroyed when this PR closes.",
+ "The preview has its own database and encryption key. It is destroyed when this PR closes.",
].join("\n");
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
@@ -81,24 +94,35 @@ jobs:
teardown:
name: Tear down preview
- if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository
+ if: vars.ENABLE_CLOUDFLARE_PREVIEWS == 'true' && github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ persist-credentials: false
+
+ - name: Verify preview commit
+ env:
+ EXPECTED_SHA: ${{ github.event.pull_request.head.sha }}
+ run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
- - uses: oven-sh/setup-bun@v2
+ - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: 1.3.11
- # destroy talks straight to the Cloudflare API — no install needed.
+ # Destroy talks straight to the Cloudflare API, so no install is needed.
- name: Destroy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_PREVIEW_ACCOUNT_ID }}
- run: bun apps/host-cloudflare/scripts/preview.ts destroy --pr ${{ github.event.pull_request.number }}
+ run: bun legacy/host-cloudflare/scripts/preview.ts destroy --pr ${{ github.event.pull_request.number }}
- name: Mark comment as torn down
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const marker = "";
@@ -112,6 +136,6 @@ jobs:
await github.rest.issues.updateComment({
...context.repo,
comment_id: existing.id,
- body: `${marker}\n### Cloudflare preview\n\nTorn down — the PR is closed.`,
+ body: `${marker}\n### Cloudflare preview\n\nTorn down because the PR is closed.`,
});
}
diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml
deleted file mode 100644
index cad7d4a30..000000000
--- a/.github/workflows/publish-desktop.yml
+++ /dev/null
@@ -1,233 +0,0 @@
-name: Publish Desktop App
-run-name: "${{ format('publish desktop {0}', github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name) }}"
-
-# Triggered manually or by publish-executor-package.yml after a CLI release
-# lands. Builds Electron distributables for mac/win/linux with the compiled
-# executor CLI bundled in `resources/executor/`, then attaches them to the GitHub
-# release matching the tag so electron-updater can pick them up.
-
-on:
- workflow_dispatch:
- inputs:
- tag:
- description: Git tag to publish (e.g. v1.4.1)
- required: true
- type: string
-
-permissions:
- contents: read
-
-concurrency:
- group: publish-desktop-${{ github.ref }}
- cancel-in-progress: false
-
-jobs:
- build:
- permissions:
- contents: read
- strategy:
- fail-fast: false
- matrix:
- include:
- # smoke: run the compiled-sidecar smoke test on legs whose target
- # matches the runner (the mac x64 leg cross-compiles on an arm64
- # runner, so its binary can't be executed natively there).
- - os: macos-latest
- arch: arm64
- platform: mac
- bun-target: bun-darwin-arm64
- smoke: true
- - os: macos-latest
- arch: x64
- platform: mac
- bun-target: bun-darwin-x64
- smoke: false
- - os: ubuntu-latest
- arch: x64
- platform: linux
- bun-target: bun-linux-x64
- smoke: true
- - os: windows-latest
- arch: x64
- platform: win
- bun-target: bun-windows-x64
- smoke: true
-
- runs-on: ${{ matrix.os }}
-
- # A healthy leg finishes in ~10 minutes. Without a deadline, a hung step
- # wedges the whole publish queue: the concurrency group below queues later
- # runs behind this one without cancelling it (v1.5.7's mac leg sat 4 hours
- # on a bun install that errored without exiting, holding v1.5.8 pending).
- timeout-minutes: 45
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- persist-credentials: false
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@v2
- with:
- bun-version: 1.3.11
-
- - name: Validate release tag
- env:
- RAW_RELEASE_TAG: ${{ inputs.tag }}
- run: bun run scripts/validate-release-ref.ts --tag-env RAW_RELEASE_TAG --write-env RELEASE_TAG
-
- - name: Checkout release tag
- env:
- GH_TOKEN: ${{ secrets.RELEASE_PAT || github.token }}
- shell: bash
- run: |
- auth_remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
- git fetch --force --tags "$auth_remote" "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
- git checkout --detach "$RELEASE_TAG"
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 24
-
- - name: Install dependencies
- run: bun install --frozen-lockfile
-
- - name: Build web app
- run: bun run --filter @executor-js/local build
-
- - name: Build bundled executor
- env:
- BUN_TARGET: ${{ matrix.bun-target }}
- run: bun ./scripts/build-sidecar.ts
- working-directory: apps/desktop
-
- # Gate the release on the compiled binary actually booting. v1.5.0/.1
- # shipped local-server binaries that died on launch (missing libsql native
- # binding) — a regression dev mode can't catch because `bun run` resolves
- # node_modules that `bun build --compile` does not bundle.
- - name: Smoke test bundled executor
- if: matrix.smoke
- run: bun run test:smoke
- working-directory: apps/desktop
-
- - name: Build Electron main/preload/renderer
- env:
- # Crash-report DSN baked into the main bundle (see the define in
- # electron.vite.config.ts). Unset (forks, local) → crash reporting
- # is compiled out and dumps stay local.
- DESKTOP_SENTRY_DSN: ${{ vars.DESKTOP_SENTRY_DSN }}
- run: bunx --bun electron-vite build
- working-directory: apps/desktop
-
- - name: Stage Apple API key (mac signing + notarization)
- if: matrix.platform == 'mac' && env.APPLE_API_KEY != ''
- env:
- APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
- run: |
- mkdir -p "${RUNNER_TEMP}/private_keys"
- printf '%s' "$APPLE_API_KEY" > "${RUNNER_TEMP}/private_keys/AuthKey.p8"
- echo "APPLE_API_KEY_PATH=${RUNNER_TEMP}/private_keys/AuthKey.p8" >> "$GITHUB_ENV"
-
- - name: Build desktop distributables
- env:
- # electron-builder reads GH_TOKEN for the publish step. We use
- # --publish never here and attach assets explicitly in the release
- # job so we don't fight electron-updater's metadata expectations.
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Apple signing + notarization. electron-builder picks these up:
- # CSC_LINK / CSC_KEY_PASSWORD → import the Developer ID Application
- # cert (.p12, base64) into a temp keychain for codesign
- # APPLE_API_KEY (file path) / APPLE_API_KEY_ID / APPLE_API_ISSUER
- # → notarytool authentication for the notarization upload
- # When the secrets aren't set (forks, local), electron-builder
- # silently produces an unsigned build.
- CSC_LINK: ${{ secrets.CSC_LINK }}
- CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
- APPLE_API_KEY: ${{ env.APPLE_API_KEY_PATH }}
- APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
- APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
- run: bunx --bun electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish never --config electron-builder.config.ts
- working-directory: apps/desktop
-
- # The two mac legs each emit a latest-mac.yml listing only their own
- # arch. Rename per-arch here; the release job merges them back into the
- # single latest-mac.yml electron-updater clients fetch. Without this,
- # merge-multiple in the release job lets one arch clobber the other and
- # every Mac gets pointed at whichever leg uploaded last.
- - name: Rename mac update manifest per arch
- if: matrix.platform == 'mac'
- shell: bash
- run: mv apps/desktop/dist/latest-mac.yml "apps/desktop/dist/latest-mac-${{ matrix.arch }}.yml"
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v4
- with:
- name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
- path: |
- apps/desktop/dist/*.dmg
- apps/desktop/dist/*.zip
- apps/desktop/dist/*.exe
- apps/desktop/dist/*.AppImage
- apps/desktop/dist/*.deb
- apps/desktop/dist/*.rpm
- apps/desktop/dist/latest*.yml
- if-no-files-found: warn
-
- release:
- needs: build
- runs-on: ubuntu-latest
- permissions:
- contents: write
-
- steps:
- - name: Checkout validation script
- uses: actions/checkout@v4
- with:
- persist-credentials: false
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@v2
- with:
- bun-version: 1.3.11
-
- - name: Validate release tag
- env:
- RAW_RELEASE_TAG: ${{ inputs.tag }}
- run: bun run scripts/validate-release-ref.ts --tag-env RAW_RELEASE_TAG --write-env RELEASE_TAG
-
- - name: Download all artifacts
- uses: actions/download-artifact@v4
- with:
- path: artifacts
- merge-multiple: true
-
- - name: Merge mac update manifests
- run: |
- set -euo pipefail
- bun scripts/merge-latest-mac-yml.ts \
- artifacts/latest-mac-x64.yml \
- artifacts/latest-mac-arm64.yml \
- artifacts/latest-mac.yml
- rm artifacts/latest-mac-x64.yml artifacts/latest-mac-arm64.yml
-
- - name: Upload to GitHub Release
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- set -euo pipefail
- while IFS= read -r file; do
- echo "Uploading: $file"
- gh release upload "$RELEASE_TAG" "$file" --repo "$GITHUB_REPOSITORY" --clobber
- done < <(find artifacts -type f \
- \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" \
- -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \
- -o -name "latest*.yml" \))
-
- # Flip draft → published only after every desktop asset is uploaded —
- # this is the atomic point where the new tag becomes "latest".
- - name: Promote release (draft → published)
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: gh release edit "$RELEASE_TAG" --draft=false --repo "$GITHUB_REPOSITORY"
diff --git a/.github/workflows/publish-executor-package.yml b/.github/workflows/publish-executor-package.yml
deleted file mode 100644
index 113ed9411..000000000
--- a/.github/workflows/publish-executor-package.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-name: Publish Executor
-run-name: "${{ format('publish executor {0}', github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name) }}"
-
-on:
- push:
- tags:
- - "v*"
- workflow_dispatch:
- inputs:
- tag:
- description: Git tag to publish
- required: true
- type: string
-
-permissions:
- contents: read
-
-concurrency:
- group: publish-executor-package-${{ github.ref }}
- cancel-in-progress: false
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- permissions:
- actions: write
- contents: write
- id-token: write
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- persist-credentials: false
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@v2
- with:
- bun-version: 1.3.11
-
- - name: Validate release tag
- env:
- RAW_RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
- run: bun run scripts/validate-release-ref.ts --tag-env RAW_RELEASE_TAG --write-env RELEASE_TAG
-
- - name: Checkout release tag
- env:
- GH_TOKEN: ${{ secrets.RELEASE_PAT || github.token }}
- run: |
- auth_remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
- git fetch --force --tags "$auth_remote" "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
- git checkout --detach "$RELEASE_TAG"
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 24
- registry-url: https://registry.npmjs.org
-
- - name: Update npm for trusted publishing
- run: |
- npm install -g npm@latest
- npm --version
-
- - name: Install dependencies
- run: bun install --frozen-lockfile
-
- - name: Run release checks
- run: bun run release:check
-
- - name: Publish package and create release
- env:
- GH_TOKEN: ${{ github.token }}
- run: |
- export GITHUB_REF_TYPE=tag
- export GITHUB_REF_NAME="$RELEASE_TAG"
- export GITHUB_REF="refs/tags/$RELEASE_TAG"
- bun run release:publish
-
- - name: Trigger desktop build
- env:
- GH_TOKEN: ${{ github.token }}
- run: gh workflow run publish-desktop.yml -f tag="$RELEASE_TAG"
-
- - name: Trigger self-host Docker publish
- env:
- GH_TOKEN: ${{ github.token }}
- run: gh workflow run publish-selfhost-docker.yml -f tag="$RELEASE_TAG"
diff --git a/.github/workflows/publish-selfhost-docker.yml b/.github/workflows/publish-selfhost-docker.yml
deleted file mode 100644
index ac54fb799..000000000
--- a/.github/workflows/publish-selfhost-docker.yml
+++ /dev/null
@@ -1,107 +0,0 @@
-name: Publish Self-host Docker Image
-run-name: "${{ format('publish self-host docker {0}', inputs.tag) }}"
-
-on:
- workflow_dispatch:
- inputs:
- tag:
- description: Git tag to publish (e.g. v1.5.0)
- required: true
- type: string
-
-permissions:
- contents: read
-
-concurrency:
- group: publish-selfhost-docker-${{ inputs.tag }}
- cancel-in-progress: false
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- persist-credentials: false
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@v2
- with:
- bun-version: 1.3.11
-
- - name: Validate release tag
- env:
- RAW_RELEASE_TAG: ${{ inputs.tag }}
- run: bun run scripts/validate-release-ref.ts --tag-env RAW_RELEASE_TAG --write-env RELEASE_TAG
-
- - name: Checkout release tag
- env:
- GH_TOKEN: ${{ secrets.RELEASE_PAT || github.token }}
- run: |
- auth_remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
- git fetch --force --tags "$auth_remote" "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
- git checkout --detach "$RELEASE_TAG"
-
- - name: Resolve image metadata
- id: image
- shell: bash
- run: |
- set -euo pipefail
-
- version="${RELEASE_TAG#v}"
- owner="$(printf '%s' "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')"
- image="ghcr.io/${owner}/executor-selfhost"
- revision="$(git rev-parse HEAD)"
-
- if [[ "$version" == *-* ]]; then
- channel="beta"
- else
- channel="latest"
- fi
-
- {
- echo "image=$image"
- echo "version=$version"
- echo "channel=$channel"
- echo "tags<> "$GITHUB_OUTPUT"
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Log in to GHCR
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Build and push self-host image
- uses: docker/build-push-action@v6
- with:
- context: .
- file: apps/host-selfhost/Dockerfile
- platforms: linux/amd64,linux/arm64
- push: true
- tags: ${{ steps.image.outputs.tags }}
- labels: ${{ steps.image.outputs.labels }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c51c05ea2..404e5fc19 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,137 +1,2084 @@
-name: Release
-run-name: prepare release
+name: Release native Executor
+run-name: "${{ format('{0} {1} at {2}', inputs.mode, inputs.tag, inputs.commit_sha) }}"
on:
- push:
- branches:
- - main
+ workflow_dispatch:
+ inputs:
+ mode:
+ description: Build only, or publish and promote the release
+ required: true
+ default: dry-run
+ type: choice
+ options:
+ - dry-run
+ - publish
+ tag:
+ description: Exact v-prefixed Cargo version, for example v0.1.0
+ required: true
+ type: string
+ commit_sha:
+ description: Exact 40-character commit SHA to build
+ required: true
+ type: string
permissions:
contents: read
concurrency:
- group: release-${{ github.ref }}
+ group: native-release
cancel-in-progress: false
+env:
+ BUILDKIT_IMAGE: moby/buildkit:v0.30.0@sha256:0168606be2315b7c807a03b3d8aa79beefdb31c98740cebdffdfeebf31190c9f
+ BUILDX_VERSION: v0.34.1
+ BUN_VERSION: 1.3.11
+ EXECUTOR_REPOSITORY: ${{ github.repository }}
+ MACOS_BUILD_CLANG_VERSION: Apple clang version 17.0.0 (clang-1700.0.13.5)
+ MACOS_BUILD_DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
+ MACOS_BUILD_LD_VERSION: "1167.5"
+ MACOS_BUILD_SDK_VERSION: "15.5"
+ MACOS_BUILD_XCODE_BUILD: 16F6
+ MACOS_BUILD_XCODE_VERSION: "16.4"
+ MACOS_FLOOR_CLANG_VERSION: Apple clang version 15.0.0 (clang-1500.3.9.4)
+ MACOS_FLOOR_DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
+ MACOS_FLOOR_LD_VERSION: "1053.12"
+ MACOS_FLOOR_SDK_VERSION: "14.5"
+ MACOS_FLOOR_XCODE_BUILD: 15F31d
+ MACOS_FLOOR_XCODE_VERSION: "15.4"
+ MACOSX_DEPLOYMENT_TARGET: "14.0"
+ RUST_VERSION: 1.96.0
+
jobs:
- release:
- runs-on: ubuntu-latest
+ validate:
+ name: Validate immutable release inputs
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ outputs:
+ channel: ${{ steps.release.outputs.channel }}
+ commit_sha: ${{ steps.release.outputs.commit_sha }}
+ image: ${{ steps.release.outputs.image }}
+ prerelease: ${{ steps.release.outputs.prerelease }}
+ source_date_epoch: ${{ steps.release.outputs.source_date_epoch }}
+ version: ${{ steps.release.outputs.version }}
+ steps:
+ - name: Verify exact native release environment policy
+ if: inputs.mode == 'publish'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ environment_file="$RUNNER_TEMP/native-release-environment.json"
+ policies_file="$RUNNER_TEMP/native-release-branch-policies.json"
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "repos/$EXECUTOR_REPOSITORY/environments/native-release" \
+ > "$environment_file"
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "repos/$EXECUTOR_REPOSITORY/environments/native-release/deployment-branch-policies?per_page=100" \
+ > "$policies_file"
+
+ python3 - "$environment_file" "$policies_file" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ environment = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+ policies = json.loads(pathlib.Path(sys.argv[2]).read_text(encoding="utf-8"))
+
+ if environment.get("name") != "native-release":
+ raise SystemExit("native-release environment name does not match")
+ if environment.get("can_admins_bypass") is not False:
+ raise SystemExit("native-release must disable administrator bypass")
+
+ protection_rules = environment.get("protection_rules")
+ if not isinstance(protection_rules, list):
+ raise SystemExit("native-release protection rules are missing")
+ rule_types = sorted(rule.get("type") for rule in protection_rules)
+ if rule_types != ["branch_policy", "required_reviewers"]:
+ raise SystemExit(
+ "native-release must have only branch_policy and required_reviewers rules"
+ )
+
+ reviewer_rule = next(
+ rule for rule in protection_rules if rule.get("type") == "required_reviewers"
+ )
+ if reviewer_rule.get("prevent_self_review") is not False:
+ raise SystemExit("native-release must set prevent_self_review=false")
+ reviewers = reviewer_rule.get("reviewers")
+ if not isinstance(reviewers, list) or len(reviewers) != 1:
+ raise SystemExit("native-release must have exactly one required reviewer")
+ reviewer = reviewers[0]
+ identity = reviewer.get("reviewer")
+ if (
+ reviewer.get("type") != "User"
+ or not isinstance(identity, dict)
+ or identity.get("login") != "bmdavis419"
+ or identity.get("id") != 45952064
+ ):
+ raise SystemExit("native-release required reviewer must be bmdavis419 (45952064)")
+
+ deployment_policy = environment.get("deployment_branch_policy")
+ if deployment_policy != {
+ "protected_branches": False,
+ "custom_branch_policies": True,
+ }:
+ raise SystemExit("native-release must use custom deployment branch policies only")
+
+ branch_policies = policies.get("branch_policies")
+ if (
+ policies.get("total_count") != 1
+ or not isinstance(branch_policies, list)
+ or len(branch_policies) != 1
+ or branch_policies[0].get("name") != "main"
+ or branch_policies[0].get("type") != "branch"
+ ):
+ raise SystemExit("native-release must allow only the main branch")
+ PY
+
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Bind version, commit, tag, and release state
+ id: release
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ INPUT_COMMIT_SHA: ${{ inputs.commit_sha }}
+ RELEASE_MODE: ${{ inputs.mode }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ RUN_ATTEMPT: ${{ github.run_attempt }}
+ RUN_ID: ${{ github.run_id }}
+ WORKFLOW_REF: ${{ github.ref }}
+ WORKFLOW_SHA: ${{ github.sha }}
+ run: |
+ set -euo pipefail
+
+ if [[ ! "$INPUT_COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then
+ echo "commit_sha must be an exact lowercase 40-character Git commit SHA" >&2
+ exit 1
+ fi
+ if [ "$WORKFLOW_SHA" != "$INPUT_COMMIT_SHA" ]; then
+ echo "Workflow revision $WORKFLOW_SHA does not match requested release commit $INPUT_COMMIT_SHA" >&2
+ exit 1
+ fi
+ if [[ ! "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Invalid workflow run attempt: $RUN_ATTEMPT" >&2
+ exit 1
+ fi
+
+ actual_sha="$(git rev-parse HEAD)"
+ if [ "$actual_sha" != "$INPUT_COMMIT_SHA" ]; then
+ echo "Checked out $actual_sha, expected $INPUT_COMMIT_SHA" >&2
+ exit 1
+ fi
+
+ git fetch --no-tags origin "+refs/heads/main:refs/remotes/origin/main"
+ if ! git merge-base --is-ancestor "$actual_sha" refs/remotes/origin/main; then
+ echo "Release commit $actual_sha is not on origin/main" >&2
+ exit 1
+ fi
+ if [ "$RUN_ATTEMPT" = 1 ] \
+ && [ "$(git rev-parse refs/remotes/origin/main)" != "$actual_sha" ]; then
+ echo "The first release attempt must use the current origin/main commit" >&2
+ exit 1
+ fi
+
+ version="$(python3 - <<'PY'
+ import pathlib
+ import tomllib
+
+ manifest = tomllib.loads(pathlib.Path("Cargo.toml").read_text(encoding="utf-8"))
+ print(manifest["package"]["version"])
+ PY
+ )"
+ python3 - "$version" <<'PY'
+ import re
+ import sys
+
+ pattern = re.compile(
+ r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
+ r"(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)"
+ r"(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?"
+ r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
+ )
+ if pattern.fullmatch(sys.argv[1]) is None:
+ raise SystemExit("Cargo.toml package version is not valid SemVer")
+ PY
+ if [ "$RELEASE_TAG" != "v$version" ]; then
+ echo "Release tag $RELEASE_TAG does not match Cargo.toml version $version" >&2
+ exit 1
+ fi
+ if [[ "$version" == *+* ]]; then
+ echo "Cargo release versions cannot contain build metadata because OCI tags do not support +" >&2
+ exit 1
+ fi
+
+ if git fetch --no-tags origin "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"; then
+ tag_sha="$(git rev-parse "$RELEASE_TAG^{commit}")"
+ if [ "$tag_sha" != "$actual_sha" ]; then
+ echo "Release tag $RELEASE_TAG points to $tag_sha, expected $actual_sha" >&2
+ exit 1
+ fi
+ elif [ "$RELEASE_MODE" = "publish" ]; then
+ echo "Publish mode requires existing remote tag $RELEASE_TAG" >&2
+ exit 1
+ else
+ echo "Dry run continues without remote tag $RELEASE_TAG"
+ fi
+ if [ "$RELEASE_MODE" = "publish" ] \
+ && [ "$WORKFLOW_REF" != refs/heads/main ]; then
+ echo "Publish mode must be dispatched from refs/heads/main, got $WORKFLOW_REF" >&2
+ exit 1
+ fi
+
+ if [ "$RELEASE_MODE" = "publish" ] \
+ && gh release view "$RELEASE_TAG" --repo "$EXECUTOR_REPOSITORY" >/dev/null 2>&1; then
+ release_title="$RELEASE_TAG [executor-run:$RUN_ID]"
+ release_state="$RUNNER_TEMP/existing-release.json"
+ gh api "repos/$EXECUTOR_REPOSITORY/releases/tags/$RELEASE_TAG" > "$release_state"
+ actual_title="$(jq -r '.name // ""' "$release_state")"
+ if [ "$actual_title" != "$release_title" ]; then
+ echo "Release $RELEASE_TAG belongs to a different workflow run" >&2
+ exit 1
+ fi
+ python3 - "$release_state" "$RELEASE_TAG" "$actual_sha" "$version" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ release = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+ expected_prerelease = "-" in sys.argv[4]
+ if (
+ release.get("tag_name") != sys.argv[2]
+ or release.get("target_commitish") != sys.argv[3]
+ or release.get("prerelease") is not expected_prerelease
+ or release.get("draft") not in (True, False)
+ ):
+ raise SystemExit("existing release does not match the validated release state")
+ if release.get("draft") is False and release.get("immutable") is not True:
+ raise SystemExit("published release does not match the immutable rerun state")
+ PY
+
+ run_binding="$RUNNER_TEMP/RELEASE-RUN.json"
+ python3 - "$run_binding" "$EXECUTOR_REPOSITORY" "$RELEASE_TAG" "$actual_sha" "$RUN_ID" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ binding = {
+ "commit": sys.argv[4],
+ "repository": sys.argv[2],
+ "run_id": sys.argv[5],
+ "tag": sys.argv[3],
+ }
+ pathlib.Path(sys.argv[1]).write_text(
+ json.dumps(binding, sort_keys=True, separators=(",", ":")) + "\n",
+ encoding="utf-8",
+ )
+ PY
+ is_draft="$(jq -r .draft "$release_state")"
+ binding_count="$(
+ jq '[.assets[]? | select(.name == "RELEASE-RUN.json")] | length' \
+ "$release_state"
+ )"
+ if [ "$binding_count" -eq 1 ]; then
+ binding_download="$RUNNER_TEMP/existing-release-binding"
+ rm -rf "$binding_download"
+ mkdir -p "$binding_download"
+ gh release download "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --pattern RELEASE-RUN.json \
+ --dir "$binding_download"
+ if ! cmp "$run_binding" "$binding_download/RELEASE-RUN.json"; then
+ echo "Release $RELEASE_TAG belongs to a different workflow run or source" >&2
+ exit 1
+ fi
+ elif [ "$binding_count" -eq 0 ] && [ "$is_draft" = true ]; then
+ echo "Same-run draft is missing RELEASE-RUN.json; staging will repair it"
+ else
+ echo "Release $RELEASE_TAG must contain exactly one RELEASE-RUN.json asset" >&2
+ exit 1
+ fi
+
+ if [ "$is_draft" != true ]; then
+ echo "Same-run retry will reuse immutable published release $RELEASE_TAG"
+ else
+ echo "Same-run retry will repair and verify draft release $RELEASE_TAG"
+ fi
+ fi
+
+ source_date_epoch="$(git show -s --format=%ct HEAD)"
+ if [[ ! "$source_date_epoch" =~ ^[0-9]+$ ]]; then
+ echo "Could not resolve the release commit timestamp" >&2
+ exit 1
+ fi
+
+ owner="$(printf '%s' "${EXECUTOR_REPOSITORY%%/*}" | tr '[:upper:]' '[:lower:]')"
+ if [[ "$version" == *-* ]]; then
+ channel=beta
+ prerelease=true
+ else
+ channel=latest
+ prerelease=false
+ fi
+
+ {
+ echo "channel=$channel"
+ echo "commit_sha=$actual_sha"
+ echo "image=ghcr.io/${owner}/executor"
+ echo "prerelease=$prerelease"
+ echo "source_date_epoch=$source_date_epoch"
+ echo "version=$version"
+ } >> "$GITHUB_OUTPUT"
+
+ authorize-publish:
+ name: Authorize protected native publication
+ if: inputs.mode == 'publish'
+ needs: validate
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ environment: native-release
permissions:
- actions: write
- contents: write
- id-token: write
- pull-requests: write
+ contents: read
+ outputs:
+ registry_username: ${{ steps.token.outputs.registry_username }}
+ steps:
+ - name: Require the environment-scoped release token and capabilities
+ id: token
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ run: |
+ set -euo pipefail
+ if [ -z "$GH_TOKEN" ]; then
+ echo "native-release must provide the NATIVE_RELEASE_TOKEN secret" >&2
+ exit 1
+ fi
+
+ token_owner="$(gh api user --jq .login)"
+ if [ "$token_owner" != bmdavis419 ]; then
+ echo "NATIVE_RELEASE_TOKEN must belong to bmdavis419, got $token_owner" >&2
+ exit 1
+ fi
+ oauth_scopes="$(
+ gh api --include user \
+ | awk -F ': ' 'tolower($1) == "x-oauth-scopes" { print $2; exit }' \
+ | tr -d '\r'
+ )"
+ python3 - "$oauth_scopes" <<'PY'
+ import sys
+
+ actual_scopes = {scope.strip() for scope in sys.argv[1].split(",") if scope.strip()}
+ expected_scopes = {"public_repo", "write:packages"}
+ if actual_scopes != expected_scopes:
+ raise SystemExit(
+ "NATIVE_RELEASE_TOKEN must have exactly public_repo and write:packages scopes"
+ )
+ PY
+ if [ "$(gh api "repos/$EXECUTOR_REPOSITORY" --jq .permissions.push)" != true ]; then
+ echo "NATIVE_RELEASE_TOKEN cannot write to $EXECUTOR_REPOSITORY" >&2
+ exit 1
+ fi
+ if [ "$(
+ gh api "repos/$EXECUTOR_REPOSITORY/immutable-releases" --jq .enabled
+ )" != true ]; then
+ echo "Immutable releases must be enabled for $EXECUTOR_REPOSITORY" >&2
+ exit 1
+ fi
+
+ printf '%s' "$GH_TOKEN" \
+ | docker login ghcr.io --username "$token_owner" --password-stdin
+ docker logout ghcr.io
+ echo "registry_username=$token_owner" >> "$GITHUB_OUTPUT"
+ - name: Verify exact immutable release tag ruleset
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ run: |
+ set -euo pipefail
+ summaries="$RUNNER_TEMP/release-tag-rulesets.json"
+ details="$RUNNER_TEMP/release-tag-rulesets.ndjson"
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "repos/$EXECUTOR_REPOSITORY/rulesets?includes_parents=true&per_page=100" \
+ > "$summaries"
+
+ : > "$details"
+ while IFS= read -r ruleset_id; do
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "repos/$EXECUTOR_REPOSITORY/rulesets/$ruleset_id" \
+ >> "$details"
+ printf '\n' >> "$details"
+ done < <(
+ jq -r '.[] | select(.target == "tag" and .enforcement == "active") | .id' \
+ "$summaries"
+ )
+
+ python3 - "$details" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ rulesets = [
+ json.loads(line)
+ for line in pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").splitlines()
+ if line
+ ]
+ for ruleset in rulesets:
+ ref_name = ruleset.get("conditions", {}).get("ref_name", {})
+ rules = ruleset.get("rules", [])
+ rule_types = sorted(rule.get("type") for rule in rules)
+ update_rules = [rule for rule in rules if rule.get("type") == "update"]
+ if (
+ ruleset.get("target") == "tag"
+ and ruleset.get("enforcement") == "active"
+ and ruleset.get("bypass_actors") == []
+ and ref_name.get("include") == ["refs/tags/v*"]
+ and ref_name.get("exclude") == []
+ and rule_types == ["deletion", "update"]
+ and len(update_rules) == 1
+ and update_rules[0].get("parameters")
+ == {"update_allows_fetch_and_merge": False}
+ ):
+ break
+ else:
+ raise SystemExit(
+ "an exact active no-bypass v* tag ruleset must restrict updates and deletions"
+ )
+ PY
+
+ web:
+ name: Build embedded web application once
+ needs: validate
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
steps:
- - name: Checkout
- uses: actions/checkout@v4
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- fetch-depth: 0
+ ref: ${{ needs.validate.outputs.commit_sha }}
persist-credentials: false
- - name: Setup Bun
- uses: oven-sh/setup-bun@v2
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
+ with:
+ bun-version: ${{ env.BUN_VERSION }}
+
+ - name: Install web dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Build embedded web application
+ env:
+ SOURCE_DATE_EPOCH: ${{ needs.validate.outputs.source_date_epoch }}
+ run: bun run --cwd web build
+
+ - name: Verify canonical web payload
+ shell: bash
+ run: |
+ set -euo pipefail
+ test -f web/build/index.html
+ test -f web/build/THIRD_PARTY_JAVASCRIPT_LICENSES.json
+
+ - name: Upload canonical web payload
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
- bun-version: 1.3.11
+ name: executor-web-build
+ path: web/build/
+ if-no-files-found: error
+ compression-level: 9
+ overwrite: true
+ retention-days: 14
- - name: Setup Node
- uses: actions/setup-node@v4
+ notices:
+ name: Generate third-party notices
+ needs: validate
+ runs-on: ubuntu-22.04
+ timeout-minutes: 20
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- node-version: 24
- registry-url: https://registry.npmjs.org
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
- - name: Update npm for trusted publishing
+ - name: Install Rust toolchain
+ shell: bash
run: |
- npm install -g npm@latest
- npm --version
+ set -euo pipefail
+ rustup toolchain install "$RUST_VERSION" --profile minimal
+ rustup override set "$RUST_VERSION"
- - name: Install dependencies
- run: bun install --frozen-lockfile
+ - name: Generate dependency license inventory
+ shell: bash
+ env:
+ SOURCE_DATE_EPOCH: ${{ needs.validate.outputs.source_date_epoch }}
+ run: |
+ set -euo pipefail
+ cargo install cargo-about --version 0.9.0 --locked --features cli
+ cargo about generate about.hbs > THIRD_PARTY_LICENSES.html
- - name: Create or update release pull request
- id: changesets
- uses: changesets/action@v1
+ - name: Upload notices
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
- version: bun run changeset:version
- commit: Version Packages
- title: Version Packages
- createGithubReleases: false
+ name: executor-license-notices
+ path: THIRD_PARTY_LICENSES.html
+ if-no-files-found: error
+ compression-level: 9
+ overwrite: true
+ retention-days: 14
+
+ macos-service-tests:
+ name: Test macOS service lifecycle on compatibility floor
+ needs: validate
+ runs-on: macos-14
+ timeout-minutes: 45
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Verify pinned compatibility-floor Apple toolchain
+ shell: bash
+ run: |
+ bash scripts/verify-apple-toolchain.sh \
+ "$MACOS_FLOOR_DEVELOPER_DIR" \
+ "$MACOS_FLOOR_XCODE_VERSION" \
+ "$MACOS_FLOOR_XCODE_BUILD" \
+ "$MACOS_FLOOR_SDK_VERSION" \
+ "$MACOS_FLOOR_CLANG_VERSION" \
+ "$MACOS_FLOOR_LD_VERSION"
+
+ - name: Install Rust toolchain
+ shell: bash
+ run: |
+ set -euo pipefail
+ rustup toolchain install "$RUST_VERSION" --profile minimal
+ rustup override set "$RUST_VERSION"
+
+ - name: Test Rust service lifecycle
env:
- # PAT (RELEASE_PAT) so the auto-opened Version Packages PR
- # triggers downstream workflows (pkg-pr-new, CI). PRs authored
- # by the default GITHUB_TOKEN do not trigger other workflows by
- # GitHub design. Falls back to GITHUB_TOKEN when the secret is
- # not set, so the workflow keeps working in forks / before the
- # secret is configured.
- GITHUB_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
+ DEVELOPER_DIR: ${{ env.MACOS_FLOOR_DEVELOPER_DIR }}
+ run: cargo test --locked --lib service::tests
- - name: Smoke test packed @executor-js library packages
- if: steps.changesets.outputs.hasChangesets == 'false'
- run: bun run release:smoke:packages
+ - name: Test LaunchAgent safety with isolated fixtures
+ run: bash scripts/test-install-launchd-safety.sh
- - name: Publish @executor-js library packages
- if: steps.changesets.outputs.hasChangesets == 'false'
- run: bun run release:publish:packages
+ - name: Test bounded LaunchAgent logging
+ run: bash scripts/test-bounded-log.sh
- - name: Detect release version change
- if: steps.changesets.outputs.hasChangesets == 'false'
- id: detect_release
+ - name: Test archive install and uninstall lifecycle
+ run: bash scripts/test-install-release-archive.sh
+
+ build-native:
+ name: Build ${{ matrix.target }}
+ needs:
+ - validate
+ - web
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-22.04
+ platform: linux/amd64
+ target: x86_64-unknown-linux-gnu
+ - runner: ubuntu-22.04-arm
+ platform: linux/arm64
+ target: aarch64-unknown-linux-gnu
+ - runner: macos-15-intel
+ target: x86_64-apple-darwin
+ - runner: macos-15
+ target: aarch64-apple-darwin
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 45
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download canonical web payload
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-web-build
+ path: web/build
+
+ - name: Verify canonical web payload
+ shell: bash
run: |
- before="${{ github.event.before }}"
- if [ "$before" = "0000000000000000000000000000000000000000" ]; then
- before="$(git rev-list --max-count=1 HEAD^ 2>/dev/null || true)"
+ set -euo pipefail
+ test -f web/build/index.html
+ test -f web/build/THIRD_PARTY_JAVASCRIPT_LICENSES.json
+
+ - name: Verify pinned release Apple toolchain
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ bash scripts/verify-apple-toolchain.sh \
+ "$MACOS_BUILD_DEVELOPER_DIR" \
+ "$MACOS_BUILD_XCODE_VERSION" \
+ "$MACOS_BUILD_XCODE_BUILD" \
+ "$MACOS_BUILD_SDK_VERSION" \
+ "$MACOS_BUILD_CLANG_VERSION" \
+ "$MACOS_BUILD_LD_VERSION"
+
+ - name: Install Rust toolchain
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ set -euo pipefail
+ rustup toolchain install "$RUST_VERSION" --profile minimal --target "${{ matrix.target }}"
+ rustup override set "$RUST_VERSION"
+
+ - name: Set up pinned Docker Buildx for Linux native build
+ if: runner.os == 'Linux'
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ with:
+ driver-opts: image=${{ env.BUILDKIT_IMAGE }}
+ version: ${{ env.BUILDX_VERSION }}
+
+ - name: Build Linux release binary in pinned Ubuntu 22.04 toolchain
+ if: runner.os == 'Linux'
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
+ env:
+ SOURCE_DATE_EPOCH: ${{ needs.validate.outputs.source_date_epoch }}
+ with:
+ context: .
+ file: Dockerfile.release-native
+ target: artifact
+ platforms: ${{ matrix.platform }}
+ build-args: |
+ RUST_TARGET=${{ matrix.target }}
+ SOURCE_DATE_EPOCH=${{ needs.validate.outputs.source_date_epoch }}
+ build-contexts: |
+ release-web=web/build
+ outputs: type=local,dest=${{ runner.temp }}/pinned-native
+ provenance: false
+ sbom: false
+
+ - name: Stage pinned Linux release binary
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ set -euo pipefail
+ mkdir -p "target/${{ matrix.target }}/release"
+ install -m 0755 \
+ "$RUNNER_TEMP/pinned-native/executor" \
+ "target/${{ matrix.target }}/release/executor"
+
+ - name: Build macOS release binary
+ if: runner.os == 'macOS'
+ env:
+ DEVELOPER_DIR: ${{ env.MACOS_BUILD_DEVELOPER_DIR }}
+ SOURCE_DATE_EPOCH: ${{ needs.validate.outputs.source_date_epoch }}
+ run: cargo build --locked --release --target "${{ matrix.target }}"
+
+ - name: Verify macOS 14 deployment target
+ if: runner.os == 'macOS'
+ shell: bash
+ env:
+ DEVELOPER_DIR: ${{ env.MACOS_BUILD_DEVELOPER_DIR }}
+ run: |
+ set -euo pipefail
+ binary="target/${{ matrix.target }}/release/executor"
+ build_info="$(xcrun vtool -show-build "$binary")"
+ printf '%s\n' "$build_info"
+ minos="$(printf '%s\n' "$build_info" | awk '$1 == "minos" { print $2; exit }')"
+ if [ "$minos" != "$MACOSX_DEPLOYMENT_TARGET" ]; then
+ echo "macOS release binary declares minos $minos, expected $MACOSX_DEPLOYMENT_TARGET" >&2
+ exit 1
+ fi
+
+ - name: Verify native binary version
+ shell: bash
+ env:
+ EXPECTED_VERSION: ${{ needs.validate.outputs.version }}
+ run: |
+ set -euo pipefail
+ actual="$(target/${{ matrix.target }}/release/executor --version)"
+ test "$actual" = "executor $EXPECTED_VERSION"
+
+ - name: Verify Linux glibc 2.35 compatibility ceiling
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ set -euo pipefail
+ binary="target/${{ matrix.target }}/release/executor"
+ max_glibc="$(readelf --version-info "$binary" \
+ | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' \
+ | sed 's/^GLIBC_//' \
+ | sort -Vu \
+ | tail -n 1)"
+ test -n "$max_glibc"
+ if [ "$(printf '%s\n' "$max_glibc" 2.35 | sort -V | tail -n 1)" != 2.35 ]; then
+ echo "Linux release binary requires GLIBC_$max_glibc, above the 2.35 baseline" >&2
+ exit 1
fi
- version="$(node -e "console.log(JSON.parse(require('fs').readFileSync('apps/cli/package.json', 'utf8')).version)")"
- if [ -n "$before" ] && git cat-file -e "$before:apps/cli/package.json" 2>/dev/null; then
- previous_version="$(git show "$before:apps/cli/package.json" | node -e "let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { data += chunk; }); process.stdin.on('end', () => { console.log(JSON.parse(data).version ?? ''); });")"
+ - name: Stage native binary artifact
+ shell: bash
+ run: |
+ set -euo pipefail
+ output="$RUNNER_TEMP/native-binary"
+ mkdir -p "$output"
+ install -m 0755 "target/${{ matrix.target }}/release/executor" "$output/executor"
+
+ - name: Upload raw native binary
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: executor-native-${{ matrix.target }}
+ path: ${{ runner.temp }}/native-binary/executor
+ if-no-files-found: error
+ compression-level: 0
+ overwrite: true
+ retention-days: 14
+
+ package-native:
+ name: Package native archives in pinned compressor environment
+ needs:
+ - build-native
+ - notices
+ - validate
+ - web
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download raw native binaries
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ path: ${{ runner.temp }}/native-binaries
+ pattern: executor-native-*
+
+ - name: Download third-party notices
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-license-notices
+ path: ${{ runner.temp }}/release-metadata
+
+ - name: Download canonical web payload
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-web-build
+ path: ${{ runner.temp }}/canonical-web
+
+ - name: Stage complete packaging input
+ shell: bash
+ run: |
+ set -euo pipefail
+ input="$RUNNER_TEMP/release-inputs"
+ mkdir -p "$input/native"
+ for target in \
+ aarch64-apple-darwin \
+ aarch64-unknown-linux-gnu \
+ x86_64-apple-darwin \
+ x86_64-unknown-linux-gnu; do
+ source="$RUNNER_TEMP/native-binaries/executor-native-$target/executor"
+ test -s "$source"
+ mkdir "$input/native/$target"
+ install -m 0755 "$source" "$input/native/$target/executor"
+ done
+ install -m 0644 LICENSE "$input/LICENSE"
+ install -m 0644 \
+ "$RUNNER_TEMP/release-metadata/THIRD_PARTY_LICENSES.html" \
+ "$input/THIRD_PARTY_LICENSES.html"
+ install -m 0644 \
+ "$RUNNER_TEMP/canonical-web/THIRD_PARTY_JAVASCRIPT_LICENSES.json" \
+ "$input/THIRD_PARTY_JAVASCRIPT_LICENSES.json"
+
+ - name: Set up pinned Docker Buildx for archive packaging
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ with:
+ driver-opts: image=${{ env.BUILDKIT_IMAGE }}
+ version: ${{ env.BUILDX_VERSION }}
+
+ - name: Build archives in pinned Python and zlib environment
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
+ with:
+ context: .
+ file: Dockerfile.release-package
+ target: artifacts
+ build-args: |
+ BUILDKIT_IMAGE=${{ env.BUILDKIT_IMAGE }}
+ BUILDX_VERSION=${{ env.BUILDX_VERSION }}
+ MACOS_BUILD_CLANG_VERSION=${{ env.MACOS_BUILD_CLANG_VERSION }}
+ MACOS_BUILD_LD_VERSION=${{ env.MACOS_BUILD_LD_VERSION }}
+ MACOS_BUILD_SDK_VERSION=${{ env.MACOS_BUILD_SDK_VERSION }}
+ MACOS_BUILD_XCODE_BUILD=${{ env.MACOS_BUILD_XCODE_BUILD }}
+ MACOS_BUILD_XCODE_VERSION=${{ env.MACOS_BUILD_XCODE_VERSION }}
+ MACOS_FLOOR_CLANG_VERSION=${{ env.MACOS_FLOOR_CLANG_VERSION }}
+ MACOS_FLOOR_LD_VERSION=${{ env.MACOS_FLOOR_LD_VERSION }}
+ MACOS_FLOOR_SDK_VERSION=${{ env.MACOS_FLOOR_SDK_VERSION }}
+ MACOS_FLOOR_XCODE_BUILD=${{ env.MACOS_FLOOR_XCODE_BUILD }}
+ MACOS_FLOOR_XCODE_VERSION=${{ env.MACOS_FLOOR_XCODE_VERSION }}
+ REPRODUCIBILITY_RUN=first
+ SOURCE_DATE_EPOCH=${{ needs.validate.outputs.source_date_epoch }}
+ build-contexts: |
+ release-inputs=${{ runner.temp }}/release-inputs
+ outputs: type=local,dest=${{ runner.temp }}/release-artifacts
+ provenance: false
+ sbom: false
+
+ - name: Repeat pinned archive build
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
+ with:
+ context: .
+ file: Dockerfile.release-package
+ target: artifacts
+ build-args: |
+ BUILDKIT_IMAGE=${{ env.BUILDKIT_IMAGE }}
+ BUILDX_VERSION=${{ env.BUILDX_VERSION }}
+ MACOS_BUILD_CLANG_VERSION=${{ env.MACOS_BUILD_CLANG_VERSION }}
+ MACOS_BUILD_LD_VERSION=${{ env.MACOS_BUILD_LD_VERSION }}
+ MACOS_BUILD_SDK_VERSION=${{ env.MACOS_BUILD_SDK_VERSION }}
+ MACOS_BUILD_XCODE_BUILD=${{ env.MACOS_BUILD_XCODE_BUILD }}
+ MACOS_BUILD_XCODE_VERSION=${{ env.MACOS_BUILD_XCODE_VERSION }}
+ MACOS_FLOOR_CLANG_VERSION=${{ env.MACOS_FLOOR_CLANG_VERSION }}
+ MACOS_FLOOR_LD_VERSION=${{ env.MACOS_FLOOR_LD_VERSION }}
+ MACOS_FLOOR_SDK_VERSION=${{ env.MACOS_FLOOR_SDK_VERSION }}
+ MACOS_FLOOR_XCODE_BUILD=${{ env.MACOS_FLOOR_XCODE_BUILD }}
+ MACOS_FLOOR_XCODE_VERSION=${{ env.MACOS_FLOOR_XCODE_VERSION }}
+ REPRODUCIBILITY_RUN=second
+ SOURCE_DATE_EPOCH=${{ needs.validate.outputs.source_date_epoch }}
+ build-contexts: |
+ release-inputs=${{ runner.temp }}/release-inputs
+ outputs: type=local,dest=${{ runner.temp }}/release-artifacts-repeat
+ provenance: false
+ sbom: false
+
+ - name: Verify byte-identical archives and checksums
+ shell: bash
+ run: |
+ set -euo pipefail
+ first="$RUNNER_TEMP/release-artifacts"
+ second="$RUNNER_TEMP/release-artifacts-repeat"
+ cmp "$first/BUILD-TOOLCHAINS.txt" "$second/BUILD-TOOLCHAINS.txt"
+ for target in \
+ aarch64-apple-darwin \
+ aarch64-unknown-linux-gnu \
+ x86_64-apple-darwin \
+ x86_64-unknown-linux-gnu; do
+ archive="executor-${target}.tar.gz"
+ cmp "$first/$archive" "$second/$archive"
+ cmp "$first/$archive.sha256" "$second/$archive.sha256"
+ (cd "$first" && sha256sum --check "$archive.sha256")
+ done
+
+ - name: Upload canonical packaged archives
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: executor-release-archives
+ path: ${{ runner.temp }}/release-artifacts/
+ if-no-files-found: error
+ compression-level: 0
+ overwrite: true
+ retention-days: 14
+
+ smoke-native:
+ name: Smoke packaged ${{ matrix.target }}
+ needs:
+ - package-native
+ - validate
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-22.04
+ target: x86_64-unknown-linux-gnu
+ - runner: ubuntu-22.04-arm
+ target: aarch64-unknown-linux-gnu
+ - runner: macos-15-intel
+ target: x86_64-apple-darwin
+ - runner: macos-14
+ target: aarch64-apple-darwin
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 15
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download packaged native archive
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-release-archives
+ path: ${{ runner.temp }}/release-archive
+
+ - name: Download canonical web payload
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-web-build
+ path: ${{ runner.temp }}/canonical-web
+
+ - name: Verify and extract packaged native archive
+ shell: bash
+ run: |
+ set -euo pipefail
+ archive="executor-${{ matrix.target }}.tar.gz"
+ input="$RUNNER_TEMP/release-archive"
+ output="$RUNNER_TEMP/packaged-executor"
+ if command -v sha256sum >/dev/null 2>&1; then
+ (cd "$input" && sha256sum --check "$archive.sha256")
else
- previous_version=""
+ (cd "$input" && shasum -a 256 --check "$archive.sha256")
+ fi
+ mkdir -p "$output"
+ tar -xzf "$input/$archive" -C "$output"
+ test -x "$output/executor"
+
+ - name: Boot archive and verify first boot, health, and embedded web identity
+ shell: bash
+ run: |
+ set -euo pipefail
+ binary="$RUNNER_TEMP/packaged-executor/executor"
+ data_dir="$RUNNER_TEMP/executor-smoke-data"
+ log_file="$RUNNER_TEMP/executor-smoke.log"
+ port="$(python3 -c 'import socket; sock = socket.socket(); sock.bind(("127.0.0.1", 0)); print(sock.getsockname()[1]); sock.close()')"
+ mkdir -p "$data_dir"
+ chmod 0700 "$data_dir"
+
+ "$binary" server \
+ --bind "127.0.0.1:$port" \
+ --data-dir "$data_dir" \
+ > "$log_file" 2>&1 &
+ server_pid=$!
+ cleanup() {
+ if kill -0 "$server_pid" >/dev/null 2>&1; then
+ kill -TERM "$server_pid" >/dev/null 2>&1 || true
+ wait "$server_pid" || true
+ fi
+ rm -rf "$data_dir"
+ }
+ trap cleanup EXIT
+
+ ready=false
+ for _ in {1..120}; do
+ if curl --fail --silent --show-error "http://127.0.0.1:$port/healthz" >/dev/null 2>&1; then
+ ready=true
+ break
+ fi
+ if ! kill -0 "$server_pid" >/dev/null 2>&1; then
+ break
+ fi
+ sleep 1
+ done
+ if [ "$ready" != true ]; then
+ cat "$log_file" >&2
+ exit 1
fi
- release_tag="v$version"
- if [ -n "$previous_version" ] && [ "$previous_version" != "$version" ]; then
- echo "changed=true" >> "$GITHUB_OUTPUT"
- elif ! git ls-remote --exit-code --tags origin "refs/tags/$release_tag" >/dev/null 2>&1; then
- echo "changed=true" >> "$GITHUB_OUTPUT"
+ grep -F 'Complete first-boot setup at:' "$log_file" >/dev/null
+ grep -E "http://127\\.0\\.0\\.1:${port}/setup#token=.+" "$log_file" >/dev/null
+ scripts/smoke-release-server.sh \
+ "http://127.0.0.1:$port" \
+ "$RUNNER_TEMP/canonical-web"
+
+ kill -TERM "$server_pid"
+ wait "$server_pid"
+ trap - EXIT
+ rm -rf "$data_dir"
+
+ checksums:
+ name: Assemble checksum manifest
+ needs:
+ - macos-service-tests
+ - package-native
+ - smoke-native
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ steps:
+ - name: Download target artifacts
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-release-archives
+ path: release-artifacts
+
+ - name: Verify archives and assemble manifest
+ shell: bash
+ working-directory: release-artifacts
+ run: |
+ set -euo pipefail
+
+ expected_targets=(
+ aarch64-apple-darwin
+ aarch64-unknown-linux-gnu
+ x86_64-apple-darwin
+ x86_64-unknown-linux-gnu
+ )
+
+ : > SHA256SUMS
+ for target in "${expected_targets[@]}"; do
+ archive="executor-${target}.tar.gz"
+ sidecar="${archive}.sha256"
+ test -f "$archive"
+ test -f "$sidecar"
+ cat "$sidecar" >> SHA256SUMS
+ done
+
+ test "$(find . -maxdepth 1 -name 'executor-*.tar.gz' -type f | wc -l)" -eq 4
+ test -s BUILD-TOOLCHAINS.txt
+ sha256sum BUILD-TOOLCHAINS.txt >> SHA256SUMS
+ sha256sum --check SHA256SUMS
+
+ - name: Upload complete release bundle
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: executor-release-artifacts
+ path: release-artifacts/
+ if-no-files-found: error
+ compression-level: 0
+ overwrite: true
+ retention-days: 14
+
+ stage-release:
+ name: Stage draft GitHub release
+ if: inputs.mode == 'publish'
+ needs:
+ - assemble-container
+ - authorize-publish
+ - checksums
+ - validate
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ environment: native-release
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Download release bundle
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-release-artifacts
+ path: release-artifacts
+
+ - name: Create or reuse draft and upload exact assets
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ IMAGE: ${{ needs.validate.outputs.image }}
+ IMAGE_DIGEST: ${{ needs.assemble-container.outputs.digest }}
+ PRERELEASE: ${{ needs.validate.outputs.prerelease }}
+ RELEASE_SHA: ${{ needs.validate.outputs.commit_sha }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ RUN_ID: ${{ github.run_id }}
+ run: |
+ set -euo pipefail
+
+ git fetch --force --no-tags origin \
+ "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
+ tag_sha="$(git rev-parse "$RELEASE_TAG^{commit}")"
+ if [ "$tag_sha" != "$RELEASE_SHA" ]; then
+ echo "Release tag $RELEASE_TAG moved to $tag_sha, expected $RELEASE_SHA" >&2
+ exit 1
+ fi
+
+ if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Invalid assembled container digest: $IMAGE_DIGEST" >&2
+ exit 1
+ fi
+
+ bindings="$RUNNER_TEMP/release-bindings"
+ mkdir -p "$bindings"
+ run_binding="$bindings/RELEASE-RUN.json"
+ image_binding="$bindings/RELEASE-IMAGE.json"
+ python3 - \
+ "$run_binding" \
+ "$image_binding" \
+ "$EXECUTOR_REPOSITORY" \
+ "$RELEASE_TAG" \
+ "$RELEASE_SHA" \
+ "$RUN_ID" \
+ "$IMAGE" \
+ "$IMAGE_DIGEST" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ run_binding = {
+ "commit": sys.argv[5],
+ "repository": sys.argv[3],
+ "run_id": sys.argv[6],
+ "tag": sys.argv[4],
+ }
+ image_binding = {
+ **run_binding,
+ "digest": sys.argv[8],
+ "image": sys.argv[7],
+ }
+ for path, binding in (
+ (sys.argv[1], run_binding),
+ (sys.argv[2], image_binding),
+ ):
+ pathlib.Path(path).write_text(
+ json.dumps(binding, sort_keys=True, separators=(",", ":")) + "\n",
+ encoding="utf-8",
+ )
+ PY
+
+ assets=(
+ "$image_binding"
+ "$run_binding"
+ release-artifacts/BUILD-TOOLCHAINS.txt
+ release-artifacts/executor-aarch64-apple-darwin.tar.gz
+ release-artifacts/executor-aarch64-apple-darwin.tar.gz.sha256
+ release-artifacts/executor-aarch64-unknown-linux-gnu.tar.gz
+ release-artifacts/executor-aarch64-unknown-linux-gnu.tar.gz.sha256
+ release-artifacts/executor-x86_64-apple-darwin.tar.gz
+ release-artifacts/executor-x86_64-apple-darwin.tar.gz.sha256
+ release-artifacts/executor-x86_64-unknown-linux-gnu.tar.gz
+ release-artifacts/executor-x86_64-unknown-linux-gnu.tar.gz.sha256
+ release-artifacts/SHA256SUMS
+ )
+ for asset in "${assets[@]}"; do
+ test -f "$asset"
+ done
+
+ release_title="$RELEASE_TAG [executor-run:$RUN_ID]"
+ published_release=false
+ if gh release view "$RELEASE_TAG" --repo "$EXECUTOR_REPOSITORY" >/dev/null 2>&1; then
+ release_state="$RUNNER_TEMP/staged-release.json"
+ gh api "repos/$EXECUTOR_REPOSITORY/releases/tags/$RELEASE_TAG" > "$release_state"
+ actual_title="$(jq -r '.name // ""' "$release_state")"
+ if [ "$actual_title" != "$release_title" ]; then
+ echo "Release $RELEASE_TAG belongs to a different workflow run" >&2
+ exit 1
+ fi
+ python3 - "$release_state" "$RELEASE_TAG" "$RELEASE_SHA" "$PRERELEASE" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ release = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+ expected_prerelease = sys.argv[4] == "true"
+ if (
+ release.get("tag_name") != sys.argv[2]
+ or release.get("target_commitish") != sys.argv[3]
+ or release.get("prerelease") is not expected_prerelease
+ or release.get("draft") not in (True, False)
+ ):
+ raise SystemExit("existing release does not match the validated release state")
+ if release.get("draft") is False and release.get("immutable") is not True:
+ raise SystemExit("published release is not immutable")
+ PY
+
+ is_draft="$(jq -r .draft "$release_state")"
+ binding_count="$(
+ jq '[.assets[]? | select(.name == "RELEASE-RUN.json")] | length' \
+ "$release_state"
+ )"
+ if [ "$binding_count" -eq 1 ]; then
+ ownership="$RUNNER_TEMP/existing-release-ownership"
+ rm -rf "$ownership"
+ mkdir -p "$ownership"
+ gh release download "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --pattern RELEASE-RUN.json \
+ --dir "$ownership"
+ if ! cmp "$run_binding" "$ownership/RELEASE-RUN.json"; then
+ echo "Release $RELEASE_TAG belongs to a different workflow run or source" >&2
+ exit 1
+ fi
+ elif [ "$binding_count" -eq 0 ] && [ "$is_draft" = true ]; then
+ echo "Recovering same-run draft created before its binding asset uploaded"
+ else
+ echo "Release $RELEASE_TAG must contain exactly one RELEASE-RUN.json asset" >&2
+ exit 1
+ fi
+
+ if [ "$is_draft" != true ]; then
+ published_release=true
+ fi
else
- echo "changed=false" >> "$GITHUB_OUTPUT"
+ args=(
+ release create "$RELEASE_TAG"
+ --repo "$EXECUTOR_REPOSITORY"
+ --title "$release_title"
+ --target "$RELEASE_SHA"
+ --verify-tag
+ --generate-notes
+ --draft
+ )
+ if [ "$PRERELEASE" = true ]; then
+ args+=(--prerelease)
+ fi
+ gh "${args[@]}"
+ fi
+
+ if [ "$published_release" = false ]; then
+ gh release upload "$RELEASE_TAG" \
+ "${assets[@]}" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --clobber
+ fi
+
+ expected_assets="$(for asset in "${assets[@]}"; do basename "$asset"; done | sort)"
+ remote_assets="$(gh release view "$RELEASE_TAG" --repo "$EXECUTOR_REPOSITORY" --json assets --jq '.assets[].name' | sort)"
+ if [ "$remote_assets" != "$expected_assets" ]; then
+ echo "Release assets do not match the exact native release bundle" >&2
+ diff -u <(printf '%s\n' "$expected_assets") <(printf '%s\n' "$remote_assets") || true
+ exit 1
fi
- echo "version=$version" >> "$GITHUB_OUTPUT"
+ verified_assets="$RUNNER_TEMP/verified-release-assets"
+ rm -rf "$verified_assets"
+ mkdir -p "$verified_assets"
+ gh release download "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --dir "$verified_assets"
+ for asset in "${assets[@]}"; do
+ cmp "$asset" "$verified_assets/$(basename "$asset")"
+ done
+
+ build-container-dry-run:
+ name: Dry-run container ${{ matrix.platform }}
+ if: inputs.mode == 'dry-run'
+ needs:
+ - checksums
+ - validate
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-22.04
+ artifact: amd64
+ platform: linux/amd64
+ target: x86_64-unknown-linux-gnu
+ - runner: ubuntu-22.04-arm
+ artifact: arm64
+ platform: linux/arm64
+ target: aarch64-unknown-linux-gnu
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download matching native release artifact
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-release-artifacts
+ path: ${{ runner.temp }}/release-archive
- - name: Validate release tag
- if: steps.changesets.outputs.hasChangesets == 'false' && steps.detect_release.outputs.changed == 'true'
- id: validate_release
+ - name: Download canonical web payload
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-web-build
+ path: ${{ runner.temp }}/canonical-web
+
+ - name: Verify and extract native release artifact
+ shell: bash
+ run: |
+ set -euo pipefail
+ archive="executor-${{ matrix.target }}.tar.gz"
+ input="$RUNNER_TEMP/release-archive"
+ output="$RUNNER_TEMP/prebuilt-executor"
+ (cd "$input" && sha256sum --check "$archive.sha256")
+ mkdir -p "$output"
+ tar -xzf "$input/$archive" -C "$output"
+ test -x "$output/executor"
+ test -f "$output/LICENSE"
+ test -f "$output/THIRD_PARTY_LICENSES.html"
+ test -f "$output/THIRD_PARTY_JAVASCRIPT_LICENSES.json"
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ with:
+ driver-opts: image=${{ env.BUILDKIT_IMAGE }}
+ version: ${{ env.BUILDX_VERSION }}
+
+ - name: Build prebuilt runtime without publishing
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
+ env:
+ SOURCE_DATE_EPOCH: ${{ needs.validate.outputs.source_date_epoch }}
+ with:
+ context: .
+ file: Dockerfile
+ target: runtime-prebuilt
+ platforms: ${{ matrix.platform }}
+ build-contexts: |
+ prebuilt-executor=${{ runner.temp }}/prebuilt-executor
+ load: true
+ push: false
+ tags: executor-release-smoke:${{ matrix.artifact }}
+ labels: |
+ org.opencontainers.image.title=Executor
+ org.opencontainers.image.description=Native self-hosted Executor
+ org.opencontainers.image.source=https://github.com/${{ env.EXECUTOR_REPOSITORY }}
+ org.opencontainers.image.revision=${{ needs.validate.outputs.commit_sha }}
+ org.opencontainers.image.version=${{ needs.validate.outputs.version }}
+ provenance: false
+ sbom: false
+
+ - name: Smoke release-only prebuilt image path
+ shell: bash
+ run: |
+ scripts/smoke-release-container.sh \
+ "executor-release-smoke:${{ matrix.artifact }}" \
+ "$RUNNER_TEMP/canonical-web" \
+ "executor-release-smoke-${GITHUB_RUN_ID}-${{ matrix.artifact }}" \
+ "executor-release-smoke-data-${GITHUB_RUN_ID}-${{ matrix.artifact }}"
+
+ publish-container-arch:
+ name: Publish container ${{ matrix.platform }} by digest
+ if: inputs.mode == 'publish'
+ needs:
+ - authorize-publish
+ - checksums
+ - validate
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-22.04
+ platform: linux/amd64
+ target: x86_64-unknown-linux-gnu
+ artifact: amd64
+ - runner: ubuntu-22.04-arm
+ platform: linux/arm64
+ target: aarch64-unknown-linux-gnu
+ artifact: arm64
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 20
+ environment: native-release
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download matching native release artifact
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-release-artifacts
+ path: ${{ runner.temp }}/release-archive
+
+ - name: Verify and extract native release artifact
+ shell: bash
+ run: |
+ set -euo pipefail
+ archive="executor-${{ matrix.target }}.tar.gz"
+ input="$RUNNER_TEMP/release-archive"
+ output="$RUNNER_TEMP/prebuilt-executor"
+ (cd "$input" && sha256sum --check "$archive.sha256")
+ mkdir -p "$output"
+ tar -xzf "$input/$archive" -C "$output"
+ test -x "$output/executor"
+ test -f "$output/LICENSE"
+ test -f "$output/THIRD_PARTY_LICENSES.html"
+ test -f "$output/THIRD_PARTY_JAVASCRIPT_LICENSES.json"
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ with:
+ driver-opts: image=${{ env.BUILDKIT_IMAGE }}
+ version: ${{ env.BUILDX_VERSION }}
+
+ - name: Log in to GHCR
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
+ with:
+ registry: ghcr.io
+ username: ${{ needs.authorize-publish.outputs.registry_username }}
+ password: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+
+ - name: Build and push architecture image by digest
+ id: build
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
env:
- RELEASE_VERSION: ${{ steps.detect_release.outputs.version }}
- run: bun run scripts/validate-release-ref.ts --version-env RELEASE_VERSION --output tag
+ SOURCE_DATE_EPOCH: ${{ needs.validate.outputs.source_date_epoch }}
+ with:
+ context: .
+ file: Dockerfile
+ target: runtime-prebuilt
+ platforms: ${{ matrix.platform }}
+ build-contexts: |
+ prebuilt-executor=${{ runner.temp }}/prebuilt-executor
+ outputs: type=image,name=${{ needs.validate.outputs.image }},push-by-digest=true,name-canonical=true,push=true,rewrite-timestamp=true
+ labels: |
+ org.opencontainers.image.title=Executor
+ org.opencontainers.image.description=Native self-hosted Executor
+ org.opencontainers.image.source=https://github.com/${{ env.EXECUTOR_REPOSITORY }}
+ org.opencontainers.image.revision=${{ needs.validate.outputs.commit_sha }}
+ org.opencontainers.image.version=${{ needs.validate.outputs.version }}
+ provenance: false
+ sbom: false
- - name: Create and push release tag
- if: steps.changesets.outputs.hasChangesets == 'false' && steps.detect_release.outputs.changed == 'true'
+ - name: Export architecture digest
+ shell: bash
env:
- GH_TOKEN: ${{ secrets.RELEASE_PAT || github.token }}
- RELEASE_TAG: ${{ steps.validate_release.outputs.tag }}
+ IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
run: |
- auth_remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
- if git ls-remote --exit-code --tags "$auth_remote" "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then
- echo "Tag $RELEASE_TAG already exists."
- exit 0
+ set -euo pipefail
+ if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Invalid architecture image digest: $IMAGE_DIGEST" >&2
+ exit 1
fi
+ mkdir -p "$RUNNER_TEMP/container-digests"
+ touch "$RUNNER_TEMP/container-digests/${IMAGE_DIGEST#sha256:}"
+
+ - name: Upload architecture digest
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: executor-container-digest-${{ matrix.artifact }}
+ path: ${{ runner.temp }}/container-digests/
+ if-no-files-found: error
+ compression-level: 0
+ overwrite: true
+ retention-days: 1
+
+ assemble-container:
+ name: Assemble multi-platform container manifest
+ if: inputs.mode == 'publish'
+ needs:
+ - authorize-publish
+ - publish-container-arch
+ - validate
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ environment: native-release
+ permissions:
+ contents: read
+ outputs:
+ digest: ${{ steps.manifest.outputs.digest }}
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download architecture digests
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ path: ${{ runner.temp }}/container-digests
+ pattern: executor-container-digest-*
+ merge-multiple: true
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ with:
+ driver-opts: image=${{ env.BUILDKIT_IMAGE }}
+ version: ${{ env.BUILDX_VERSION }}
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git tag "$RELEASE_TAG"
- git push "$auth_remote" "$RELEASE_TAG"
+ - name: Log in to GHCR
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
+ with:
+ registry: ghcr.io
+ username: ${{ needs.authorize-publish.outputs.registry_username }}
+ password: ${{ secrets.NATIVE_RELEASE_TOKEN }}
- - name: Trigger CLI publish
- if: steps.changesets.outputs.hasChangesets == 'false' && steps.detect_release.outputs.changed == 'true'
+ - name: Assemble and verify staging manifest
+ id: manifest
+ shell: bash
env:
- GH_TOKEN: ${{ github.token }}
- RELEASE_TAG: ${{ steps.validate_release.outputs.tag }}
+ IMAGE: ${{ needs.validate.outputs.image }}
+ VERSION: ${{ needs.validate.outputs.version }}
run: |
- gh workflow run publish-executor-package.yml --ref "$RELEASE_TAG" -f tag="$RELEASE_TAG"
+ set -euo pipefail
+ mapfile -t digests < <(find "$RUNNER_TEMP/container-digests" -maxdepth 1 -type f -printf '%f\n' | sort)
+ if [ "${#digests[@]}" -ne 2 ]; then
+ echo "Expected two architecture digests, found ${#digests[@]}" >&2
+ exit 1
+ fi
+
+ sources=()
+ for digest in "${digests[@]}"; do
+ if [[ ! "$digest" =~ ^[0-9a-f]{64}$ ]]; then
+ echo "Invalid architecture digest: $digest" >&2
+ exit 1
+ fi
+ sources+=("$IMAGE@sha256:$digest")
+ done
- # Desktop build downloads CLI binaries from the release, so it must
- # run after CLI publish completes. Trigger it from the CLI workflow
- # or manually via: gh workflow run publish-desktop.yml -f tag=vX.Y.Z
+ staging="$IMAGE:staging-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
+ docker buildx imagetools create \
+ --annotation "index:org.opencontainers.image.version=$VERSION" \
+ --tag "$staging" \
+ "${sources[@]}"
+ raw_manifest="$(docker buildx imagetools inspect "$staging" --raw)"
+ python3 scripts/verify-release-index.py \
+ --version "$VERSION" \
+ <<< "$raw_manifest"
+
+ digest="$(docker buildx imagetools inspect "$staging" | awk '$1 == "Digest:" { print $2; exit }')"
+ if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Invalid assembled image digest: $digest" >&2
+ exit 1
+ fi
+ echo "digest=$digest" >> "$GITHUB_OUTPUT"
+
+ smoke-container:
+ name: Smoke published prebuilt container ${{ matrix.platform }}
+ if: inputs.mode == 'publish'
+ needs:
+ - assemble-container
+ - authorize-publish
+ - validate
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-22.04
+ artifact: amd64
+ platform: linux/amd64
+ - runner: ubuntu-22.04-arm
+ artifact: arm64
+ platform: linux/arm64
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 15
+ environment: native-release
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ persist-credentials: false
+
+ - name: Download canonical web payload
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-web-build
+ path: ${{ runner.temp }}/canonical-web
+
+ - name: Log in to GHCR
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
+ with:
+ registry: ghcr.io
+ username: ${{ needs.authorize-publish.outputs.registry_username }}
+ password: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+
+ - name: Pull and smoke exact assembled digest
+ shell: bash
+ env:
+ IMAGE: ${{ needs.validate.outputs.image }}
+ IMAGE_DIGEST: ${{ needs.assemble-container.outputs.digest }}
+ run: |
+ set -euo pipefail
+ reference="$IMAGE@$IMAGE_DIGEST"
+ docker pull "$reference"
+ scripts/smoke-release-container.sh \
+ "$reference" \
+ "$RUNNER_TEMP/canonical-web" \
+ "executor-published-smoke-${GITHUB_RUN_ID}-${{ matrix.artifact }}" \
+ "executor-published-smoke-data-${GITHUB_RUN_ID}-${{ matrix.artifact }}"
+
+ - name: Verify exact digest is anonymously pullable
+ shell: bash
+ env:
+ IMAGE: ${{ needs.validate.outputs.image }}
+ IMAGE_DIGEST: ${{ needs.assemble-container.outputs.digest }}
+ run: |
+ set -euo pipefail
+ anonymous_config="$RUNNER_TEMP/anonymous-docker-config"
+ mkdir -p "$anonymous_config"
+ DOCKER_CONFIG="$anonymous_config" docker pull "$IMAGE@$IMAGE_DIGEST"
+
+ promote:
+ name: Promote release
+ if: inputs.mode == 'publish'
+ needs:
+ - assemble-container
+ - authorize-publish
+ - smoke-container
+ - stage-release
+ - validate
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ environment: native-release
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout exact commit
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: ${{ needs.validate.outputs.commit_sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Download release bundle for final verification
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: executor-release-artifacts
+ path: release-artifacts
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ with:
+ driver-opts: image=${{ env.BUILDKIT_IMAGE }}
+ version: ${{ env.BUILDX_VERSION }}
+
+ - name: Log in to GHCR
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
+ with:
+ registry: ghcr.io
+ username: ${{ needs.authorize-publish.outputs.registry_username }}
+ password: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+
+ - name: Revalidate remote release tag before promotion
+ shell: bash
+ env:
+ RELEASE_SHA: ${{ needs.validate.outputs.commit_sha }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ run: |
+ set -euo pipefail
+ git fetch --force --no-tags origin \
+ "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
+ tag_sha="$(git rev-parse "$RELEASE_TAG^{commit}")"
+ if [ "$tag_sha" != "$RELEASE_SHA" ]; then
+ echo "Release tag $RELEASE_TAG moved to $tag_sha, expected $RELEASE_SHA" >&2
+ exit 1
+ fi
+
+ - name: Assign immutable tags and verify channel eligibility
+ shell: bash
+ env:
+ CHANNEL: ${{ needs.validate.outputs.channel }}
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ IMAGE: ${{ needs.validate.outputs.image }}
+ IMAGE_DIGEST: ${{ needs.assemble-container.outputs.digest }}
+ RELEASE_SHA: ${{ needs.validate.outputs.commit_sha }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ VERSION: ${{ needs.validate.outputs.version }}
+ run: |
+ set -euo pipefail
+ if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Invalid container digest: $IMAGE_DIGEST" >&2
+ exit 1
+ fi
+
+ inspect_digest() {
+ local reference="$1"
+ local error_file="$RUNNER_TEMP/imagetools-inspect-error"
+ local output digest
+ : > "$error_file"
+ if output="$(docker buildx imagetools inspect "$reference" 2>"$error_file")"; then
+ digest="$(awk '$1 == "Digest:" { print $2; exit }' <<< "$output")"
+ if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Could not resolve a valid digest for $reference" >&2
+ return 2
+ fi
+ printf '%s\n' "$digest"
+ return 0
+ fi
+ if grep -Eqi 'manifest unknown|not found|404' "$error_file"; then
+ return 1
+ fi
+ cat "$error_file" >&2
+ return 2
+ }
+
+ release_history="$RUNNER_TEMP/published-releases.json"
+ gh api --paginate --slurp \
+ "repos/$EXECUTOR_REPOSITORY/releases?per_page=100" \
+ > "$release_history"
+ history_relation="$(
+ python3 scripts/check-release-channel-order.py \
+ --channel "$CHANNEL" \
+ --history-file "$release_history" \
+ --candidate "$VERSION"
+ )"
+ echo "Published release history permits $history_relation candidate $VERSION"
+
+ channel_reference="$IMAGE:$CHANNEL"
+ if current_channel_digest="$(inspect_digest "$channel_reference")"; then
+ channel_manifest="$(docker buildx imagetools inspect "$channel_reference" --raw)"
+ current_channel_version="$(
+ jq -r '.annotations["org.opencontainers.image.version"] // ""' \
+ <<< "$channel_manifest"
+ )"
+ if [ -z "$current_channel_version" ]; then
+ echo "Existing channel $channel_reference has no release version annotation" >&2
+ exit 1
+ fi
+ relation="$(
+ python3 scripts/check-release-channel-order.py \
+ --channel "$CHANNEL" \
+ --current "$current_channel_version" \
+ --candidate "$VERSION"
+ )"
+ if [ "$relation" = equal ]; then
+ if [ "$current_channel_digest" != "$IMAGE_DIGEST" ]; then
+ echo "Channel $channel_reference already has version $VERSION at a different digest" >&2
+ exit 1
+ fi
+ echo "Channel $channel_reference already points to the expected digest"
+ fi
+ else
+ rc=$?
+ if [ "$rc" -ne 1 ]; then
+ exit "$rc"
+ fi
+ echo "Channel $channel_reference does not exist yet"
+ fi
+
+ immutable_tags=("$RELEASE_TAG" "$VERSION" "sha-$RELEASE_SHA")
+ missing_tags=()
+ for tag in "${immutable_tags[@]}"; do
+ reference="$IMAGE:$tag"
+ if existing="$(inspect_digest "$reference")"; then
+ if [ "$existing" != "$IMAGE_DIGEST" ]; then
+ echo "Immutable tag $reference already points to $existing, expected $IMAGE_DIGEST" >&2
+ exit 1
+ fi
+ echo "Immutable tag $reference already points to the expected digest"
+ else
+ status=$?
+ if [ "$status" -ne 1 ]; then
+ exit "$status"
+ fi
+ missing_tags+=("$tag")
+ fi
+ done
+
+ for tag in "${missing_tags[@]}"; do
+ docker buildx imagetools create \
+ --tag "$IMAGE:$tag" \
+ "$IMAGE@$IMAGE_DIGEST"
+ done
+
+ for tag in "${immutable_tags[@]}"; do
+ actual="$(inspect_digest "$IMAGE:$tag")"
+ if [ "$actual" != "$IMAGE_DIGEST" ]; then
+ echo "Immutable tag $IMAGE:$tag resolved to $actual after assignment" >&2
+ exit 1
+ fi
+ done
+
+ - name: Verify release ownership and image binding before publication
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ IMAGE: ${{ needs.validate.outputs.image }}
+ IMAGE_DIGEST: ${{ needs.assemble-container.outputs.digest }}
+ RELEASE_SHA: ${{ needs.validate.outputs.commit_sha }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ RUN_ID: ${{ github.run_id }}
+ run: |
+ set -euo pipefail
+ if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Invalid assembled container digest: $IMAGE_DIGEST" >&2
+ exit 1
+ fi
+
+ release_title="$RELEASE_TAG [executor-run:$RUN_ID]"
+ release_state="$RUNNER_TEMP/pre-publication-release.json"
+ gh api "repos/$EXECUTOR_REPOSITORY/releases/tags/$RELEASE_TAG" > "$release_state"
+ python3 - \
+ "$release_state" \
+ "$RELEASE_TAG" \
+ "$RELEASE_SHA" \
+ "$release_title" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ release = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+ if (
+ release.get("tag_name") != sys.argv[2]
+ or release.get("target_commitish") != sys.argv[3]
+ or release.get("name") != sys.argv[4]
+ or release.get("draft") not in (True, False)
+ ):
+ raise SystemExit("release ownership does not match this workflow run")
+ if release.get("draft") is False and release.get("immutable") is not True:
+ raise SystemExit("published release is not immutable")
+ PY
+
+ bindings="$RUNNER_TEMP/final-release-bindings"
+ mkdir -p "$bindings"
+ run_binding="$bindings/RELEASE-RUN.json"
+ image_binding="$bindings/RELEASE-IMAGE.json"
+ python3 - \
+ "$run_binding" \
+ "$image_binding" \
+ "$EXECUTOR_REPOSITORY" \
+ "$RELEASE_TAG" \
+ "$RELEASE_SHA" \
+ "$RUN_ID" \
+ "$IMAGE" \
+ "$IMAGE_DIGEST" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ run_binding = {
+ "commit": sys.argv[5],
+ "repository": sys.argv[3],
+ "run_id": sys.argv[6],
+ "tag": sys.argv[4],
+ }
+ image_binding = {
+ **run_binding,
+ "digest": sys.argv[8],
+ "image": sys.argv[7],
+ }
+ for path, binding in (
+ (sys.argv[1], run_binding),
+ (sys.argv[2], image_binding),
+ ):
+ pathlib.Path(path).write_text(
+ json.dumps(binding, sort_keys=True, separators=(",", ":")) + "\n",
+ encoding="utf-8",
+ )
+ PY
+
+ assets=(
+ "$image_binding"
+ "$run_binding"
+ release-artifacts/BUILD-TOOLCHAINS.txt
+ release-artifacts/executor-aarch64-apple-darwin.tar.gz
+ release-artifacts/executor-aarch64-apple-darwin.tar.gz.sha256
+ release-artifacts/executor-aarch64-unknown-linux-gnu.tar.gz
+ release-artifacts/executor-aarch64-unknown-linux-gnu.tar.gz.sha256
+ release-artifacts/executor-x86_64-apple-darwin.tar.gz
+ release-artifacts/executor-x86_64-apple-darwin.tar.gz.sha256
+ release-artifacts/executor-x86_64-unknown-linux-gnu.tar.gz
+ release-artifacts/executor-x86_64-unknown-linux-gnu.tar.gz.sha256
+ release-artifacts/SHA256SUMS
+ )
+ for asset in "${assets[@]}"; do
+ test -f "$asset"
+ done
+
+ expected_assets="$(for asset in "${assets[@]}"; do basename "$asset"; done | sort)"
+ remote_assets="$(gh release view "$RELEASE_TAG" --repo "$EXECUTOR_REPOSITORY" --json assets --jq '.assets[].name' | sort)"
+ if [ "$remote_assets" != "$expected_assets" ]; then
+ echo "Release assets do not match the exact pre-publication bundle" >&2
+ diff -u <(printf '%s\n' "$expected_assets") <(printf '%s\n' "$remote_assets") || true
+ exit 1
+ fi
+
+ verified_assets="$RUNNER_TEMP/final-verified-release-assets"
+ rm -rf "$verified_assets"
+ mkdir -p "$verified_assets"
+ gh release download "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --dir "$verified_assets"
+ for asset in "${assets[@]}"; do
+ cmp "$asset" "$verified_assets/$(basename "$asset")"
+ done
+
+ - name: Publish GitHub release
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ PRERELEASE: ${{ needs.validate.outputs.prerelease }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ run: |
+ set -euo pipefail
+ is_draft="$(
+ gh release view "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --json isDraft \
+ --jq .isDraft
+ )"
+ if [ "$is_draft" = true ]; then
+ args=(release edit "$RELEASE_TAG" --repo "$EXECUTOR_REPOSITORY" --draft=false)
+ if [ "$PRERELEASE" = true ]; then
+ args+=(--prerelease)
+ else
+ args+=(--latest)
+ fi
+ gh "${args[@]}"
+ else
+ actual_prerelease="$(
+ gh release view "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --json isPrerelease \
+ --jq .isPrerelease
+ )"
+ if [ "$actual_prerelease" != "$PRERELEASE" ]; then
+ echo "Published release $RELEASE_TAG has unexpected prerelease state" >&2
+ exit 1
+ fi
+ echo "GitHub release $RELEASE_TAG is already published"
+ fi
+
+ - name: Verify published GitHub release is immutable
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ PRERELEASE: ${{ needs.validate.outputs.prerelease }}
+ RELEASE_SHA: ${{ needs.validate.outputs.commit_sha }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ RUN_ID: ${{ github.run_id }}
+ run: |
+ set -euo pipefail
+ release_state="$RUNNER_TEMP/published-release.json"
+ gh api "repos/$EXECUTOR_REPOSITORY/releases/tags/$RELEASE_TAG" > "$release_state"
+ release_title="$RELEASE_TAG [executor-run:$RUN_ID]"
+ python3 - \
+ "$release_state" \
+ "$RELEASE_TAG" \
+ "$RELEASE_SHA" \
+ "$PRERELEASE" \
+ "$release_title" <<'PY'
+ import json
+ import pathlib
+ import sys
+
+ release = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+ if (
+ release.get("tag_name") != sys.argv[2]
+ or release.get("target_commitish") != sys.argv[3]
+ or release.get("draft") is not False
+ or release.get("prerelease") is not (sys.argv[4] == "true")
+ or release.get("name") != sys.argv[5]
+ or release.get("immutable") is not True
+ ):
+ raise SystemExit("published GitHub release is not the expected immutable release")
+ PY
+
+ verified=false
+ verification_error="$RUNNER_TEMP/release-attestation-error"
+ verification_output="$RUNNER_TEMP/release-attestation.json"
+ for attempt in {1..24}; do
+ if gh release verify "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --format json \
+ > "$verification_output" \
+ 2> "$verification_error"; then
+ cat "$verification_output"
+ verified=true
+ break
+ fi
+ if [ "$attempt" -lt 24 ]; then
+ sleep 5
+ fi
+ done
+ if [ "$verified" != true ]; then
+ cat "$verification_error" >&2
+ echo "GitHub release attestation was not available after bounded retries" >&2
+ exit 1
+ fi
+
+ - name: Move mutable channel after GitHub release publication
+ shell: bash
+ env:
+ CHANNEL: ${{ needs.validate.outputs.channel }}
+ GH_TOKEN: ${{ secrets.NATIVE_RELEASE_TOKEN }}
+ IMAGE: ${{ needs.validate.outputs.image }}
+ IMAGE_DIGEST: ${{ needs.assemble-container.outputs.digest }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ VERSION: ${{ needs.validate.outputs.version }}
+ run: |
+ set -euo pipefail
+ test "$(
+ gh release view "$RELEASE_TAG" \
+ --repo "$EXECUTOR_REPOSITORY" \
+ --json isDraft \
+ --jq .isDraft
+ )" = false
+ if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Invalid container digest: $IMAGE_DIGEST" >&2
+ exit 1
+ fi
+
+ inspect_digest() {
+ local reference="$1"
+ local error_file="$RUNNER_TEMP/imagetools-inspect-error"
+ local output digest
+ : > "$error_file"
+ if output="$(docker buildx imagetools inspect "$reference" 2>"$error_file")"; then
+ digest="$(awk '$1 == "Digest:" { print $2; exit }' <<< "$output")"
+ if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
+ echo "Could not resolve a valid digest for $reference" >&2
+ return 2
+ fi
+ printf '%s\n' "$digest"
+ return 0
+ fi
+ if grep -Eqi 'manifest unknown|not found|404' "$error_file"; then
+ return 1
+ fi
+ cat "$error_file" >&2
+ return 2
+ }
+
+ release_history="$RUNNER_TEMP/published-releases-after-promotion.json"
+ gh api --paginate --slurp \
+ "repos/$EXECUTOR_REPOSITORY/releases?per_page=100" \
+ > "$release_history"
+ python3 scripts/check-release-channel-order.py \
+ --channel "$CHANNEL" \
+ --history-file "$release_history" \
+ --candidate "$VERSION"
+
+ channel_reference="$IMAGE:$CHANNEL"
+ move_channel=true
+ if current_channel_digest="$(inspect_digest "$channel_reference")"; then
+ channel_manifest="$(docker buildx imagetools inspect "$channel_reference" --raw)"
+ current_channel_version="$(
+ jq -r '.annotations["org.opencontainers.image.version"] // ""' \
+ <<< "$channel_manifest"
+ )"
+ if [ -z "$current_channel_version" ]; then
+ echo "Existing channel $channel_reference has no release version annotation" >&2
+ exit 1
+ fi
+ relation="$(
+ python3 scripts/check-release-channel-order.py \
+ --channel "$CHANNEL" \
+ --current "$current_channel_version" \
+ --candidate "$VERSION"
+ )"
+ if [ "$relation" = equal ]; then
+ if [ "$current_channel_digest" != "$IMAGE_DIGEST" ]; then
+ echo "Channel $channel_reference already has version $VERSION at a different digest" >&2
+ exit 1
+ fi
+ move_channel=false
+ fi
+ else
+ status=$?
+ if [ "$status" -ne 1 ]; then
+ exit "$status"
+ fi
+ fi
+
+ if [ "$move_channel" = true ]; then
+ docker buildx imagetools create \
+ --tag "$channel_reference" \
+ "$IMAGE@$IMAGE_DIGEST"
+ fi
+ channel_digest="$(inspect_digest "$channel_reference")"
+ if [ "$channel_digest" != "$IMAGE_DIGEST" ]; then
+ echo "Channel tag $channel_reference resolved to $channel_digest" >&2
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
index 4c3f79186..7d6eadfbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,7 @@ coverage
*.lcov
# logs
-logs
+/logs/
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
@@ -53,18 +53,18 @@ executor.har
secret.key
# desktop app build artifacts
-apps/desktop/resources/
+legacy/desktop/resources/
# cloud local dev database
.pglite
-apps/cloud/.dev-db/
-apps/cloud/.e2e-db/
+legacy/cloud/.dev-db/
+legacy/cloud/.e2e-db/
# e2e suite: generated run artifacts + throwaway target state (the * also
# covers ad-hoc variants like .e2e-stub-db-manual from debugging boots)
e2e/runs/
-apps/cloud/.e2e-stub-db*/
-apps/host-selfhost/.e2e-data*/
+legacy/cloud/.e2e-stub-db*/
+legacy/host-selfhost/.e2e-data*/
# playwright e2e artifacts
test-results/
@@ -115,3 +115,8 @@ scratch/
# Throwaway UX prototype (not part of the app)
ux-demo/
+
+
+# Added by cargo
+
+/target
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index d92df54ec..20ace484d 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -5,6 +5,7 @@
".reference",
".turbo",
"dist",
+ "legacy",
"vendor",
"e2e/runs",
"node_modules",
@@ -12,7 +13,6 @@
"bun.lock",
"*.tsbuildinfo",
"executor-*.tgz",
- "**/routeTree.gen.ts",
- "apps/cloud/src/services/executor-schema.ts"
+ "**/routeTree.gen.ts"
]
}
diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc
index 53839c4bb..9c9841171 100644
--- a/.oxlintrc.jsonc
+++ b/.oxlintrc.jsonc
@@ -59,10 +59,11 @@
"overrides": [
{
"files": [
- "apps/cli/src/**/*.{ts,tsx}",
- "apps/desktop/src/main.ts",
+ "legacy/cli/src/**/*.{ts,tsx}",
+ "legacy/desktop/src/main/**/*.{ts,tsx}",
"scripts/**/*.{ts,js}",
"apps/*/scripts/**/*.{ts,js}",
+ "legacy/*/scripts/**/*.{ts,js}",
"packages/kernel/runtime-*/src/**/*.{ts,tsx,js,mjs}",
],
"rules": {
@@ -153,6 +154,7 @@
"vendor/",
"emulators/",
"e2e/runs/",
+ "legacy/",
"node_modules/",
"packages/core/fumadb/",
"packages/core/sdk/src/vendor/json-schema-to-typescript/",
diff --git a/.skills/cli-release/SKILL.md b/.skills/cli-release/SKILL.md
index 11fb4234c..d5263e88c 100644
--- a/.skills/cli-release/SKILL.md
+++ b/.skills/cli-release/SKILL.md
@@ -1,133 +1,65 @@
---
name: cli-release
-description: Runbook for releasing the `executor` CLI package (stable and beta). Covers scope of what ships with the CLI, user-facing changelog conventions, Changesets + Version Packages PR flow, beta train entry/exit, and owner preferences. Use when the user asks to cut a release, prepare release notes, enter/exit a beta train, or write changesets for the CLI.
+description: Runbook for the supported native Executor release. Covers Cargo versioning, immutable release inputs, dry runs, protected authorization, and owner-gated publishing.
---
-# Executor CLI release runbook
+# Executor release runbook
-## Authoritative doc
+## Authoritative document
-`RELEASING.md` at repo root is the source of truth. This skill encodes the owner's preferences on top of it.
+`RELEASING.md` at the repository root is the source of truth. The supported
+native Rust product, published by `.github/workflows/release.yml`, is the only
+release surface.
-## What the `executor` CLI actually ships
+Do not infer another release path from archived source or historical tags.
-The CLI binary bundles:
+## Native product release
-- `apps/cli/**` — CLI source + daemon
-- `apps/local/**` — the web UI (embedded as a virtual module via `apps/cli/src/build.ts:178`) + drizzle migrations (`build.ts:205`)
-- `packages/**` — `core`, `kernel`, `hosts/mcp`, `runtime-quickjs`, and every plugin under `packages/plugins/**`
+`Cargo.toml` is the only native product version source. The manual
+`.github/workflows/release.yml` workflow is the only entrypoint allowed to
+publish native archives, the root container image, or the primary GitHub
+release.
-Does **not** ship in the CLI:
+The workflow requires:
-- `apps/cloud/**` (Cloudflare Workers deployment)
-- `apps/marketing/**`, `apps/desktop/**`
-- `examples/**`, `tests/**`
+- `mode`: `dry-run` or `publish`
+- `tag`: exact `v`
+- `commit_sha`: exact lowercase 40-character commit on `origin/main`
-**Implication for changelogs**: when asked "what changed since the last release", scope is `git log v..HEAD -- apps/cli apps/local packages`, not just `apps/cli`. Skipping `apps/local` and `packages` misses the bulk of product changes (Connections UI, OAuth plugins, SDK scope, OTEL, etc.).
+Always run dry-run mode first. Publish mode additionally requires the remote tag
+to point at the exact supplied commit, the protected `native-release`
+environment, and its scoped `NATIVE_RELEASE_TOKEN` secret.
-## Versioning preferences
-
-- Prior convention in this repo uses **`patch`** bumps for feature-heavy releases (see `.changeset/executor-1.4.6-beta.md` for precedent). Don't push back on patch unless there are genuine SemVer-breaking API changes to a library consumer surface.
-- Breaking CLI UX changes (removed flags, changed argv shape) have historically still been `patch` bumps. Follow the owner's call — ask, don't assume `minor`.
-- Normal release/patch PRs must add a `.changeset/*.md` file with frontmatter like `"executor": patch`. Do **not** directly bump `apps/cli/package.json` or `bun.lock` in a feature/fix PR.
-- Only the Changesets-generated `Version Packages` PR should move `apps/cli/package.json`. If a normal PR directly changes that version, merging it to `main` can make `.github/workflows/release.yml` tag the commit and dispatch `publish-executor-package.yml`, causing an immediate CLI publish.
-- `@executor-js/*` library packages have their own publish path.
-
-## Release notes: standard Changesets flow — the changeset body IS the changelog
-
-As of v1.5.0 this repo uses the canonical Changesets pipeline. The old
-`apps/cli/release-notes/next.md` rolling file is gone — do not recreate it.
-
-### How it's wired
-
-- Every user-visible PR adds a `.changeset/*.md`; its **body** is the
- user-facing changelog entry.
-- `changeset version` (run by `changesets/action@v1` when building the
- Version Packages PR) compiles changeset bodies into each bumped
- package's `CHANGELOG.md` using `@changesets/changelog-github`
- (configured in `.changeset/config.json`), which prefixes each entry
- with the PR link and credits the author automatically.
-- `apps/cli/src/release.ts` (`changelogSectionForVersion`) extracts the
- released version's `## ` section from `apps/cli/CHANGELOG.md`
- and uses it as the GitHub Release body. Missing section → falls back to
- `--generate-notes`.
-- Per-package `CHANGELOG.md` seed files are still required for every
- workspace package (`bun run lint:changelog-stubs --fix` creates them);
- `changesets/action@v1` crashes with `ENOENT` on missing files.
-- `@changesets/changelog-github` needs `GITHUB_TOKEN` during
- `changeset version`. CI provides it; locally:
- `GITHUB_TOKEN=$(gh auth token) bun run changeset:version`.
-
-### Writing changeset bodies
-
-- Lead with user-visible behavior, not implementation. One sentence for a
- typical fix; a short paragraph for a feature.
-- Big releases: a changeset body can be a full markdown section — use
- **bold sub-headings** + bullets, never `#`/`##` headings (they end up
- nested inside a changelog list item).
-- Breaking changes: include the before/after surface in the body.
-- Don't duplicate content across changesets — every changeset in the
- release lands in the same version section.
-- Attribution is automatic via changelog-github; don't hand-write
- `Thanks @...` lines.
-
-### When drafting a release-spanning changeset from `git log`
-
-- Look at `git diff v..HEAD -- README.md` first — best single view of user-facing changes.
-- Read commits in bulk (`git log --oneline v..HEAD -- apps/cli apps/local packages`), bucket by theme, then write prose.
-- Merged PRs without changesets still ship in the release — their content
- ships regardless; only the changelog text is driven by changesets. If
- something important landed without a changeset, fold its story into a
- release-summary changeset.
-
-## Beta release flow
+```sh
+git fetch origin main
+sha="$(git rev-parse origin/main)"
+version="$(git show "$sha:Cargo.toml" | python3 -c 'import sys,tomllib; print(tomllib.load(sys.stdin.buffer)["package"]["version"])')"
+tag="v$version"
+gh workflow run release.yml --ref main \
+ -f mode=dry-run \
+ -f tag="$tag" \
+ -f commit_sha="$sha"
```
-git checkout -b rs/beta-v-start
-bun run release:beta:start # creates .changeset/pre.json
-# write .changeset/executor--beta.md (frontmatter + user-facing body)
-git add ... && git commit # ONLY when owner says commit
-git push -u origin rs/beta-v-start
-# Open PR -> merge -> release.yml opens "Version Packages (beta)" PR -> merge to publish
-```
-
-- Published under npm dist-tag `beta`.
-- Users install: `npm i -g executor@beta`.
-- Exit the train with `bun run release:beta:stop` when going back to stable.
-
-## Stable release flow
-
-Identical to beta except skip `release:beta:start`/`stop`. Changesets produce a normal `Version Packages` PR; merging publishes under `latest`.
-
-## Owner preferences (hard rules)
-- **Never commit until the owner explicitly says so.** Set everything up in the working tree, run `git status`, and stop.
-- **No AI / Claude / Anthropic / Co-Authored-By trailers** in commits, commit messages, PRs, or any generated file. This is in `CLAUDE.md` — do not violate.
-- **Branch naming**: `rs/` for Rhys's branches. Beta-start branch: `rs/beta-v-start`.
-- **Remote**: `origin` = `https://github.com/RhysSullivan/executor.git`. If another remote appears (e.g. a fork remote), ask whether to remove it.
-- **Dirty working tree**: if there are uncommitted changes when starting a release, ask whether to include them, stash them, or commit separately first. Don't sweep them into the release commit silently.
-- **Don't estimate time** — code is cheap to write. Focus on what to do, not how long it takes.
-- **Fact-check scope claims** before publishing. If release notes say "does not affect X", verify by reading the diff.
-
-## Common commands
-
-```
-bun run changeset # interactive; or write .changeset/*.md directly
-bun run lint:changelog-stubs --fix # seed missing per-package CHANGELOG.md files
-bun run release:beta:start # enter prerelease
-bun run release:beta:stop # exit prerelease
-bun run release:publish:dry-run # build full CLI payload without publishing
-bun run release:publish:packages:dry-run # pack @executor-js/* without publishing
-bun run release:check # invoked by publish workflow
-```
+Create and push the tag only after the dry run succeeds, then dispatch the same
+workflow with `mode=publish`. Follow the exact tag verification sequence in
+`RELEASING.md`.
-## What the workflow does after merge to `main`
+## Retired package release paths
-1. `.github/workflows/release.yml` opens/updates a `Version Packages` PR.
-2. Merging that PR:
- - Publishes every `@executor-js/*` library that's not yet on npm (via `scripts/publish-packages.ts`).
- - If `apps/cli/package.json` bumped, tags the commit and dispatches `publish-executor-package.yml`, which runs `release:check`, does a full dry-run build, publishes the CLI to npm, and creates/updates the GitHub Release with binary assets.
+The TypeScript workspaces are private implementation packages used by the
+active product and its retained source. This fork does not publish them to npm.
+The old npm workflows, publisher scripts, and Changesets release machinery are
+retired. Do not recreate a package release path from archived source or
+upstream package metadata.
-## Fallback behavior
+## Owner rules
-If something is unclear (bump level, whether to include in-flight work, whether to push), **ask the owner**. A release is a high-blast-radius action; one clarifying question is cheaper than a rogue publish.
+- Never commit, push, tag, dispatch publish mode, or publish without explicit
+ owner approval.
+- Never add AI assistant attribution or co-author trailers.
+- Preserve unrelated dirty worktree changes.
+- Verify version, tag, commit, workflow inputs, and remote state before
+ describing a release as ready.
+- Ask one question when release scope or publish authority is unclear.
diff --git a/.skills/warden-security-review/SKILL.md b/.skills/warden-security-review/SKILL.md
index 83555b865..f403c9bcb 100644
--- a/.skills/warden-security-review/SKILL.md
+++ b/.skills/warden-security-review/SKILL.md
@@ -23,6 +23,13 @@ npm exec --yes --package=@sentry/warden -- warden --help
The repo has a `warden.toml` that uses remote skills from `getsentry/warden-skills`.
+Before scanning, verify that every configured target resolves to at least one
+non-ignored file:
+
+```bash
+bun run lint:warden-targets
+```
+
Reference skills are mirrored under `.reference/warden-skills` when needed. `.reference/` is gitignored.
## Local Outputs
@@ -42,22 +49,29 @@ Warden may not treat bare directories as recursive targets. Prefer explicit quot
## Recommended Scans
-Authz on cloud/API surfaces:
+Authz on the active Rust API and OAuth surfaces plus archived opt-in API
+surfaces:
```bash
npm exec --yes --package=@sentry/warden -- \
- warden "apps/cloud/src/auth/**/*.ts" "apps/cloud/src/api/**/*.ts" \
- "apps/cloud/src/routes/**/*.tsx" "packages/core/api/src/**/*.ts" \
+ warden "src/api.rs" "src/api/**/*.rs" "src/oauth/**/*.rs" \
+ "legacy/cloud/src/auth/**/*.ts" "legacy/cloud/src/api/**/*.ts" \
+ "legacy/cloud/src/routes/**/*.tsx" "packages/core/api/src/**/*.ts" \
--skill wrdn-authz --fail-on off --report-on low --min-confidence low \
--parallel 2 --log -o .warden-runs/authz.jsonl
```
Code execution on sink-bearing runtime/plugin files:
+The archived local runtime remains in scope because explicit legacy development
+and test commands can still execute it. No release workflow publishes it. Its
+server code lives directly under `legacy/local/src`, not a `src/server`
+subdirectory.
+
```bash
rg -l "\b(exec|spawn|execFile|fork|subprocess|Deno\.Command|new Function|eval\(|vm\.|QuickJS|quickjs|Worker\(|import\(|compile|instantiate|runIn|shell|command|child_process)\b" \
- apps/local/src/server apps/cli/src packages/core/execution/src packages/core/sdk/src packages/kernel packages/plugins \
- -g "*.ts" -g "*.tsx" -g "!*.test.ts" -g "!*.spec.ts" -g "!*.e2e.ts" -g "!**/dist/**" -g "!**/node_modules/**" \
+ src legacy/local/src legacy/cli/src packages/core/execution/src packages/core/sdk/src packages/kernel packages/plugins \
+ -g "*.rs" -g "*.ts" -g "*.tsx" -g "!*.test.ts" -g "!*.spec.ts" -g "!*.e2e.ts" -g "!**/dist/**" -g "!**/node_modules/**" \
> .warden-runs/code-execution-targets.txt
npm exec --yes --package=@sentry/warden -- \
@@ -69,15 +83,12 @@ npm exec --yes --package=@sentry/warden -- \
Data exfiltration on backend/API/storage/plugin SDK surfaces:
```bash
-find apps/cloud/src/api apps/cloud/src/auth apps/local/src/server \
- packages/core/api/src packages/core/storage-core/src packages/core/storage-file/src \
- packages/core/storage-postgres/src packages/core/storage-drizzle/src \
- packages/plugins/mcp/src packages/plugins/openapi/src packages/plugins/graphql/src \
- packages/plugins/google-discovery/src packages/plugins/oauth2/src \
- packages/plugins/onepassword/src packages/plugins/workos-vault/src \
- packages/plugins/file-secrets/src packages/plugins/keychain/src \
- -type f \( -name "*.ts" -o -name "*.tsx" \) |
- rg -v '(\.test\.|\.spec\.|\.e2e\.|dist/|node_modules/|embedded-migrations\.gen\.ts|/react/)' \
+find src/api src/catalog src/invocation src/mcp src/oauth src/protocols src/runtime \
+ src/api.rs src/database.rs src/outbound.rs src/request_logs.rs \
+ legacy/cloud/src/api legacy/cloud/src/auth legacy/cloud/src/routes legacy/local/src \
+ packages/core/api/src packages/plugins packages/react/src/api \
+ -type f \( -name "*.rs" -o -name "*.ts" -o -name "*.tsx" \) |
+ rg -v '(\.test\.|\.spec\.|\.e2e\.|dist/|node_modules/|embedded-migrations\.gen\.ts)' \
> .warden-runs/exfil-targets-focused.txt
npm exec --yes --package=@sentry/warden -- \
@@ -109,12 +120,9 @@ For each candidate:
- Determine what data returns to the caller: raw body, parsed fields, typed error message, timing/status oracle, or no observable data.
- State confidence and deployment caveats.
-## Current Known Findings
-
-As of the Warden pass on 2026-04-29:
-
-- Real: authenticated SSRF in plugin/source setup URL fetching for OpenAPI, Google Discovery, GraphQL, and MCP remote endpoints.
-- Real: mutable third-party GitHub Actions refs in publish/release workflows, especially `oven-sh/setup-bun@v2` and `changesets/action@v1`.
-- Clean in that pass: authz scan on cloud auth/API/core API surfaces; code-execution scan on narrowed CLI/runtime/kernel/plugin sink files.
+## Result freshness
-Do not claim the whole codebase is secure from those clean runs. They are scoped scanner results.
+Do not carry old findings or clean claims forward without rerunning the current
+targets. Report the exact commit, skill, target list, confidence threshold, and
+output artifact for every result. A clean scoped pass is not evidence that the
+whole codebase is secure.
diff --git a/AGENTS.md b/AGENTS.md
index 1c0dd1661..c9c7a8788 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -115,10 +115,16 @@ before using it.
- `packages/plugins/*`: protocol and provider plugins. Plugin-specific
runtime, React, API, and testing helpers should live with the owning plugin.
- `packages/react`: shared React UI and atom/client integration.
+- `packages/app`: shared React application shell and Vite composition.
- `packages/hosts/mcp`: MCP host surface for exposing Executor through MCP.
- `packages/kernel/*`: execution runtimes and code execution substrate.
-- `apps/local`, `apps/cloud`, `apps/cli`, and `apps/desktop`: product entry
- points that compose the packages.
+- `packages/core/execution` and `packages/core/cli`: shared execution and
+ configuration tooling retained for package compatibility.
+- `src` and `web`: the active local/self-hosted Rust and Svelte product.
+- `legacy/cli` and `legacy/local`: archived TypeScript compatibility entry
+ points.
+- `legacy/cloud`, `legacy/desktop`, `legacy/host-cloudflare`, and
+ `legacy/host-selfhost`: archived deployment entry points.
## Other
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 000000000..9b0d766c4
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,4028 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "serde",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures",
+ "password-hash",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "borrow-or-share"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead",
+ "chacha20",
+ "cipher",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+ "zeroize",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cow-utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "dragonbox_ecma"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf"
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "executor"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "argon2",
+ "async-trait",
+ "axum",
+ "base64",
+ "chacha20poly1305",
+ "clap",
+ "directories",
+ "hkdf",
+ "hmac",
+ "http-body-util",
+ "ipnet",
+ "jsonschema",
+ "libc",
+ "oxc",
+ "percent-encoding",
+ "rand 0.8.6",
+ "reqwest",
+ "rmcp",
+ "rquickjs",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "sha2",
+ "sqlx",
+ "subtle",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tower",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "fancy-regex"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277"
+dependencies = [
+ "bit-set",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fluent-uri"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fraction"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "hmac-sha1-compact"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823"
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots 1.0.8",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-escape-simd"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e770254dd7802184595b1d30da2a15cb72569e2aca2b177aef8d22eac8a693"
+
+[[package]]
+name = "jsonschema"
+version = "0.46.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8374249b1bdce1c1773a09fa294347e19e4bfeb86d845c2d8ebaf8a8dbf11233"
+dependencies = [
+ "ahash",
+ "bytecount",
+ "data-encoding",
+ "email_address",
+ "fancy-regex",
+ "fraction",
+ "getrandom 0.3.4",
+ "idna",
+ "itoa",
+ "num-cmp",
+ "num-traits",
+ "percent-encoding",
+ "referencing",
+ "regex",
+ "regex-syntax",
+ "serde",
+ "serde_json",
+ "unicode-general-category",
+ "uuid-simd",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
+dependencies = [
+ "bitflags",
+ "libc",
+ "plain",
+ "redox_syscall 0.8.1",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "micromap"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nonmax"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
+dependencies = [
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.6",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-cmp"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
+[[package]]
+name = "owo-colors"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
+
+[[package]]
+name = "oxc"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c837e177cfe9e45dc8b474b16565046b673e5e2e8c2a2aece798ff9a1e104602"
+dependencies = [
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_codegen",
+ "oxc_diagnostics",
+ "oxc_parser",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_syntax",
+ "oxc_transformer",
+ "oxc_transformer_plugins",
+]
+
+[[package]]
+name = "oxc-browserslist"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b87743c2f6963036971ca587539cd9235693d953dc018a734014241afb20d5b"
+dependencies = [
+ "flate2",
+ "postcard",
+ "rustc-hash",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "oxc-miette"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b776084bf11ad750806cb63f9ed2606a12ab374cf458f36a7731eba1f5d753fc"
+dependencies = [
+ "cfg-if",
+ "owo-colors",
+ "oxc-miette-derive",
+ "textwrap",
+ "thiserror",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "oxc-miette-derive"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66e0bafc397eee2f94c5bf89bb5a82ba6d522d1b79e2924c8169f053febb433f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "oxc_allocator"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b34ec71e68e73a0ed7d929e58ab2bb4f58752dea944c08f14ccc3b8272c77946"
+dependencies = [
+ "allocator-api2",
+ "hashbrown 0.17.1",
+ "oxc_data_structures",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_ast"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa406c62bb247767a6a051be6ac3be7db6c4f818129ccae570da6ee9eb96ead0"
+dependencies = [
+ "bitflags",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_estree",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_ast_macros"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3706e193b659865e03155a44e443f0447abf6789f9a3eb436ae10e557afab899"
+dependencies = [
+ "phf",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "oxc_ast_visit"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5353724c6c835bfd37ed59db65ccd506b7721598bcbe4750eda165754d105463"
+dependencies = [
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_codegen"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d57d63987a27e8e67bc40518a4133b96ad2c0475f4735b8110e577a25680a09e"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "dragonbox_ecma",
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_index",
+ "oxc_semantic",
+ "oxc_sourcemap",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_compat"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a25f516a7f4c05f66da375d07b29a87fe0e6037c79374f409153cb9496f3e0"
+dependencies = [
+ "cow-utils",
+ "oxc-browserslist",
+ "oxc_syntax",
+ "rustc-hash",
+ "serde",
+]
+
+[[package]]
+name = "oxc_data_structures"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fde9ca13ef89018fb4c18502cefb9668baac6066905d27a914c29fb5fae1c8ac"
+dependencies = [
+ "ropey",
+]
+
+[[package]]
+name = "oxc_diagnostics"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cbafa6cc3c38d72e35cd9337a38a4558e9fee4521fa5cabe321b90519e80aa"
+dependencies = [
+ "cow-utils",
+ "oxc-miette",
+ "percent-encoding",
+]
+
+[[package]]
+name = "oxc_ecmascript"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55408e4f458ec4b9cd842f67364e9395b28bdf2201c7755ed04d6707c1084ea"
+dependencies = [
+ "cow-utils",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_estree"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6c062d52b9ff15d118b87679bdd0a05c43599c1d55fd1cab280da1cc822858"
+
+[[package]]
+name = "oxc_index"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "191884bee6c3744909a51acc7d78d4ae370d817b25875b10642f632327b6296e"
+dependencies = [
+ "nonmax",
+ "serde",
+]
+
+[[package]]
+name = "oxc_parser"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6efc796fc0c65a2a6bd40984b65b1052b5550b017af303f02b08794e304c14c8"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "memchr",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash",
+ "seq-macro",
+]
+
+[[package]]
+name = "oxc_regular_expression"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e533f1345534dceaee6c2c7102e5f3d2e7466269199eca9abeb871e02a7496a4"
+dependencies = [
+ "bitflags",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_diagnostics",
+ "oxc_span",
+ "oxc_str",
+ "phf",
+ "rustc-hash",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_semantic"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de31d2015099e33ecdc23ad59730b768a141a0b2941d43970a26ba50de003e10"
+dependencies = [
+ "itertools",
+ "memchr",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_index",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash",
+ "self_cell",
+ "smallvec",
+]
+
+[[package]]
+name = "oxc_sourcemap"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ac6db7d8c33f46a0b9ebb702c077364abcd6b64c3a3a97bb86b9f5b3554833c"
+dependencies = [
+ "base64-simd",
+ "json-escape-simd",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "oxc_span"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5245fa99a7c9405d57e2d9666afc9fd01ac644e37aff1cb3e1cedd50c17915"
+dependencies = [
+ "compact_str",
+ "oxc-miette",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+ "oxc_str",
+]
+
+[[package]]
+name = "oxc_str"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c61444d5681ce805fbbb5dc0498d980685a4c6a82dd57c2e17ff948041c1fb"
+dependencies = [
+ "compact_str",
+ "hashbrown 0.17.1",
+ "oxc_allocator",
+ "oxc_estree",
+]
+
+[[package]]
+name = "oxc_syntax"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "296b94ecc1235a2b5ea2a47412d4bc573b7a25a93132a1a2f49c617d85b58995"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "dragonbox_ecma",
+ "nonmax",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+ "oxc_index",
+ "oxc_span",
+ "oxc_str",
+ "phf",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_transformer"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494dcdb3e8c8dcb50e2ce3ba238030c9116a8b47b54f55c0f0211d68ae66b183"
+dependencies = [
+ "base64",
+ "compact_str",
+ "hmac-sha1-compact",
+ "indexmap",
+ "itoa",
+ "memchr",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_compat",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_regular_expression",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "oxc_traverse",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "oxc_transformer_plugins"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6ce80c16ee776e23a0bcb472153db9ec66c59b709fcdb63f43f7623cdfadce7"
+dependencies = [
+ "cow-utils",
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_parser",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "oxc_transformer",
+ "oxc_traverse",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_traverse"
+version = "0.137.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da52a3734ef3ad4c10ebb6f4e768f63ea1597677d53785e3ceb26d9735f5c1f8"
+dependencies = [
+ "itoa",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_ast_visit",
+ "oxc_data_structures",
+ "oxc_ecmascript",
+ "oxc_semantic",
+ "oxc_span",
+ "oxc_str",
+ "oxc_syntax",
+ "rustc-hash",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "referencing"
+version = "0.46.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65a910f9d63351f9c9d7dc3053d02b214ea3a005917140c70087d7b59cbc5bb1"
+dependencies = [
+ "ahash",
+ "fluent-uri",
+ "getrandom 0.3.4",
+ "hashbrown 0.16.1",
+ "itoa",
+ "micromap",
+ "parking_lot",
+ "percent-encoding",
+ "serde_json",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "relative-path"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots 1.0.8",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rmcp"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "futures",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "ropey"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
+dependencies = [
+ "smallvec",
+ "str_indices",
+]
+
+[[package]]
+name = "rquickjs"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0688f8b0192998cca685adefdfad3483da295fa40a0ec406b4c14ecd729e858"
+dependencies = [
+ "rquickjs-core",
+]
+
+[[package]]
+name = "rquickjs-core"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fee8c5383f0cfda3b980a80ca4520e726e09b593c59562f579daa51b6c20411"
+dependencies = [
+ "async-lock",
+ "hashbrown 0.17.1",
+ "relative-path",
+ "rquickjs-sys",
+]
+
+[[package]]
+name = "rquickjs-sys"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "698077537c286a169de8693b216672bcef148bf2e2e112ebf50758c68e9afa09"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "smawk"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+dependencies = [
+ "base64",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.5",
+ "hashlink",
+ "indexmap",
+ "log",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rustls",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-sqlite",
+ "syn",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand 0.8.6",
+ "rsa",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand 0.8.6",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "str_indices"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-general-category"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
+
+[[package]]
+name = "unicode-id-start"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "uuid-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.8",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "whoami"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+dependencies = [
+ "libredox",
+ "wasite",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 000000000..c033263c7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,70 @@
+[package]
+name = "executor"
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.96"
+license = "MIT"
+build = "build.rs"
+
+[dependencies]
+anyhow = "1"
+argon2 = "0.5"
+async-trait = "0.1"
+axum = "0.8"
+base64 = "0.22"
+chacha20poly1305 = "0.10"
+clap = { version = "4", features = ["derive", "env"] }
+directories = "6"
+hkdf = "0.12"
+hmac = "0.12"
+ipnet = "2.12.0"
+jsonschema = { version = "0.46.6", default-features = false }
+libc = "0.2"
+oxc = { version = "=0.137.0", default-features = false, features = [
+ "semantic",
+ "transformer",
+ "codegen",
+ "ast_visit",
+] }
+percent-encoding = "2"
+rand = "0.8.5"
+reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
+rmcp = { version = "=1.8.0", default-features = false }
+rquickjs = { version = "=0.12.0", default-features = false, features = ["std", "futures"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+serde_yaml = "0.9"
+sha2 = "0.10"
+sqlx = { version = "0.8", default-features = false, features = [
+ "runtime-tokio-rustls",
+ "sqlite",
+ "migrate",
+ "macros",
+] }
+subtle = "2"
+thiserror = "2"
+tokio = { version = "1", features = [
+ "macros",
+ "rt-multi-thread",
+ "net",
+ "signal",
+ "time",
+ "fs",
+ "sync",
+ "process",
+ "io-util",
+ "io-std",
+] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+url = "2"
+uuid = { version = "1", features = ["v4", "serde"] }
+
+[dev-dependencies]
+http-body-util = "0.1"
+tempfile = "3"
+tower = { version = "0.5", features = ["util"] }
+
+[build-dependencies]
+base64 = "0.22"
+sha2 = "0.10"
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..6b1d975b8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,125 @@
+# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e
+
+ARG SOURCE_DATE_EPOCH
+
+FROM oven/bun:1.3.11@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS web-builder
+
+ARG TARGETARCH
+
+WORKDIR /build
+
+COPY package.json bun.lock ./
+COPY patches ./patches
+COPY apps ./apps
+COPY e2e ./e2e
+COPY examples ./examples
+COPY legacy ./legacy
+COPY packages ./packages
+COPY web/package.json ./web/package.json
+
+RUN --mount=type=cache,id=executor-bun-${TARGETARCH},target=/root/.bun/install/cache,sharing=locked \
+ bun install --frozen-lockfile --ignore-scripts --filter @executor-js/web
+
+COPY web ./web
+RUN bun run --cwd web build
+
+
+FROM rust:1.96-bookworm@sha256:6d19f49541d185805745b8baa781b1fd482118c81a3154510ee18dcce985d005 AS rust-builder
+
+ARG TARGETARCH
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock build.rs LICENSE about.toml about.hbs ./
+COPY migrations ./migrations
+COPY packaging/launchd/bounded-log.sh ./packaging/launchd/bounded-log.sh
+COPY packaging/launchd/dev.executor.gateway.plist ./packaging/launchd/dev.executor.gateway.plist
+COPY packaging/systemd/executor.env.example ./packaging/systemd/executor.env.example
+COPY packaging/systemd/executor.service ./packaging/systemd/executor.service
+COPY scripts/install-launchd.sh ./scripts/install-launchd.sh
+COPY scripts/install-systemd.sh ./scripts/install-systemd.sh
+COPY scripts/lib/install-systemd-master-key.sh ./scripts/lib/install-systemd-master-key.sh
+COPY src ./src
+COPY --from=web-builder /build/web/build ./web/build
+
+RUN --mount=type=cache,id=executor-cargo-registry-${TARGETARCH},target=/usr/local/cargo/registry,sharing=locked \
+ --mount=type=cache,id=executor-cargo-target-${TARGETARCH},target=/build/target,sharing=locked \
+ cargo install cargo-about --version 0.9.0 --locked --features cli && \
+ cargo about generate about.hbs > THIRD_PARTY_LICENSES.html && \
+ cargo build --locked --release && \
+ cp target/release/executor /usr/local/bin/executor
+
+
+FROM debian:bookworm-20260623-slim@sha256:60eac759739651111db372c07be67863818726f754804b8707c90979bda511df AS runtime-base
+
+ARG SOURCE_DATE_EPOCH
+ARG DEBIAN_SNAPSHOT=20260623T000000Z
+ARG EXECUTOR_UID=10001
+ARG EXECUTOR_GID=10001
+
+RUN printf '%s\n' \
+ 'Types: deb' \
+ "URIs: http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}" \
+ 'Suites: bookworm bookworm-updates' \
+ 'Components: main' \
+ 'Check-Valid-Until: no' \
+ '' \
+ 'Types: deb' \
+ "URIs: http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}" \
+ 'Suites: bookworm-security' \
+ 'Components: main' \
+ 'Check-Valid-Until: no' \
+ > /etc/apt/sources.list.d/debian.sources && \
+ apt-get update && \
+ apt-get install --yes --no-install-recommends ca-certificates curl && \
+ rm -rf \
+ /var/cache/ldconfig/aux-cache \
+ /var/lib/apt/lists/* \
+ /var/log/apt/* \
+ /var/log/dpkg.log && \
+ groupadd --gid "${EXECUTOR_GID}" executor && \
+ useradd --uid "${EXECUTOR_UID}" --gid executor --no-create-home \
+ --home-dir /var/lib/executor --shell /usr/sbin/nologin executor && \
+ chage --lastday \
+ "$(date --utc --date="@${SOURCE_DATE_EPOCH:-0}" +%Y-%m-%d)" \
+ executor && \
+ install --directory --owner executor --group executor --mode 0700 \
+ /var/lib/executor && \
+ install --directory --owner root --group executor --mode 0750 \
+ /etc/executor
+
+ENV EXECUTOR_DATA_DIR=/var/lib/executor \
+ EXECUTOR_PUBLIC_ORIGIN=http://127.0.0.1:4788 \
+ RUST_LOG=executor=info
+
+USER executor:executor
+WORKDIR /var/lib/executor
+
+EXPOSE 4788
+VOLUME ["/var/lib/executor"]
+STOPSIGNAL SIGTERM
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+ CMD ["curl", "--fail", "--silent", "--show-error", "http://127.0.0.1:4788/healthz"]
+
+ENTRYPOINT ["/usr/local/bin/executor"]
+CMD ["server", "--bind", "0.0.0.0:4788", "--allow-unsafe-http-non-loopback"]
+
+
+# Release automation supplies the extracted Linux archive as the named
+# prebuilt-executor context. This target never compiles web or Rust code.
+FROM runtime-base AS runtime-prebuilt
+
+COPY --from=prebuilt-executor --chmod=0755 /executor /usr/local/bin/executor
+COPY --from=prebuilt-executor --chmod=0644 /LICENSE /usr/share/licenses/executor/LICENSE
+COPY --from=prebuilt-executor --chmod=0644 /THIRD_PARTY_LICENSES.html /usr/share/licenses/executor/THIRD_PARTY_LICENSES.html
+COPY --from=prebuilt-executor --chmod=0644 /THIRD_PARTY_JAVASCRIPT_LICENSES.json /usr/share/licenses/executor/THIRD_PARTY_JAVASCRIPT_LICENSES.json
+
+
+# Keep the default target self-contained for local and CI builds.
+FROM runtime-base AS runtime
+
+COPY --from=rust-builder /usr/local/bin/executor /usr/local/bin/executor
+COPY --from=rust-builder /build/LICENSE /usr/share/licenses/executor/LICENSE
+COPY --from=rust-builder /build/THIRD_PARTY_LICENSES.html /usr/share/licenses/executor/THIRD_PARTY_LICENSES.html
+COPY --from=rust-builder /build/web/build/THIRD_PARTY_JAVASCRIPT_LICENSES.json /usr/share/licenses/executor/THIRD_PARTY_JAVASCRIPT_LICENSES.json
diff --git a/Dockerfile.dockerignore b/Dockerfile.dockerignore
new file mode 100644
index 000000000..d314f70d4
--- /dev/null
+++ b/Dockerfile.dockerignore
@@ -0,0 +1,69 @@
+**
+
+!Dockerfile
+!Cargo.toml
+!Cargo.lock
+!build.rs
+!package.json
+!bun.lock
+!LICENSE
+!about.toml
+!about.hbs
+
+# Bun validates the complete workspace graph during a frozen filtered install.
+# Keep these roots manifest-only so application source changes do not invalidate
+# the dependency layer.
+!apps
+apps/**
+!apps/*/package.json
+
+!e2e
+e2e/**
+!e2e/package.json
+
+!examples
+examples/**
+!examples/*/package.json
+
+!legacy
+legacy/**
+!legacy/*/package.json
+
+!packages
+packages/**
+!packages/*/*/package.json
+!packages/app/package.json
+!packages/react/package.json
+
+!migrations
+!migrations/**
+!packaging
+packaging/**
+!packaging/launchd
+!packaging/launchd/bounded-log.sh
+!packaging/launchd/dev.executor.gateway.plist
+!packaging/systemd
+!packaging/systemd/executor.env.example
+!packaging/systemd/executor.service
+!patches
+!patches/**
+!scripts
+scripts/**
+!scripts/install-launchd.sh
+!scripts/install-systemd.sh
+!scripts/package-release-archive.py
+!scripts/lib
+!scripts/lib/install-systemd-master-key.sh
+!src
+!src/**
+!web
+!web/**
+
+web/build
+web/.svelte-kit
+web/node_modules
+web/**/.env*
+web/**/*.key
+web/**/*.pem
+web/**/*.db*
+web/**/*secret*
diff --git a/Dockerfile.release-native b/Dockerfile.release-native
new file mode 100644
index 000000000..f1a81320f
--- /dev/null
+++ b/Dockerfile.release-native
@@ -0,0 +1,79 @@
+# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e
+
+ARG SOURCE_DATE_EPOCH
+
+FROM ubuntu:22.04@sha256:4f838adc7181d9039ac795a7d0aba05a9bd9ecd480d294483169c5def983b64d AS builder
+
+ARG RUST_TARGET
+ARG RUST_VERSION=1.96.0
+ARG SOURCE_DATE_EPOCH
+ARG TARGETARCH
+ARG UBUNTU_SNAPSHOT=20260623T000000Z
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN sed -i -E \
+ "s|^deb |deb [snapshot=${UBUNTU_SNAPSHOT}] |" \
+ /etc/apt/sources.list && \
+ apt-get -o Acquire::Check-Valid-Until=false update && \
+ apt-get -o APT::Get::Always-Include-Phased-Updates=true install \
+ --yes --no-install-recommends \
+ build-essential \
+ ca-certificates \
+ curl \
+ pkg-config \
+ xz-utils && \
+ rm -rf \
+ /var/cache/ldconfig/aux-cache \
+ /var/lib/apt/lists/* \
+ /var/log/apt/* \
+ /var/log/dpkg.log
+
+RUN case "$TARGETARCH" in \
+ amd64) \
+ expected_target=x86_64-unknown-linux-gnu; \
+ checksum=c295047583a56238ea06b43f849f4b877fa12bfd4c7103f8d9a74c94c9c4e108 \
+ ;; \
+ arm64) \
+ expected_target=aarch64-unknown-linux-gnu; \
+ checksum=371eadcca97062219cbd8593628eb5d2802bc370515d085fedce1b56b2baed57 \
+ ;; \
+ *) \
+ echo "Unsupported release architecture: $TARGETARCH" >&2; \
+ exit 1 \
+ ;; \
+ esac && \
+ test "$RUST_TARGET" = "$expected_target" && \
+ archive="rust-${RUST_VERSION}-${RUST_TARGET}.tar.xz" && \
+ curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
+ --max-filesize 268435456 \
+ --output "/tmp/$archive" \
+ "https://static.rust-lang.org/dist/$archive" && \
+ printf '%s %s\n' "$checksum" "/tmp/$archive" | sha256sum --check - && \
+ tar -xJf "/tmp/$archive" -C /tmp && \
+ "/tmp/rust-${RUST_VERSION}-${RUST_TARGET}/install.sh" \
+ --prefix=/usr/local \
+ --disable-ldconfig && \
+ rm -rf "/tmp/$archive" "/tmp/rust-${RUST_VERSION}-${RUST_TARGET}"
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock build.rs ./
+COPY migrations ./migrations
+COPY packaging/launchd/bounded-log.sh ./packaging/launchd/bounded-log.sh
+COPY packaging/launchd/dev.executor.gateway.plist ./packaging/launchd/dev.executor.gateway.plist
+COPY packaging/systemd/executor.env.example ./packaging/systemd/executor.env.example
+COPY packaging/systemd/executor.service ./packaging/systemd/executor.service
+COPY scripts/install-launchd.sh ./scripts/install-launchd.sh
+COPY scripts/install-systemd.sh ./scripts/install-systemd.sh
+COPY scripts/lib/install-systemd-master-key.sh ./scripts/lib/install-systemd-master-key.sh
+COPY src ./src
+COPY --from=release-web / ./web/build
+
+RUN cargo build --locked --release --target "$RUST_TARGET"
+
+FROM scratch AS artifact
+
+ARG RUST_TARGET
+
+COPY --from=builder "/build/target/${RUST_TARGET}/release/executor" /executor
diff --git a/Dockerfile.release-package b/Dockerfile.release-package
new file mode 100644
index 000000000..3a897fa55
--- /dev/null
+++ b/Dockerfile.release-package
@@ -0,0 +1,83 @@
+# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e
+
+FROM ubuntu:22.04@sha256:4f838adc7181d9039ac795a7d0aba05a9bd9ecd480d294483169c5def983b64d AS packager
+
+ARG SOURCE_DATE_EPOCH
+ARG BUILDKIT_IMAGE
+ARG BUILDX_VERSION
+ARG MACOS_BUILD_CLANG_VERSION
+ARG MACOS_BUILD_LD_VERSION
+ARG MACOS_BUILD_SDK_VERSION
+ARG MACOS_BUILD_XCODE_BUILD
+ARG MACOS_BUILD_XCODE_VERSION
+ARG MACOS_FLOOR_CLANG_VERSION
+ARG MACOS_FLOOR_LD_VERSION
+ARG MACOS_FLOOR_SDK_VERSION
+ARG MACOS_FLOOR_XCODE_BUILD
+ARG MACOS_FLOOR_XCODE_VERSION
+ARG REPRODUCIBILITY_RUN
+ARG UBUNTU_SNAPSHOT=20260623T000000Z
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN sed -i -E \
+ "s|^deb |deb [snapshot=${UBUNTU_SNAPSHOT}] |" \
+ /etc/apt/sources.list && \
+ apt-get -o Acquire::Check-Valid-Until=false update && \
+ apt-get -o APT::Get::Always-Include-Phased-Updates=true install \
+ --yes --no-install-recommends \
+ python3-minimal && \
+ rm -rf \
+ /var/cache/ldconfig/aux-cache \
+ /var/lib/apt/lists/* \
+ /var/log/apt/* \
+ /var/log/dpkg.log
+
+COPY scripts/package-release-archive.py /usr/local/bin/package-release-archive.py
+COPY --from=release-inputs / /release-inputs
+
+RUN set -eux; \
+ test -n "$SOURCE_DATE_EPOCH"; \
+ test -n "$REPRODUCIBILITY_RUN"; \
+ mkdir -p /output /staging; \
+ { \
+ printf 'buildx=%s\n' "$BUILDX_VERSION"; \
+ printf 'buildkit=%s\n' "$BUILDKIT_IMAGE"; \
+ printf 'macos-release-xcode=%s (%s)\n' "$MACOS_BUILD_XCODE_VERSION" "$MACOS_BUILD_XCODE_BUILD"; \
+ printf 'macos-release-sdk=%s\n' "$MACOS_BUILD_SDK_VERSION"; \
+ printf 'macos-release-clang=%s\n' "$MACOS_BUILD_CLANG_VERSION"; \
+ printf 'macos-release-ld=%s\n' "$MACOS_BUILD_LD_VERSION"; \
+ printf 'macos-floor-xcode=%s (%s)\n' "$MACOS_FLOOR_XCODE_VERSION" "$MACOS_FLOOR_XCODE_BUILD"; \
+ printf 'macos-floor-sdk=%s\n' "$MACOS_FLOOR_SDK_VERSION"; \
+ printf 'macos-floor-clang=%s\n' "$MACOS_FLOOR_CLANG_VERSION"; \
+ printf 'macos-floor-ld=%s\n' "$MACOS_FLOOR_LD_VERSION"; \
+ printf 'ubuntu-packager=22.04@sha256:4f838adc7181d9039ac795a7d0aba05a9bd9ecd480d294483169c5def983b64d\n'; \
+ printf 'ubuntu-snapshot=%s\n' "$UBUNTU_SNAPSHOT"; \
+ python3 -c 'import sys, zlib; print(f"python={sys.version.split()[0]}"); print(f"zlib-build={zlib.ZLIB_VERSION}"); print(f"zlib-runtime={zlib.ZLIB_RUNTIME_VERSION}")'; \
+ } > /output/BUILD-TOOLCHAINS.txt; \
+ for target in \
+ aarch64-apple-darwin \
+ aarch64-unknown-linux-gnu \
+ x86_64-apple-darwin \
+ x86_64-unknown-linux-gnu; do \
+ stage="/staging/$target"; \
+ archive="executor-${target}.tar.gz"; \
+ mkdir "$stage"; \
+ install -m 0755 "/release-inputs/native/$target/executor" "$stage/executor"; \
+ install -m 0644 /release-inputs/LICENSE "$stage/LICENSE"; \
+ install -m 0644 \
+ /release-inputs/THIRD_PARTY_LICENSES.html \
+ "$stage/THIRD_PARTY_LICENSES.html"; \
+ install -m 0644 \
+ /release-inputs/THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ "$stage/THIRD_PARTY_JAVASCRIPT_LICENSES.json"; \
+ python3 /usr/local/bin/package-release-archive.py \
+ --source "$stage" \
+ --output "/output/$archive" \
+ --epoch "$SOURCE_DATE_EPOCH"; \
+ (cd /output && sha256sum "$archive" > "$archive.sha256"); \
+ done
+
+FROM scratch AS artifacts
+
+COPY --from=packager /output/ /
diff --git a/README.md b/README.md
index 5eabe62c6..75ef7229b 100644
--- a/README.md
+++ b/README.md
@@ -1,184 +1,191 @@
-# executor
+# Executor
-[https://github.com/user-attachments/assets/11225f83-e848-42ba-99b2-a993bcc88dad](https://github.com/user-attachments/assets/11225f83-e848-42ba-99b2-a993bcc88dad)
+Executor is a single-user, self-hosted tool gateway for AI agents. One Rust
+binary serves the Svelte dashboard, stores state in SQLite, exposes an MCP
+endpoint, and runs concurrent TypeScript tool workflows in isolated QuickJS
+workers.
-The integration layer for AI agents. One catalog for every tool, shared across every agent you use.
+The active product supports:
-[Ask DeepWiki](https://deepwiki.com/RhysSullivan/executor)
+- OpenAPI, GraphQL, and MCP sources
+- API key, bearer, basic, manual OAuth token, and managed OAuth credentials
+- one global tool catalog with Enabled, Ask, and Disabled modes
+- interactive approval for sensitive calls
+- a stateful Streamable HTTP MCP endpoint and a local stdio bridge
+- native Linux and macOS binaries, plus Docker
-## Quick start
+Windows is not a release target. The previous TypeScript CLI, local app,
+hosted deployments, managed cloud, and Electron products are archived in
+[`legacy/`](legacy/README.md).
-```bash
-npm install -g executor
-executor install
-executor web
-```
-
-This installs the local background service and opens the web UI. From there, add your first source and start using tools.
+`Cargo.toml` is the native product version source. Published releases provide
+four checksum-verified Linux and macOS archives plus the multi-platform
+`ghcr.io//executor` image. See [installation](docs/install.md)
+and [release operations](RELEASING.md).
-### Use as an MCP server
+## Try it from this checkout
-Point any MCP-compatible agent (Cursor, Claude Code, OpenCode, etc.) at Executor to share your tool catalog, auth, and policies across all of them.
+The production binary embeds the dashboard, so the web build must run before
+the release Cargo build. From the repository root:
-```bash
+```sh
+bun run bootstrap
+bun run --cwd web build
+cargo build --locked --release
-executor mcp
+mkdir -p .executor-local/data
+chmod 0700 .executor-local/data
+./target/release/executor server --data-dir "$PWD/.executor-local/data"
```
-Example `mcp.json` for Claude Code / Cursor:
-
-```json
-{
- "mcpServers": {
- "executor": {
- "command": "executor",
- "args": ["mcp"]
- }
- }
-}
-```
+This checkout expects Bun 1.3.x and Rust 1.96 or newer. The release workflow
+currently pins Bun 1.3.11 and Rust 1.96.0.
-### Use with Pi
+Executor listens at `http://127.0.0.1:4788` by default. Open the one-time
+`/setup#token=...` URL printed by the server and create the only administrator
+account with a password of at least 12 characters. The dashboard then signs in
+with those credentials. Open **API tokens** and create a token for your client.
+The token secret is shown once.
-[Pi](https://pi.dev) does not include a built-in MCP client. To use Executor from Pi, install the community bridge:
+On WSL2, paste the setup URL into the Windows browser. You can also open the
+dashboard from the WSL shell after setup:
-```bash
-pi install git:github.com/gvkhosla/pi-executor-mcp@v0.2.0
+```sh
+/mnt/c/windows/explorer.exe http://127.0.0.1:4788
```
-Reload Pi, then verify the bridge:
+State is under `.executor-local/data` in this example. Stop the server before
+copying that directory for backup, and keep `executor.db`, its WAL files, and
+`master.key` together.
-```text
-/reload
-/executor-status
-```
-
-After that, ask Pi to search, inspect, and call tools through Executor.
-
-## Add a source
-
-If you can represent it with a JSON schema, it can be an integration. Executor has first-party support for OpenAPI, GraphQL, MCP, and Google Discovery — but the plugin system is open to any source type.
-
-### Via the web UI
+## Connect sources
-Run `executor web`, go to **Add Source**, paste a URL, and Executor will detect the type, index the tools, and handle auth.
+Open **Sources**, choose a connector, and follow the preview or connection
+flow:
-### Via the CLI
+- OpenAPI accepts a URL or pasted OpenAPI 3.0/3.1 JSON or YAML.
+- GraphQL connects to an introspection-enabled endpoint.
+- MCP Streamable HTTP connects to a remote or local HTTP MCP endpoint.
+- MCP stdio selects only a machine-admin-approved command template.
-```bash
-executor call executor openapi addSource '{
- "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
- "namespace": "petstore",
- "baseUrl": "https://petstore3.swagger.io/api/v3"
-}'
-```
-
-Use `baseUrl` when the OpenAPI document has relative `servers` entries (for example `"/api/v3"`).
+After import, review each source under **Tools**. GraphQL queries start Enabled,
+mutations start Ask, and deprecated operations start Disabled. MCP tools are
+Enabled only when the upstream explicitly marks them read-only and not
+destructive. Other MCP tools start Ask.
-## Use tools
+See [source and OAuth setup](docs/sources.md) and the detailed
+[MCP contract](docs/mcp.md).
-Agents discover and call tools through a typed TypeScript runtime:
+## Use the CLI
-```ts
-// discover by intent
-const matches = await tools.discover({ query: "github issues", limit: 5 });
+Client commands talk to an already-running Executor server. They never open a
+second copy of the database.
-// inspect the schema
-const detail = await tools.describe.tool({
- path: matches.bestPath,
- includeSchemas: true,
-});
+```sh
+./target/release/executor --version
+export EXECUTOR_API_TOKEN='token-shown-by-the-dashboard'
-// call with type safety
-const issues = await tools.github.issues.list({
- owner: "vercel",
- repo: "next.js",
-});
+./target/release/executor tools sources
+./target/release/executor tools search 'create issue'
+./target/release/executor tools describe source_slug.tool_name
+./target/release/executor call source_slug.tool_name '{"input":"value"}'
```
-Use tools via the CLI:
+Use `--base-url` or `EXECUTOR_BASE_URL` for another instance. Remote instances
+must use HTTPS unless you deliberately pass `--allow-insecure-http` on a
+separately authenticated and encrypted tunnel. A private LAN alone does not
+protect the bearer token. See the [CLI guide](docs/cli.md).
-```bash
-executor tools search "send email"
-executor call --help
-executor call github --help
-executor call github issues --help
-executor call cloudflare --help --match dns --limit 20
-executor call github issues create '{"owner":"octocat","repo":"Hello-World","title":"Hi"}'
-executor call gmail send '{"to":"alice@example.com","subject":"Hi"}'
-```
+## Use Executor as an MCP server
-`executor call`, `executor resume`, and `executor tools ...` commands auto-start a local daemon if needed.
-If the default port is busy, the CLI will pick an available local port and track it automatically.
+For clients that support Streamable HTTP, use the instance origin plus `/mcp`
+and send the dashboard API token as a bearer token. Client configuration
+schemas differ, so treat this as a schematic shape and adapt it to the selected
+client's MCP documentation:
-If an execution pauses for auth or approval, resume it:
-
-```bash
-executor resume --execution-id exec_123
-```
-
-## CLI reference
-
-```bash
-executor install # install/start the durable background service
-executor web # open the running web UI
-executor web --foreground # start a temporary foreground runtime + web UI
-executor daemon run # start persistent local daemon in background
-executor daemon status # show daemon status
-executor daemon stop # stop daemon
-executor daemon restart # restart daemon
-executor mcp # start MCP endpoint
-executor call '{"k":"v"}' # invoke a tool by path segments
-executor call --help # browse namespaces/resources/methods
-executor call --help --match "" --limit # narrow huge namespaces
-executor resume --execution-id # resume paused execution
-executor tools search "" # search tools by intent
-executor tools sources # list configured sources + tool counts
-executor tools describe # show tool TypeScript/JSON schema
+```json
+{
+ "mcpServers": {
+ "executor": {
+ "type": "http",
+ "url": "http://127.0.0.1:4788/mcp",
+ "headers": {
+ "Authorization": "Bearer "
+ }
+ }
+ }
+}
```
-## Developing locally
+For a client that launches only stdio MCP servers, the common shape is:
-```bash
-bun install
-bun dev
+```json
+{
+ "mcpServers": {
+ "executor": {
+ "command": "/absolute/path/to/executor",
+ "args": ["mcp"],
+ "env": {
+ "EXECUTOR_API_TOKEN": ""
+ }
+ }
+ }
+}
```
-The dev server starts at `http://127.0.0.1:4788`.
+The bridge connects to `http://127.0.0.1:4788` by default. Set
+`EXECUTOR_BASE_URL` when the server uses another origin.
-### Tests
+## Install and operate
-```bash
-bun run test # unit + integration suites
-bun run test:e2e # full-stack e2e: boots the cloud and self-host apps and drives them
-```
-
-The browser e2e scenarios need Playwright's Chromium once per machine:
-`bunx playwright install chromium`.
-
-## Community
+- [Native install and first boot](docs/install.md)
+- [Docker Compose](docs/docker.md)
+- [Linux systemd](docs/systemd.md)
+- [macOS launchd](docs/launchd.md)
+- [Runtime and sandbox boundary](docs/runtime.md)
+- [Architecture and security contracts](docs/architecture.md)
-Join the Discord: [https://discord.gg/eF29HBHwM6](https://discord.gg/eF29HBHwM6)
+For a reverse proxy or managed OAuth, set `EXECUTOR_PUBLIC_ORIGIN` to the exact
+HTTPS origin used in the browser before starting Executor. Callback URLs are
+connection-specific and are displayed in the source's Managed OAuth panel.
-## Learn more
+## Develop and verify
-Visit [executor.sh](https://executor.sh) to learn more.
+`bun run bootstrap` installs workspace dependencies, prepares the retained
+TypeScript packages, and installs Playwright Chromium.
-## Attribution
+A debug Rust server does not require production web assets:
-- Thank you to [Crystian](https://www.linkedin.com/in/crystian/) for providing the npm package name `executor`.
+```sh
+mkdir -p .executor-debug
+chmod 0700 .executor-debug
+cargo run -- server --data-dir "$PWD/.executor-debug"
+```
-## References
+Debug builds intentionally embed a small fixture page. That path is suitable
+for API, CLI, and MCP work, not dashboard acceptance. Use the release build
+sequence above when you need the real embedded Svelte application.
+
+The broad merge gates are:
+
+```sh
+bun run format:check
+bun run lint
+bun run typecheck
+bun run test
+cargo fmt --check
+cargo clippy --all-targets --all-features -- -D warnings
+cargo test --all-targets --all-features
+bun run test:e2e
+```
-As part of my coding process, I give my agent access to references to other codebases to understand patterns and how other people have implemented systems.
+The e2e command builds the Svelte assets and a local debug Rust binary before
+driving the real first-boot, source, tool-mode, approval, log, token, and OAuth
+journeys in Chromium.
-A non exhaustive list of references are:
+See [`RUNNING.md`](RUNNING.md) for the current repository workflow and e2e
+status. Default commands exclude all archived application packages.
-- [Better Auth](https://github.com/better-auth/better-auth) - Storage adapter reference
-- [Effect](https://github.com/Effect-TS/effect) - General code patterns
-- [OpenCode](https://github.com/anomalyco/opencode) - Plugin system reference
-- [OpenClaw](https://github.com/openclaw/openclaw) - Plugin system reference
-- [Emdash](https://github.com/emdash-cms/emdash) - Plugin system reference
-- [Pi](https://github.com/badlogic/pi-mono) - Plugin system reference
+## License
-It's encouraged also that you can use this codebase as a reference to understand how it's implemented
+MIT
diff --git a/RELEASING.md b/RELEASING.md
index 7285fa719..8f16ab133 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -1,108 +1,243 @@
# Releasing
-This repo uses Changesets for version orchestration and three publish paths:
-the CLI (`executor` npm package plus its platform packages), the
-`@executor-js/*` library packages (`core`, `sdk`, and the public plugins), and
-the self-host Docker image.
-
-## Normal release flow
-
-1. Add a changeset in the PR that should ship:
- - `bun run changeset`
-2. Merge that PR to `main`.
-3. `.github/workflows/release.yml` opens or updates a `Version Packages` PR.
-4. Merge the `Version Packages` PR.
-5. The release workflow then does two things in parallel:
- - Publishes every `@executor-js/*` library package whose current version
- is not already on npm, via `bun run release:publish:packages`
- (see `scripts/publish-packages.ts`).
- - If `apps/cli/package.json` bumped, tags the commit and dispatches
- `.github/workflows/publish-executor-package.yml`, which:
- - runs `bun run release:check`
- - performs a full dry-run release build before publish
- - publishes the CLI npm package under the correct dist-tag
- - creates or updates the GitHub release with build artifacts
- - dispatches `.github/workflows/publish-desktop.yml`
- - dispatches `.github/workflows/publish-selfhost-docker.yml`
-6. The self-host Docker workflow publishes `ghcr.io/rhyssullivan/executor-selfhost`
- for `linux/amd64` and `linux/arm64`:
- - stable releases get `vX.Y.Z`, `X.Y.Z`, and `latest`
- - prereleases get `vX.Y.Z-...`, `X.Y.Z-...`, and `beta`
-
-## Beta releases
-
-Enter prerelease mode before starting a beta train:
-
-- `bun run release:beta:start`
-
-That commits `.changeset/pre.json` into the repo and causes future release PRs to produce versions like `1.5.0-beta.0`, `1.5.0-beta.1`, and so on.
-
-When the beta train is done:
-
-- `bun run release:beta:stop`
-
-Stable versions publish to npm under `latest`.
-Beta versions publish to npm under `beta`.
-
-## Local dry run
-
-To build the full CLI release payload without publishing to npm or GitHub:
-
-- `bun run release:publish:dry-run`
-
-That produces:
-
-- platform archives in `apps/cli/dist`
-- the packed wrapper tarball in `apps/cli/dist/release`
-
-To pack the `@executor-js/*` library packages without publishing:
-
-- `bun run release:publish:packages:dry-run`
-
-To validate the self-host Dockerfile locally without publishing:
-
-- `docker build -f apps/host-selfhost/Dockerfile -t executor-selfhost:local .`
-
-## Release notes
-
-Release notes follow the standard Changesets flow: **the changeset body
-IS the changelog entry.** Write the user-facing summary in the
-`.changeset/*.md` you add with your PR; `changeset version` compiles
-every changeset into the bumped packages' `CHANGELOG.md` files (via
-`@changesets/changelog-github`, which links the PR and credits the
-author), and `apps/cli/src/release.ts` uses the released version's
-section of `apps/cli/CHANGELOG.md` as the GitHub Release body. If the
-section is missing it falls back to `gh release create
---generate-notes`.
-
-There is no separate release-notes file to remember to update — if your
-change deserves a mention, its changeset body is the mention.
-
-### Authoring rules
-
-Write changeset bodies for users, not for the diff:
-
-- Lead with the user-visible behavior, not the implementation.
-- A typical fix is one sentence. A feature can be a short paragraph.
-- For a large release, a changeset body can be a full markdown section
- (bold sub-headings + bullets). Avoid `#`/`##` headings inside bodies —
- they end up nested inside a changelog list item.
-- For breaking changes, include the before/after surface in the body.
-
-Contributor attribution is automatic: `@changesets/changelog-github`
-prefixes each entry with the PR link and the author's handle.
-
-## Notes
-
-- Changesets owns the published CLI version via `apps/cli/package.json`.
-- Only the Version Packages PR should change `apps/cli/package.json`; the rest of the workspace is not version-synced for release PRs.
-- Per-package `CHANGELOG.md` files are seeded for every workspace package
- (`bun run lint:changelog-stubs --fix`). `changeset version` inserts
- generated sections after the H1, and the `changesets/action@v1` GitHub
- Action reads each bumped package's `CHANGELOG.md` to build the Version
- Packages PR description (it crashes with `ENOENT` if any are missing).
-- `@changesets/changelog-github` needs a `GITHUB_TOKEN` when running
- `changeset version` (it resolves PR numbers and authors). CI provides
- one; locally use `GITHUB_TOKEN=$(gh auth token) bun run changeset:version`.
-- The publish workflow supports either npm trusted publishing or an `NPM_TOKEN` secret.
-- Re-running the publish workflow for the same tag is safe for packages that are already on npm; existing versions are skipped.
+`Cargo.toml` is the only version source for the native Executor product. The
+manual `Release native Executor` workflow is the only entrypoint that may
+publish native archives, the root Docker image, or the primary GitHub release.
+It derives `EXECUTOR_REPOSITORY` from the repository running the workflow. The
+supported installer currently defaults to `davis7dotsh/executor-fork-testing`
+and accepts the same variable as an explicit fork override.
+
+## Prepare the release commit
+
+1. Change `[package].version` in `Cargo.toml`.
+2. Run `cargo check --locked`. If Cargo reports that the lockfile needs an
+ update, run `cargo check` once, review the `Cargo.lock` change, then rerun
+ `cargo check --locked`.
+3. Verify `cargo run -- --version` prints `executor `.
+4. Merge the release commit to `main` and record its full SHA.
+
+Release tags must be exactly `v`. The commit must be on
+`origin/main`, and publish mode requires the existing remote tag to resolve to
+the exact 40-character SHA supplied to the workflow. Release versions cannot
+contain SemVer build metadata (`+...`) because OCI image tags do not support
+that character.
+
+The `ghcr.io//executor` container package must be public
+before promotion. On the first publish attempt, GHCR may create it as private.
+If the anonymous-pull gate fails, open the package settings, change visibility
+to public, then rerun the workflow with the same tag and commit. The GitHub
+release remains a draft until that gate passes.
+
+Before publishing, configure these repository controls. The workflow reads
+them through GitHub's REST API and fails closed if they drift:
+
+- Create a `native-release` environment with only a required-reviewers rule
+ and a branch-policy rule. Set `prevent_self_review=false`, make the sole
+ reviewer the GitHub user `bmdavis419` (user ID `45952064`), and configure a
+ custom deployment branch policy whose sole entry is the `main` branch.
+ Disable administrator bypass so every publish requires approval.
+- Create an active tag ruleset for exactly `refs/tags/v*`, with no exclusions
+ or bypass actors. Enable both restrict updates and restrict deletions. Do not
+ allow fetch-and-merge updates, and do not add any other rule. Do not restrict
+ creations, because the release tag must be created before dispatch.
+- Enable immutable releases in the repository release settings.
+- Add a `NATIVE_RELEASE_TOKEN` secret only to the `native-release` environment.
+ It must be a classic personal access token owned by `bmdavis419`, with the
+ exact scopes `public_repo` and `write:packages`, and no others. The same
+ account login is used as the GHCR username. Create it from the
+ [prefilled exact-scope form](https://github.com/settings/tokens/new?scopes=public_repo,write:packages)
+ because selecting `write:packages` manually can auto-select the broader
+ `repo` scope. Before saving, verify that only `public_repo` and
+ `write:packages` are selected. Do not add this token as a repository or
+ organization secret.
+
+Every native job that publishes or reads a protected release credential uses
+the `native-release` environment. A dedicated preflight checks
+that the secret is present, belongs to the expected account, has both classic
+PAT scopes and no extras, can push to the repository, can authenticate to GHCR, and sees
+immutable releases enabled. All GitHub release and GHCR writes use that token.
+The exact no-bypass ruleset check runs only after protected-environment approval
+because the public read token does not expose bypass actors. The job-scoped
+`GITHUB_TOKEN` remains read-only. Require a fresh protected-environment approval
+on reruns.
+
+## Dry run
+
+Use dry-run mode before creating the tag. It performs the four native builds,
+archive packaging, checksum assembly, version smoke tests, and the root
+multi-platform Docker build. It does not publish an image, create a GitHub
+release, or move a release channel.
+
+```sh
+git fetch origin main
+sha="$(git rev-parse origin/main)"
+version="$(git show "$sha:Cargo.toml" | python3 -c 'import sys,tomllib; print(tomllib.load(sys.stdin.buffer)["package"]["version"])')"
+tag="v$version"
+
+gh workflow run release.yml --ref main \
+ -f mode=dry-run \
+ -f tag="$tag" \
+ -f commit_sha="$sha"
+```
+
+The workflow run revision and `commit_sha` must be identical. If `main` moves
+between resolving `sha` and dispatching the workflow, the run fails closed.
+Resolve the new `origin/main` SHA and dispatch again.
+
+Inspect the run and download the `executor-release-artifacts` workflow artifact
+if desired. Dry runs are safe for forks because all publishing steps are
+skipped.
+
+## Publish
+
+After a successful dry run, create the tag at the same commit and push only
+that tag:
+
+```sh
+git tag -a "$tag" "$sha" -m "Release $tag"
+git push origin "refs/tags/$tag"
+
+test "$(git rev-parse "$tag^{commit}")" = "$sha"
+test "$(git ls-remote origin "refs/tags/$tag^{}" | cut -f1)" = "$sha" || \
+ test "$(git ls-remote origin "refs/tags/$tag" | cut -f1)" = "$sha"
+
+gh workflow run release.yml --ref main \
+ -f mode=publish \
+ -f tag="$tag" \
+ -f commit_sha="$sha"
+```
+
+Publish mode must be dispatched from `main`. On the first attempt, `main`, the
+workflow definition, workflow source revision, checked-out source, input
+commit, and release tag must all resolve to the same commit. A later attempt of
+that same run may continue after `main` advances, but the original commit must
+remain an ancestor of `main` and the remote tag must still resolve to it. Do
+not dispatch a new run with the older SHA. Rerun the complete original run so
+GitHub preserves its workflow run ID, source SHA, protected authorization, and
+every release check:
+
+```sh
+run_id=
+gh run rerun "$run_id"
+gh run watch "$run_id" --exit-status
+```
+
+Publish mode performs these gated stages:
+
+1. Verify the exact protected-environment policy, immutable tag ruleset,
+ workflow revision, Cargo version, full commit SHA, `main` ancestry, and
+ remote tag are bound to one commit.
+2. Build the Svelte payload once, then build and smoke-test four native
+ archives against that identical payload with their preserved installer
+ names. Linux binaries use a digest-pinned Ubuntu 22.04 build image and
+ checksummed Rust 1.96 toolchain archives. Both macOS binaries declare a
+ macOS 14.0 minimum, which the workflow verifies from their Mach-O metadata.
+ Release compilation selects Xcode 16.4 build 16F6, macOS SDK 15.5, Apple
+ clang 17.0.0 (clang-1700.0.13.5), and ld 1167.5 by exact path and version.
+ The macOS 14 compatibility-floor service tests select Xcode 15.4 build
+ 15F31d, macOS SDK 14.5, Apple clang 15.0.0 (clang-1500.3.9.4), and ld
+ 1053.12. The ARM64 archive is booted on macOS 14, while the x86-64 archive
+ is booted on the available standard Intel macOS 15 runner.
+3. Package all four raw binaries in one digest-pinned Ubuntu snapshot with one
+ fixed Python and zlib implementation. Build the archives twice and require
+ byte-identical archives and sidecars before assembling `SHA256SUMS`.
+4. Extract the two Linux release archives into the root `Dockerfile`'s
+ `runtime-prebuilt` target, push each architecture by digest, and assemble a
+ run-scoped multi-platform staging manifest. The runtime base and apt
+ snapshot are pinned, and image timestamps use the release commit time so a
+ retry produces the same digest. Build orchestration and manifest creation
+ use the exact Buildx v0.34.1 client and digest-pinned BuildKit v0.30.0 daemon.
+5. Create the draft with an atomic `[executor-run:]` title marker, then
+ upload the complete exact asset set. `RELEASE-RUN.json` binds the repository,
+ tag, commit, and workflow run ID. `RELEASE-IMAGE.json` additionally binds the
+ exact assembled OCI image name and digest.
+6. Boot both published architectures by exact assembled digest, then verify
+ first boot, health, every byte of the canonical embedded web payload, and
+ anonymous pull access.
+7. Re-fetch the remote release tag and assign the immutable release, version,
+ and commit image tags.
+8. Recompute and byte-compare the run binding, image binding, and every release
+ asset, then publish the draft GitHub release. Publication creates GitHub's
+ automatic immutable release attestation. Require the release API to report
+ the exact release as immutable, and retry bounded attestation verification
+ until it succeeds.
+9. Only after the GitHub release is public, move the mutable `latest` or
+ `beta` channel to the verified image digest.
+
+Stable versions move the Docker `latest` channel. Prerelease versions move the
+`beta` channel. The image is `ghcr.io//executor` and receives
+immutable `vX.Y.Z`, `X.Y.Z`, and `sha-` tags plus the channel tag.
+
+The workflow may reuse only a release whose title contains the exact current
+workflow run ID. If release creation succeeded before its binding asset upload,
+that atomic title marker lets the same run recover the partial draft. Every
+same-run draft retry uploads the complete asset list with clobber semantics,
+then downloads and byte-compares the final exact set. An already-published
+immutable release is compare-only and must contain a byte-identical
+`RELEASE-RUN.json`, `RELEASE-IMAGE.json`, and complete rebuilt bundle. A new run
+refuses to reuse either release state. Before the workflow
+assigns any immutable release, version, or commit image tag, it verifies that
+an existing tag already has the expected digest or fails closed. Only `latest`
+or `beta` may move to another digest, and neither moves until the GitHub release
+is public. The remote `v*` tag is force-refetched and compared with the validated
+commit immediately before draft-release mutation and again immediately before
+promotion. All native release runs share one
+serialized concurrency group. Promotion reads the current channel manifest's
+version annotation and refuses to move a channel to an older SemVer. A retry at
+the same version succeeds only when the channel already has the exact assembled
+digest. Published GitHub release history is checked too, so removing a channel
+tag cannot bypass the version floor. A channel without a valid version
+annotation fails closed.
+
+## Release archives
+
+The primary GitHub release contains:
+
+- `executor-x86_64-unknown-linux-gnu.tar.gz`
+- `executor-aarch64-unknown-linux-gnu.tar.gz`
+- `executor-x86_64-apple-darwin.tar.gz`
+- `executor-aarch64-apple-darwin.tar.gz`
+- one `.sha256` sidecar for each archive
+- `RELEASE-RUN.json`, binding the release to its repository, tag, commit, and
+ workflow run ID
+- `RELEASE-IMAGE.json`, binding that same source to the exact OCI image and
+ digest
+- `BUILD-TOOLCHAINS.txt`, recording the asserted build and packaging toolchains
+- `SHA256SUMS`
+
+Each archive contains `executor`, `LICENSE`, `THIRD_PARTY_LICENSES.html`, and
+`THIRD_PARTY_JAVASCRIPT_LICENSES.json`.
+
+After downloading an archive, verify both GitHub's release attestation and the
+specific local asset before checking the checksum manifest:
+
+```sh
+gh release verify "$tag" --repo davis7dotsh/executor-fork-testing
+gh release verify-asset "$tag" "executor-${target}.tar.gz" \
+ --repo davis7dotsh/executor-fork-testing
+sha256sum --check "executor-${target}.tar.gz.sha256"
+```
+
+The release installer fixture exercises archive verification, serialized
+concurrent install and uninstall, stale-lock recovery with PID-reuse defense,
+live-owner identity lookup failure, hostile and swapped install-path ancestors,
+durability ordering, interrupted rollback, resumable partial uninstall,
+Python-free installation, and locked cross-root PATH updates. Its injected
+durability log must show owned-file changes before manifest publication or
+deletion, and manifest changes before recovery retirement.
+
+The macOS command-line archives are not Developer ID signed or notarized. The
+supported path is the checksum-verifying curl installer, or a `gh` download
+followed by the attestation and checksum verification above. GitHub records the
+attestation automatically when the immutable release is published.
+Browser-downloaded
+quarantined binaries and app-style Gatekeeper distribution are not release
+claims until a signing identity and notarization workflow are configured. Both
+macOS architectures require macOS 14 Sonoma or newer.
+
+## Legacy desktop compatibility
+
+The desktop publishing workflow is retired. Historical tags predate the
+`legacy/desktop` move and cannot be built honestly with current-tree paths. The
+archived source remains available for local archaeology, but no release
+workflow claims to produce supported desktop artifacts.
diff --git a/RUNNING.md b/RUNNING.md
index 55c1c6fba..c8ae4459f 100644
--- a/RUNNING.md
+++ b/RUNNING.md
@@ -1,125 +1,219 @@
-# RUNNING.md — how things run today
-
-> **This document may be out of date.** It describes how things run today,
-> not how they must run. Trust it as a starting point; if you hit weirdness,
-> the implementation has probably moved and this file is why. Verify against
-> the code, then update this file when you notice drift. The principles in
-> [AGENTS.md](AGENTS.md) are the stable contract; everything below is
-> implementation detail that churns.
-
-## Fresh checkout / worktree setup
-
-`bun run bootstrap` from the repo root — idempotent: `bun install` (whose
-prepare hook builds `@executor-js/vite-plugin` and `packages/react`, the
-artifacts dev servers fail without) plus Playwright chromium. A fresh
-worktree that skips it dies with "Failed to resolve entry for package
-'@executor-js/vite-plugin'".
-
-Our two upstream forks — `@executor-js/emulate` (service emulators) and
-`@executor-js/mcporter` (headless MCP client) — are consumed purely as
-published npm packages; nothing in this repo references them by path. There
-are no `vendor/` submodules. Each fork is its own standalone repo
-(`github.com/UsefulSoftwareCo/emulate`, `github.com/UsefulSoftwareCo/mcporter`):
-develop on its `main`, publish a bump, then bump the dependency here. The
-`emulate` skill covers the emulator publish/deploy loop.
-
-## Dev servers
-
-- Everything except desktop/cloud: `bun run dev` (turbo, from root)
-- One app: `bun run dev` from its `apps/` directory
-- Self-host boots standalone with just env vars — see
- `e2e/setup/selfhost.globalsetup.ts` for the canonical recipe (data dir,
- bootstrap admin email/password, base URL, `EXECUTOR_ALLOW_LOCAL_NETWORK`)
-- Cloud needs WorkOS + Autumn; for a no-.env boot, point it at emulators —
- see `e2e/setup/cloud.globalsetup.ts` for the canonical recipe (the real
- SDKs against emulated services, PGlite dev DB)
-
-The e2e globalsetup files are the source of truth for "how do I boot a
-working instance of X" — read them before inventing a boot path.
-
-## E2E: running, viewing, sharing
-
-`e2e/AGENTS.md` covers writing scenarios. Operationally:
-
-- `cd e2e && bun run test` boots dev servers and runs everything;
- `--project cloud|selfhost` narrows. `E2E_CLOUD_URL`/`E2E_SELFHOST_URL`
- attach to an already-running server instead of booting.
-- Runs land in `e2e/runs///` — `result.json`, step
- screenshots, `session.mp4` + `trace.zip` for browser scenarios, and the
- scenario source as `test.ts`.
-- `cd e2e && bun run serve` builds the viewer and serves the scenario ×
- target matrix over HTTP, bound to all interfaces (reachable over the
- tailnet). It prefers port 8901 but walks forward to the next free port if
- that's taken (so concurrent worktrees, or a leaked previous viewer, never
- wedge each other) — read the printed `e2e viewer → …` URL for the actual
- port. `PORT=…` pins a port explicitly and fails loudly if it's busy. The
- built SPA is port- and mount-agnostic (relative assets + hash routing), so
- whatever port it lands on just works. Individual runs are at
- `#//` hash routes — when handing results to the user, link
- those directly, not the bare matrix.
-- `bun e2e/scripts/pr-media.ts e2e/runs//` converts a run's
- recording to a gif, uploads it to the `e2e-media` branch, and prints
- PR-ready markdown.
-
-E2E dev-server ports are derived and CLAIMED per checkout (`cd e2e && bun
-run ports` prints this checkout's block; see `e2e/src/ports.ts`) — each
-checkout hashes its repo root to a preferred block, atomically locks it,
-and walks to the next free block if squatted, so concurrent worktrees never
-collide or attach to each other's servers. `E2E_*_PORT` env vars pin ports
-explicitly. If a boot reports a squatted port, an old dev server leaked —
-`bun run reap` (repo root) lists and kills orphaned stacks.
-
-## The dev CLI: live instances, interactively
-
-`cd e2e && bun run cli` — the same primitives scenarios use, as commands.
-Boot a target, mint identities, make typed API calls, drive MCP, read the
-emulator ledger — develop interactively, then crystallize the journey into
-a scenario.
+# Running the Rust and Svelte rewrite
+
+This file describes the active local and self-hosted product. The archived
+TypeScript application entry points are under `legacy/` and are not part of
+the default workflow.
+
+## Fresh checkout
+
+From the repository root:
+
+```sh
+bun run bootstrap
+```
+
+Bootstrap runs the workspace install and prepare hooks, then installs
+Playwright Chromium. It is safe to rerun. The workspace currently declares Bun
+1.3.11 and the native release workflow uses Rust 1.96.0.
+
+`Cargo.toml` is the only native product version source. Confirm the active
+checkout before packaging with:
+
+```sh
+cargo run -- --version
+```
+
+## Production-like local run
+
+The real Svelte dashboard is compiled first and embedded in the Rust release
+binary:
+
+```sh
+bun run --cwd web build
+cargo build --locked --release
+
+mkdir -p .executor-local/data
+chmod 0700 .executor-local/data
+./target/release/executor server --data-dir "$PWD/.executor-local/data"
+```
+
+Open the setup URL printed by the process. The dashboard creates the
+administrator and immediately signs in with those credentials. Add a source,
+review its tool modes, and create an API token under `/tokens`.
+
+The server binds to `127.0.0.1:4788` unless `--bind` changes it. Keep plaintext
+HTTP on loopback. For another browser-facing hostname, terminate TLS at a
+reverse proxy and set the exact external origin with `--public-origin` or
+`EXECUTOR_PUBLIC_ORIGIN`.
+
+On this WSL2 machine, the Windows browser can reach the loopback server. Open
+it with:
+
+```sh
+/mnt/c/windows/explorer.exe http://127.0.0.1:4788
+```
+
+Use a worktree-specific data directory, as shown above, so concurrent checkouts
+never share SQLite or the instance master key. One process lock protects each
+data directory.
+
+## Debug server without a web build
+
+Normal debug compilation does not require `web/build`:
+
+```sh
+mkdir -p .executor-debug
+chmod 0700 .executor-debug
+cargo run -- server --data-dir "$PWD/.executor-debug"
+```
+
+Debug builds embed the deterministic fixture under `tests/fixtures/web-assets`.
+This is useful for Rust API, CLI, MCP, and lifecycle work. It is not a visual
+dashboard development server. Use the production-like sequence when the real
+Svelte application must be present.
+
+`EXECUTOR_WEB_ASSETS_DIR` is a compile-time packaging override for focused
+asset tests. It is not a runtime static-directory setting.
+
+## Client smoke test
+
+Create a dashboard API token, then use the same binary as a client:
+
+```sh
+export EXECUTOR_API_TOKEN='token-shown-once-by-the-dashboard'
+
+./target/release/executor tools sources
+./target/release/executor tools search 'health check'
+./target/release/executor tools describe source_slug.tool_name
+./target/release/executor call source_slug.tool_name '{}'
+```
+
+The server never auto-starts for client commands. `call`, `tools`, and `mcp`
+require a running server and an API token. `open` only opens the clean dashboard
+URL and uses the administrator login cookie in the browser.
+
+## Managed OAuth callbacks
+
+Set the final browser-facing origin before creating an OAuth connection:
```sh
-bun run cli up selfhost --share # boot, reachable over the tailnet, stays up
-bun run cli up cloud --share # emulated WorkOS+Autumn, tailscale-HTTPS fronted
-bun run cli status # what's running, URLs, creds
-bun run cli identity selfhost # fresh identity (headers / cookies / creds)
-bun run cli api selfhost tools.list
-bun run cli mcp selfhost call execute '{"code":"return 1+1;"}'
-bun run cli ledger cloud workos # what hit the emulator
-bun run cli down selfhost # tear down (also removes tailscale serves)
+./target/release/executor server \
+ --data-dir "$PWD/.executor-local/data" \
+ --public-origin https://executor.example.com
+```
+
+After importing an eligible OpenAPI, GraphQL, or HTTP MCP source, open its
+**Managed OAuth** panel. Save the provider discovery and client configuration,
+copy the exact displayed callback URL into the provider, then select **Connect
+OAuth**. Every connection has its own callback path:
+
+```text
+/api/v1/oauth/callback/
```
-Instances persist until `down` — `up --share` IS the "touch it" handoff
-artifact, and the seeding direction too: boot, drive the product into a
-state (API/MCP/UI), hand across the URL. State files in `e2e/.dev/` mark
-deliberate long-lived instances (vs leaks); attach scenarios to a running
-instance with `E2E__URL`.
-
-Why cloud `--share` is more involved (encoded in the CLI, kept here for
-when you hit it manually): the cloud app sets `secure: true` auth cookies,
-so login breaks over plain http from any non-localhost origin ("Invalid
-login state"). Both the app AND the WorkOS emulator get fronted with
-`tailscale serve` HTTPS, the emulator advertises its public URL on both
-sides (its `baseUrl` and the app's `WORKOS_API_URL` — the browser-facing
-authorize URL derives from the latter), and Vite must allow the public
-hostname (`__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS`).
-
-## Environment gotchas (learned the hard way)
-
-- The shell is fish, and the working directory resets between Bash calls.
- Use absolute paths rooted at THIS worktree; don't rely on a prior `cd`.
-- Don't write probe scripts to `/tmp` — they can't resolve workspace
- packages (`effect`, `playwright`, …). Put scratch scripts under the repo
- root (`scratch/` is gitignored) so bun resolves the workspace.
-- A fresh worktree's Vite dep-optimizer cache can serve PRE-REBASE code
- (symptom: behavior matching old code only in dev servers, while unit
- tests pass). Kill the server, clear `node_modules/.vite` /
- `.tanstack`-adjacent caches, reboot.
-- The real Tailscale CLI on this machine is
- `/opt/homebrew/opt/tailscale/bin/tailscale`; `/usr/local/bin/tailscale`
- is a broken shim pointing at a deleted app. The tailnet IP is on the
- `utun` interface (100.x.y.z) if the CLI fails.
-- `bun.lock` conflicts on rebase: take either side, re-run `bun install`,
- never hand-merge.
-- Long-lived demo servers you left up for the user look like leaks to
- cleanup tooling — `e2e/.dev/.json` marks deliberate instances;
- check it before reaping, and `bun run cli down ` is the clean
- teardown.
+The supported managed flow is OAuth 2 authorization code with PKCE. OpenID
+Connect and the `openid` scope are not supported. See
+[`docs/sources.md`](docs/sources.md).
+
+## Verification
+
+Use the narrowest relevant command while iterating. For a merge-ready change,
+run the complete active-product gates:
+
+```sh
+bun run format:check
+bun run lint
+bun run typecheck
+bun run test
+
+bun run --cwd web format:check
+bun run --cwd web lint
+bun run --cwd web check
+bun run --cwd web test
+
+cargo fmt --check
+cargo check --all-targets --all-features
+cargo clippy --all-targets --all-features -- -D warnings
+cargo test --all-targets --all-features
+```
+
+Use `vitest run ...` or a package script that delegates to Vitest for focused
+TypeScript tests. Never use `bun test`.
+
+## Browser e2e
+
+`bun run test:e2e` builds the Svelte distribution, embeds it in the debug Rust
+binary, and runs only the active `local-selfhost` browser project. The harness
+boots two fresh loopback instances: one prepared single-admin instance for the
+main journeys and one untouched instance for the first-boot setup journey.
+Both use temporary private data directories that are removed during teardown.
+
+The throwaway main-instance credentials are:
+
+```text
+username: admin
+password: executor-e2e-admin-password
+```
+
+The archived cloud browser suite remains an explicit opt-in:
+
+```sh
+bun run legacy:test:e2e:cloud
+```
+
+Runs land under `e2e/runs/local-selfhost//`. View them with
+`cd e2e && bun run serve`, then open the exact URL and port printed by the
+viewer. Credential-entry journeys deliberately omit traces and video so setup
+secrets, passwords, and one-time API tokens never become artifacts.
+
+## Service and container runs
+
+The release binary embeds its hardened systemd and launchd installation assets.
+On Linux, system service changes require root:
+
+```sh
+sudo "$(command -v executor)" service install
+executor service status
+sudo executor service restart
+sudo executor service stop
+sudo executor service start
+sudo executor service remove
+```
+
+On macOS, run the same commands without `sudo`. The LaunchAgent belongs to the
+logged-in user. `service install --no-start` writes the managed files while
+leaving the service stopped. `service status` prints exactly `active` or
+`inactive`; inactive exits with status 3.
+
+These commands mutate real operating-system service state. Do not use them for
+ordinary development checkouts or tests. Use the maintained operator guides
+for paths, permissions, backup requirements, and removal behavior:
+
+- [`docs/docker.md`](docs/docker.md)
+- [`docs/systemd.md`](docs/systemd.md)
+- [`docs/launchd.md`](docs/launchd.md)
+- [`docs/install.md`](docs/install.md)
+
+## Legacy opt-ins
+
+Default dev, test, lint, typecheck, and format paths exclude all six archived
+application packages. Work on them only through the explicit scripts:
+
+```sh
+bun run legacy:dev
+bun run legacy:dev:cli -- --help
+bun run legacy:test
+bun run legacy:typecheck
+bun run legacy:typecheck:slow
+bun run legacy:test:e2e:cloud
+bun run legacy:test:e2e:desktop
+```
+
+See [`legacy/README.md`](legacy/README.md) for the boundary.
+
+Publishing is also opt-in. `release.yml` is the sole native product release
+entrypoint and requires explicit dry-run or publish mode, a semver tag, and an
+exact full commit SHA. The separate TypeScript workflow publishes only
+explicitly confirmed `@executor-js/*` library versions under immutable
+compatibility tags. Legacy CLI and desktop publishing are retired. See
+[`RELEASING.md`](RELEASING.md) for the checked release sequence.
diff --git a/about.hbs b/about.hbs
new file mode 100644
index 000000000..31f8092d2
--- /dev/null
+++ b/about.hbs
@@ -0,0 +1,27 @@
+
+
+
+
+ Executor third-party licenses
+
+
+ Executor third-party licenses
+
+ {{#each overview}}
+ {{name}} ({{count}})
+ {{/each}}
+
+ {{#each licenses}}
+
+ {{name}}
+ Used by:
+
+ {{#each used_by}}
+ {{crate.name}} {{crate.version}}
+ {{/each}}
+
+ {{text}}
+
+ {{/each}}
+
+
\ No newline at end of file
diff --git a/about.toml b/about.toml
new file mode 100644
index 000000000..a50618dc2
--- /dev/null
+++ b/about.toml
@@ -0,0 +1,32 @@
+accepted = [
+ "0BSD",
+ "Apache-2.0",
+ "Apache-2.0 WITH LLVM-exception",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "BSL-1.0",
+ "CC0-1.0",
+ "CDLA-Permissive-2.0",
+ "ISC",
+ "MIT",
+ "MIT-0",
+ "MPL-2.0",
+ "OpenSSL",
+ "Unicode-3.0",
+ "Unicode-DFS-2016",
+ "Unlicense",
+ "Zlib",
+]
+
+targets = [
+ "aarch64-apple-darwin",
+ "aarch64-unknown-linux-gnu",
+ "x86_64-apple-darwin",
+ "x86_64-unknown-linux-gnu",
+]
+
+ignore-build-dependencies = false
+ignore-dev-dependencies = true
+filter-noassertion = true
+no-clearly-defined = true
+workarounds = ["ring", "rustls"]
diff --git a/apps/cli/src/release.ts b/apps/cli/src/release.ts
deleted file mode 100644
index 4471fe56f..000000000
--- a/apps/cli/src/release.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-import { existsSync } from "node:fs";
-import { mkdir, readdir, readFile, rename, rm } from "node:fs/promises";
-import { dirname, join, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-type ReleaseChannel = "latest" | "beta";
-
-type ReleaseCliOptions = {
- readonly dryRun: boolean;
-};
-
-type CommandInput = {
- readonly command: string;
- readonly args: ReadonlyArray;
- readonly cwd: string;
- readonly captureOutput?: boolean;
-};
-
-type CommandOutput = {
- readonly stdout: string;
- readonly stderr: string;
-};
-
-const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
-const cliRoot = resolve(repoRoot, "apps/cli");
-const distDir = resolve(cliRoot, "dist");
-const releaseDir = resolve(distDir, "release");
-const wrapperDir = resolve(distDir, "executor");
-const versionPackagePath = resolve(cliRoot, "package.json");
-const semverPattern =
- /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
-
-const parseArgs = (argv: ReadonlyArray): ReleaseCliOptions => {
- let dryRun = false;
-
- for (const arg of argv) {
- if (arg === "--dry-run") {
- dryRun = true;
- continue;
- }
-
- throw new Error(`Unknown argument: ${arg}`);
- }
-
- return { dryRun };
-};
-
-const runCommand = async (input: CommandInput): Promise => {
- const proc = Bun.spawn([input.command, ...input.args], {
- cwd: input.cwd,
- env: process.env,
- stdio: input.captureOutput ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"],
- });
-
- const exitCode = await proc.exited;
- const stdout = input.captureOutput && proc.stdout ? await new Response(proc.stdout).text() : "";
- const stderr = input.captureOutput && proc.stderr ? await new Response(proc.stderr).text() : "";
-
- if (exitCode !== 0) {
- throw new Error(
- [
- `${input.command} ${input.args.join(" ")} exited with code ${exitCode}`,
- stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null,
- stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null,
- ]
- .filter((part) => part !== null)
- .join("\n\n"),
- );
- }
-
- return {
- stdout: stdout.trim(),
- stderr: stderr.trim(),
- };
-};
-
-const readVersion = async (): Promise => {
- const pkg = (await Bun.file(versionPackagePath).json()) as { version?: string };
- const version = pkg.version?.trim();
-
- if (!version) {
- throw new Error(`Missing version in ${versionPackagePath}`);
- }
-
- return version;
-};
-
-const validateVersion = (version: string): void => {
- if (!semverPattern.test(version)) {
- throw new Error(`${versionPackagePath} version is not valid semver: ${version}`);
- }
-};
-
-const resolveChannel = (version: string): ReleaseChannel =>
- version.includes("-") ? "beta" : "latest";
-
-const resolveTagFromEnvironment = (): string | undefined => {
- const refName = process.env.GITHUB_REF_NAME?.trim();
- if (process.env.GITHUB_REF_TYPE === "tag" && refName) {
- return refName;
- }
-
- const ref = process.env.GITHUB_REF?.trim();
- if (ref?.startsWith("refs/tags/")) {
- return ref.slice("refs/tags/".length);
- }
-
- return undefined;
-};
-
-const resolveGitHubRepository = (): string => {
- const repository = process.env.GH_REPO?.trim() || process.env.GITHUB_REPOSITORY?.trim();
- if (!repository) {
- throw new Error("Set GH_REPO or GITHUB_REPOSITORY before creating a GitHub release.");
- }
-
- return repository;
-};
-
-const packWrapperPackage = async (): Promise => {
- if (!existsSync(wrapperDir)) {
- throw new Error(`Wrapper package directory not found: ${wrapperDir}`);
- }
-
- await mkdir(releaseDir, { recursive: true });
-
- const before = new Set((await readdir(wrapperDir)).filter((entry) => entry.endsWith(".tgz")));
- await runCommand({
- command: "bun",
- args: ["pm", "pack"],
- cwd: wrapperDir,
- });
-
- const after = (await readdir(wrapperDir)).filter((entry) => entry.endsWith(".tgz"));
- const archiveName = after.find((entry) => !before.has(entry)) ?? after[0];
-
- if (!archiveName) {
- throw new Error(`bun pm pack did not create a .tgz archive in ${wrapperDir}`);
- }
-
- const sourcePath = join(wrapperDir, archiveName);
- const destinationPath = join(releaseDir, archiveName);
-
- await rm(destinationPath, { force: true });
- await rename(sourcePath, destinationPath);
-
- return destinationPath;
-};
-
-const collectReleaseAssetPaths = async (
- wrapperArchivePath: string,
-): Promise> => {
- const assetNames = (await readdir(distDir))
- .filter((entry) => /^executor-.*\.(?:tar\.gz|zip)$/.test(entry))
- .sort();
-
- return [wrapperArchivePath, ...assetNames.map((entry) => join(distDir, entry))];
-};
-
-const githubReleaseExists = async (tag: string, repository: string): Promise => {
- const proc = Bun.spawn(["gh", "release", "view", tag, "--repo", repository], {
- cwd: repoRoot,
- env: process.env,
- stdio: ["ignore", "ignore", "ignore"],
- });
-
- return (await proc.exited) === 0;
-};
-
-/** The CHANGELOG section Changesets generated for `version` (compiled from
- * `.changeset/*.md` bodies at Version Packages time) — the GitHub Release
- * body. Returns null when the section is missing so the caller can fall back
- * to GitHub's auto-generated notes. */
-const changelogSectionForVersion = async (version: string): Promise => {
- const changelogPath = resolve(cliRoot, "CHANGELOG.md");
- if (!existsSync(changelogPath)) return null;
- const lines = (await readFile(changelogPath, "utf8")).split("\n");
- const start = lines.findIndex((line) => line.trim() === `## ${version}`);
- if (start === -1) return null;
- let end = lines.length;
- for (let i = start + 1; i < lines.length; i += 1) {
- if (/^## /.test(lines[i] ?? "")) {
- end = i;
- break;
- }
- }
- const body = lines
- .slice(start + 1, end)
- .join("\n")
- .trim();
- return body.length > 0 ? body : null;
-};
-
-const syncGitHubRelease = async (input: {
- readonly tag: string;
- readonly channel: ReleaseChannel;
- readonly assetPaths: ReadonlyArray;
-}): Promise => {
- if (!process.env.GH_TOKEN?.trim()) {
- throw new Error("GH_TOKEN is required to create or update a GitHub release.");
- }
-
- const repository = resolveGitHubRepository();
-
- if (await githubReleaseExists(input.tag, repository)) {
- await runCommand({
- command: "gh",
- args: [
- "release",
- "upload",
- input.tag,
- ...input.assetPaths,
- "--repo",
- repository,
- "--clobber",
- ],
- cwd: repoRoot,
- });
- return;
- }
-
- const notes = await changelogSectionForVersion(input.tag.replace(/^v/, ""));
-
- // Draft until publish-desktop.yml finishes uploading installers and flips
- // it; otherwise /releases/latest/download/ 404s during the
- // ~15-20 min desktop build window.
- const args = [
- "release",
- "create",
- input.tag,
- ...input.assetPaths,
- "--repo",
- repository,
- "--title",
- input.tag,
- ...(notes ? ["--notes", notes] : ["--generate-notes"]),
- "--verify-tag",
- "--draft",
- ];
-
- if (input.channel === "beta") {
- args.push("--prerelease");
- }
-
- await runCommand({
- command: "gh",
- args,
- cwd: repoRoot,
- });
-};
-
-const main = async () => {
- const options = parseArgs(process.argv.slice(2));
- const version = await readVersion();
- const tag = `v${version}`;
- const refTag = resolveTagFromEnvironment();
- const channel = resolveChannel(version);
-
- validateVersion(version);
-
- if (refTag && refTag !== tag) {
- throw new Error(`GitHub tag ${refTag} does not match ${versionPackagePath} version ${version}`);
- }
-
- await rm(releaseDir, { recursive: true, force: true });
- await mkdir(releaseDir, { recursive: true });
-
- await runCommand({
- command: "bun",
- args: ["run", "src/build.ts", "binary"],
- cwd: cliRoot,
- });
-
- await runCommand({
- command: "bun",
- args: ["run", "src/build.ts", "release-assets"],
- cwd: cliRoot,
- });
-
- const wrapperArchivePath = await packWrapperPackage();
- const assetPaths = await collectReleaseAssetPaths(wrapperArchivePath);
-
- console.log(`Prepared executor@${version} for ${channel}`);
- for (const assetPath of assetPaths) {
- console.log(`- ${assetPath}`);
- }
-
- if (options.dryRun) {
- return;
- }
-
- await syncGitHubRelease({
- tag,
- channel,
- assetPaths,
- });
-
- await runCommand({
- command: "bun",
- args: ["run", "src/build.ts", "publish", channel],
- cwd: cliRoot,
- });
-};
-
-await main();
diff --git a/apps/docs/README.md b/apps/docs/README.md
index 1999936d8..7b30dad46 100644
--- a/apps/docs/README.md
+++ b/apps/docs/README.md
@@ -18,14 +18,13 @@ server hot-reloads.
## How it's served
-Mintlify hosts the built site at `executor.mintlify.dev`. The Executor Cloud
-worker reverse-proxies it onto the first-party origin at `executor.sh/docs`
-(see `apps/cloud/src/edge/docs.ts`), so the public docs live at
-`executor.sh/docs` instead of a `*.mintlify.dev` subdomain.
+Mintlify builds and hosts the site at `executor.mintlify.dev`. The canonical
+public URL is `executor.sh/docs`, configured through the Mintlify project and
+the production domain's external routing settings.
Mintlify is configured to host under the `/docs` subpath (Settings → Domain
-setup → **Host at /docs**), so it serves `/docs/*` paths that the proxy
-forwards unchanged. A config change like that only takes effect on the next
-build, so push a commit to `apps/docs` to redeploy.
+setup → **Host at /docs**). The docs deployment is independent of archived code
+under `legacy/`; no supported Executor runtime serves or proxies this site. A
+Mintlify config change takes effect on the next docs build.
To deploy from this directory, point the Mintlify GitHub app at `apps/docs`.
diff --git a/apps/docs/cli.mdx b/apps/docs/cli.mdx
new file mode 100644
index 000000000..ea8c16805
--- /dev/null
+++ b/apps/docs/cli.mdx
@@ -0,0 +1,44 @@
+---
+title: Rust CLI
+description: "Use the supported executor binary as a client for an already-running self-hosted server."
+---
+
+The `executor` binary contains both the self-hosted server and its command-line
+client. Client commands connect to a running server. They do not open the SQLite
+database or start another server.
+
+## Connect
+
+The default server is `http://127.0.0.1:4788`. Create an API token in the
+dashboard, then provide it through the environment:
+
+```bash
+export EXECUTOR_API_TOKEN='token-shown-by-the-dashboard'
+executor tools sources
+executor tools search 'create issue'
+```
+
+Use `--base-url` or `EXECUTOR_BASE_URL` for another instance. Remote instances
+must use HTTPS unless a separately authenticated and encrypted tunnel protects
+the complete path and you explicitly pass `--allow-insecure-http`.
+
+## Inspect and call tools
+
+```bash
+executor tools describe github.issues_create
+executor call github.issues_create '{"owner":"acme","repo":"app","title":"Bug"}'
+```
+
+Use `--json` for machine-readable output. An Ask-mode call prints and opens the
+dashboard approval URL, then waits for the administrator's decision.
+
+## MCP stdio bridge
+
+For an MCP client that launches only stdio servers, configure it to run:
+
+```bash
+executor mcp
+```
+
+The bridge forwards MCP JSON-RPC to the running authenticated `/mcp` endpoint.
+It requires `EXECUTOR_API_TOKEN` and honors `EXECUTOR_BASE_URL`.
diff --git a/apps/docs/concepts/connections.mdx b/apps/docs/concepts/connections.mdx
index 81757241e..113556a2c 100644
--- a/apps/docs/concepts/connections.mdx
+++ b/apps/docs/concepts/connections.mdx
@@ -1,11 +1,14 @@
---
-title: Connections
-description: "A connection is a configured instance of an integration. One integration can have many connections."
+title: "Legacy concept: connections"
+description: "The supported product stores authentication directly on each source."
---
-A **connection** is a configured instance of an [integration](/concepts/integrations).
-A single integration can have many connections: the same API authenticated for two
-different accounts, say.
+
+ This page preserves an old term for incoming links. The active self-hosted product does not create
+ account connections beneath a source.
+
-A connection doesn't have to be authenticated: public APIs and public MCP servers can be
-connected with no credentials. Executor runs tool calls in a sandbox, so the agent can never access the credentials.
+
+ Configure at most one encrypted static credential profile per source, with Managed OAuth available
+ for eligible sources.
+
diff --git a/apps/docs/concepts/credentials.mdx b/apps/docs/concepts/credentials.mdx
new file mode 100644
index 000000000..f44822d71
--- /dev/null
+++ b/apps/docs/concepts/credentials.mdx
@@ -0,0 +1,28 @@
+---
+title: Credentials and OAuth
+description: "Configure one encrypted static credential profile per source, with Managed OAuth for eligible sources."
+---
+
+Each [source](/concepts/sources) has at most one active static credential
+profile. For a public upstream, leave it unauthenticated. Otherwise configure
+the profile with the fields required by that source:
+
+- API key
+- Bearer token
+- Basic username and password
+- Manual OAuth access token
+
+Saving a replacement profile replaces the previous static secret
+configuration. Clearing it removes the source's static credentials. Executor
+encrypts stored secrets with the instance master key, resolves them only when
+dispatching a request, and never exposes them to the agent or JavaScript
+sandbox.
+
+Eligible OpenAPI, GraphQL, and HTTP MCP sources can also use **Managed OAuth**.
+Configure the provider and client on the source, register the exact callback
+URL shown by Executor, then select **Connect OAuth**. Access and refresh tokens
+remain encrypted on the instance. When a source supports both forms, its
+static profile takes precedence until you clear it.
+
+The signed-in administrator manages credentials. Dashboard-issued API tokens
+can discover and call tools, but cannot read or change source secrets.
diff --git a/apps/docs/concepts/integrations.mdx b/apps/docs/concepts/integrations.mdx
index 4d15343ee..cb443fd48 100644
--- a/apps/docs/concepts/integrations.mdx
+++ b/apps/docs/concepts/integrations.mdx
@@ -1,18 +1,13 @@
---
-title: Integrations
-description: "An integration is something Executor connects to, defined by an MCP server, an OpenAPI spec, or a GraphQL endpoint."
+title: "Legacy concept: integrations"
+description: "The supported product now calls each configured upstream a source."
---
-An **integration** is something Executor can connect to. Each integration is defined by
-a source that describes the available tools:
+
+ This page preserves an old term for incoming links. The active self-hosted product calls each
+ configured upstream a **source**.
+
-- an **MCP server**
-- an **OpenAPI** specification
-- a **GraphQL** endpoint
-
-The integration describes the catalog of tools; on its own it isn't a live, authenticated
-connection. To actually call tools, you create a [connection](/concepts/connections).
-
-
- See the CLI page for adding an integration from the web UI or the CLI.
+
+ Configure an MCP, OpenAPI, or GraphQL source and review its imported tools.
diff --git a/apps/docs/concepts/policies.mdx b/apps/docs/concepts/policies.mdx
index 85318b611..4e1159c1b 100644
--- a/apps/docs/concepts/policies.mdx
+++ b/apps/docs/concepts/policies.mdx
@@ -1,14 +1,13 @@
---
-title: Policies
-description: "Policies control whether each tool is always allowed, requires approval, or is blocked."
+title: "Legacy concept: policies"
+description: "The supported product controls tools with explicit tool modes."
---
-A **policy** controls what an agent can do with each tool:
+
+ This page preserves an old term for incoming links. The active self-hosted product uses
+ **Inherit**, **Enabled**, **Ask**, and **Disabled** tool modes.
+
-- **Allow**: the tool runs without interruption.
-- **Require approval**: the call pauses until a human approves it.
-- **Block**: the tool can't be called.
-
-Policies start from a sensible default derived from the integration's spec. For example,
-read-only `GET` operations on an OpenAPI spec are allowed by default, while writes can be
-set to require approval. You can tune the policy for any tool at any time.
+
+ Review the global tool set and choose when calls run, wait for approval, or remain unavailable.
+
diff --git a/apps/docs/concepts/sources.mdx b/apps/docs/concepts/sources.mdx
new file mode 100644
index 000000000..161b3930e
--- /dev/null
+++ b/apps/docs/concepts/sources.mdx
@@ -0,0 +1,24 @@
+---
+title: Sources
+description: "A source connects one MCP server, OpenAPI specification, or GraphQL API to Executor's global tool catalog."
+---
+
+A **source** is one configured upstream that contributes tools to Executor's
+global catalog:
+
+- an MCP server over Streamable HTTP
+- a trusted local MCP stdio template approved by the machine administrator
+- an OpenAPI 3.0 or 3.1 specification
+- an introspection-enabled GraphQL API
+
+The source owns its endpoint or process template, imported tool identities,
+refresh history, default tool mode, and at most one encrypted static credential
+profile. Eligible sources can also use Managed OAuth. Add a second source when
+you intentionally need a second upstream.
+
+After adding or refreshing a source, open **Tools** to review every imported
+tool before giving an agent access.
+
+
+ Install the supported self-hosted server, then add a source from its dashboard.
+
diff --git a/apps/docs/concepts/tool-modes.mdx b/apps/docs/concepts/tool-modes.mdx
new file mode 100644
index 000000000..5eec4e20a
--- /dev/null
+++ b/apps/docs/concepts/tool-modes.mdx
@@ -0,0 +1,23 @@
+---
+title: Tool modes
+description: "Use Inherit, Enabled, Ask, and Disabled to control the one global tool catalog."
+---
+
+Every imported tool has one effective mode:
+
+- **Inherit**: follow the source default, then the tool's built-in default.
+- **Enabled**: advertise the tool and run calls immediately.
+- **Ask**: advertise the tool, but pause each call for one administrator decision.
+- **Disabled**: hide the tool from gateway clients and reject calls.
+
+Source defaults let you change a group of tools together. A tool override takes
+priority over the source default, while **Inherit** returns that tool to the
+source and built-in behavior. Broad Enabled or Disabled changes require an
+explicit confirmation in the dashboard.
+
+Ask approvals are exact and single-use. Executor shows a structural, redacted
+argument preview, and a stale tool, source, credential, or mode revision cannot
+reuse an earlier decision.
+
+All dashboard-issued API tokens use the same global tool set. Per-token tool
+catalogs are not part of the current single-user product.
diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index 3a0e19ade..8e1024e7c 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -25,19 +25,19 @@
"groups": [
{
"group": "Get started",
- "pages": ["index", "mcp-proxy"]
+ "pages": ["index"]
},
{
- "group": "Local",
- "pages": ["local/cli", "local/desktop"]
+ "group": "Run Executor",
+ "pages": ["self-hosting", "hosted/docker", "cli", "mcp-proxy"]
},
{
- "group": "Hosted",
- "pages": ["hosted/cloud", "hosted/docker", "hosted/cloudflare"]
+ "group": "Concepts",
+ "pages": ["concepts/sources", "concepts/credentials", "concepts/tool-modes"]
},
{
- "group": "Concepts",
- "pages": ["concepts/integrations", "concepts/connections", "concepts/policies"]
+ "group": "Legacy archive",
+ "pages": ["local/cli", "local/desktop", "hosted/cloud", "hosted/cloudflare"]
}
]
},
@@ -45,18 +45,18 @@
"links": [
{
"label": "GitHub",
- "href": "https://github.com/RhysSullivan/executor"
+ "href": "https://github.com/davis7dotsh/executor-fork-testing"
}
],
"primary": {
"type": "button",
- "label": "Open Executor",
- "href": "https://executor.sh"
+ "label": "Install Executor",
+ "href": "/self-hosting"
}
},
"footer": {
"socials": {
- "github": "https://github.com/RhysSullivan/executor"
+ "github": "https://github.com/davis7dotsh/executor-fork-testing"
}
}
}
diff --git a/apps/docs/hosted/cloud.mdx b/apps/docs/hosted/cloud.mdx
index 15a6df662..8346f8e67 100644
--- a/apps/docs/hosted/cloud.mdx
+++ b/apps/docs/hosted/cloud.mdx
@@ -1,14 +1,14 @@
---
-title: Executor Cloud
-description: "Use hosted Executor Cloud: no setup, with a free tier."
+title: "Legacy: managed cloud"
+description: "Archived reference for the previous managed Executor Cloud service."
---
-Executor Cloud is the hosted version: the same catalog, auth, and policies, run for
-you. There is nothing to install or keep running, and a free tier lets you start
-right away.
+
+ Executor Cloud is archived and is not offered as the supported product. Run Executor on your own
+ Linux or macOS host with the [native binary](/self-hosting) or use the supported [Docker
+ image](/hosted/docker).
+
-Sign in at [executor.sh](https://executor.sh), then add your first
-[integration](/concepts/integrations), create a [connection](/concepts/connections),
-and point your agents at the hosted [MCP endpoint](/mcp-proxy). It is the easiest way
-to share one tool catalog across every agent you use, including cloud agents like
-ChatGPT.
+This page is retained only to identify links and configurations that refer to
+the previous managed deployment. Do not use `executor.sh` as a hosted MCP
+endpoint.
diff --git a/apps/docs/hosted/cloudflare.mdx b/apps/docs/hosted/cloudflare.mdx
index 285642d16..30aa79704 100644
--- a/apps/docs/hosted/cloudflare.mdx
+++ b/apps/docs/hosted/cloudflare.mdx
@@ -1,9 +1,15 @@
---
-title: Self Hosted Cloudflare
-description: "Run Executor as a single Cloudflare Worker in your own account, with Cloudflare Access for auth and D1 for storage."
+title: "Legacy: Cloudflare Worker"
+description: "Archived reference for the previous TypeScript Cloudflare Worker deployment."
---
-Executor runs as a single Cloudflare Worker. Authentication is handled entirely by
+
+ This TypeScript Cloudflare Worker deployment is archived. It is not the supported self-hosted
+ product and does not ship the current Rust and Svelte application. Use [native
+ self-hosting](/self-hosting) or [Docker](/hosted/docker).
+
+
+The archived deployment runs as a single Cloudflare Worker. Authentication is handled entirely by
[Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/)
(there is no separate app login), storage is [D1](https://developers.cloudflare.com/d1/),
tool code runs in QuickJS inside the Worker, and the web console, API, and MCP
@@ -15,12 +21,12 @@ and you manage members and credentials in Cloudflare Access rather than in the a
## Prerequisites
- A Cloudflare account with Workers and Zero Trust (Access) enabled.
-- The [Executor repo](https://github.com/RhysSullivan/executor) checked out, with
+- The [Executor repo](https://github.com/davis7dotsh/executor-fork-testing) checked out, with
dependencies installed (`bun install`).
## Deploy
-From `apps/host-cloudflare`:
+From `legacy/host-cloudflare`:
```bash
bunx wrangler login
diff --git a/apps/docs/hosted/docker.mdx b/apps/docs/hosted/docker.mdx
index c286aa7f1..913d7636c 100644
--- a/apps/docs/hosted/docker.mdx
+++ b/apps/docs/hosted/docker.mdx
@@ -1,95 +1,55 @@
---
-title: Self Hosted Docker
-description: "Run the whole Executor server in one Docker container over a SQLite file: API, MCP, auth, code execution, and the web UI."
+title: Docker self-hosting
+description: "Run the supported Executor Rust server and embedded Svelte dashboard in the published Linux container."
---
-The self-hosted image is the entire Executor server in a single container: the
-typed API, the MCP server, authentication, QuickJS code execution, and the web
-console, all in one process over a libSQL (SQLite) file. There is no external
-database, worker, or proxy to run.
+The supported Docker image contains the same Rust binary and embedded Svelte
+dashboard as the native release. It supports Linux `amd64` and `arm64`.
-## Run it
+## Run the published image
-Using the published image:
+Pin a release version for repeatable deployments. Replace `v0.1.0` with the
+version you intend to run:
```bash
-docker run -d \
- --name executor-selfhost \
- -p 17888:17888 \
- -v executor-data:/data \
- ghcr.io/rhyssullivan/executor-selfhost:latest
+docker run --detach \
+ --name executor \
+ --publish 127.0.0.1:4788:4788 \
+ --volume executor-data:/var/lib/executor \
+ ghcr.io/davis7dotsh/executor:v0.1.0
```
-Or build from a clone of the [repo](https://github.com/RhysSullivan/executor),
-from `apps/host-selfhost`:
+Open `http://127.0.0.1:4788`. On first boot, `docker logs executor` contains the
+one-time setup URL. Create the administrator, then create an API token under
+**API tokens**.
-```bash
-docker compose up -d --build
-```
+The `executor-data` volume contains SQLite state, WAL files when present, and
+the generated `master.key`. Stop the container before backing up the complete
+volume. The database and key are one recovery unit.
-No configuration is required. Open [http://localhost:17888](http://localhost:17888);
-the first person to create an account becomes the owner. After that, people join
-through single-use invite links you mint from the **Admin** page, and open signup
-is closed.
+## Build from a checkout
-## Storage
-
-Everything that must persist (the SQLite database and the generated encryption
-keys) lives in the data directory, mounted at **`/data`** in the container. Always
-mount a volume there so it survives restarts and upgrades:
+The root Compose file builds the supported root `Dockerfile`:
```bash
--v executor-data:/data # named volume (what compose uses)
-# or a host path:
--v /srv/executor:/data
+docker compose up --detach --build
+docker compose logs executor
```
-Back it up by snapshotting that volume (or copying `/data`, primarily `data.db`).
-`EXECUTOR_DATA_DIR` (default `/data`) sets the directory, and `EXECUTOR_DB_PATH`
-(default `/data.db`) sets the database file.
-
-## Environment variables
+Compose publishes the service only on host loopback and mounts the
+`executor-data` volume at `/var/lib/executor`.
-Everything is optional: a bare run boots a working instance. The defaults below are
-the container defaults.
+## HTTPS reverse proxy
-| Variable | Default | Purpose |
-| ----------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
-| `PORT` | `17888` | HTTP port the server listens on. |
-| `EXECUTOR_HOST` | `0.0.0.0` | Bind address. The image binds all interfaces. |
-| `EXECUTOR_DATA_DIR` | `/data` | Directory holding the database and generated keys. |
-| `EXECUTOR_DB_PATH` | `/data.db` | SQLite database file. |
-| `EXECUTOR_WEB_BASE_URL` | auto (`http://localhost:17888`) | Public URL browsers use. Required behind a domain or TLS (see below). |
-| `BETTER_AUTH_SECRET` | generated, persisted in `/data` | Session secret (32+ chars). Rotating it signs everyone out. |
-| `EXECUTOR_SECRET_KEY` | generated, persisted in `/data` | Master key encrypting stored secrets. Set it to manage it yourself. |
-| `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL` | unset | Pre-create the admin headlessly (with the password below); skips browser first-run. |
-| `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` | unset | Password for the bootstrap admin. |
-| `EXECUTOR_BOOTSTRAP_ADMIN_NAME` | `Admin` | Display name for the bootstrap admin. |
-| `EXECUTOR_ORG_NAME` | `Default` | Display name of the single org every user joins. |
-| `EXECUTOR_ORG_SLUG` | `default` | URL slug for that org. |
-| `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Allow sandboxed code to reach loopback / private addresses. Keep off unless you trust the code. |
-
-## Behind a domain or TLS
-
-
- Set `EXECUTOR_WEB_BASE_URL` to the exact public URL you load in the browser (scheme, host, and
- port). If it does not match, browser logins are rejected with an invalid-origin error.
-
+Leave the container port bound to host loopback. Set the exact browser-facing
+origin and terminate TLS in a reverse proxy on the same host:
```bash
-docker run -d \
- -p 17888:17888 \
- -v executor-data:/data \
- -e EXECUTOR_WEB_BASE_URL=https://executor.example.com \
- ghcr.io/rhyssullivan/executor-selfhost:latest
+EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com docker compose up --detach
```
-For a headless deploy (CI or infra-as-code), set `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL`
-and `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` to create the admin without the browser
-setup screen.
-
-## Connect an agent
+Do not expose the raw plaintext port to an untrusted network. A private LAN
+alone does not protect dashboard sessions or MCP Bearer tokens.
-The server exposes a streamable-HTTP MCP endpoint at `/mcp`. Point your client at
-`http://localhost:17888/mcp` (or your public URL). See [MCP Proxy](/mcp-proxy) for
-how that endpoint exposes your integrations.
+The MCP endpoint is `/mcp`. See [MCP endpoint](/mcp-proxy) for client
+configuration.
diff --git a/apps/docs/index.mdx b/apps/docs/index.mdx
index 0b019f4fa..231ae5233 100644
--- a/apps/docs/index.mdx
+++ b/apps/docs/index.mdx
@@ -1,64 +1,65 @@
---
title: Introduction
-description: "Executor is the open-source integration layer for AI agents: one catalog for every tool, shared across every agent you use."
+description: "Executor is a single-user, self-hosted tool gateway for AI agents, delivered as one Rust binary with an embedded Svelte dashboard."
---
-Configure every integration once (MCP servers, OpenAPI specs, GraphQL
-endpoints) with authentication and per-tool [policies](/concepts/policies),
-then use that same catalog from any MCP-compatible agent.
+Executor is a single-user, self-hosted tool gateway for AI agents. One Rust
+binary serves the Svelte dashboard, stores state in SQLite, exposes an MCP
+endpoint, and runs TypeScript tool workflows in isolated QuickJS workers.
+
+Add OpenAPI, GraphQL, and MCP [sources](/concepts/sources), configure each
+source's [credential profile or Managed OAuth](/concepts/credentials), and
+review its [tool modes](/concepts/tool-modes). Any MCP-compatible agent can then
+use the same global catalog.
## Get started
-Choose how to run Executor:
+Choose one supported deployment:
+
+- **[Native Linux or macOS](/self-hosting)**: install the release archive and run
+ the `executor` binary.
+- **[Docker](/hosted/docker)**: run the same Rust server and embedded dashboard in
+ the published Linux container.
-- **[CLI](/local/cli)**: a background service on your machine, from the terminal
-- **[Desktop app](/local/desktop)**: the same runtime, as a desktop app
-- **[Executor Cloud](/hosted/cloud)**: hosted, with a free tier; nothing to run yourself
-- **Self-host**: your own infrastructure, via [Docker](/hosted/docker) or [Cloudflare](/hosted/cloudflare)
+Windows is not a native release target. The previous npm CLI, Electron desktop
+app, managed cloud service, and Cloudflare Worker are retained only in the
+**Legacy archive** section of these docs.
## Set up with your agent
Prefer to let your coding agent do the setup? Copy this prompt and paste it into
Claude, Cursor, or any MCP-capable agent. It will help you pick the right form
-of Executor, install it, connect over MCP, and get your first integration
+of Executor, install it, connect over MCP, and get your first source
working.
```text Setup prompt
-Help me set up Executor and get my first connection working.
-
-Executor is an open source integration layer for AI agents: one place to configure every integration (MCP servers, OpenAPI specs, GraphQL APIs) and connect to them over MCP.
-
-Start by helping me pick the right form to run it in. Chat with me about it rather than jumping straight to a yes/no question, and recommend one. If I just want the fastest path, suggest Executor Cloud (free tier, nothing to install). All forms expose the same functionality, just packaged differently:
+Help me set up Executor and get my first source working.
-Local (everything stays on my machine):
-- Desktop app: a native app for Mac, Windows, and Linux. Best for a regular desktop environment.
-- CLI (`executor`): best for a headless or server environment.
-Both run a local HTTP server as a background service that any MCP client can connect to.
+Executor is an open source gateway for sources and tools. It turns MCP servers, OpenAPI specifications, and GraphQL APIs into one global tool catalog exposed through one MCP endpoint.
-Hosted (use it from multiple agents, including cloud ones, with nothing running locally):
-- Executor Cloud: hosted, generous free tier, sign in and start immediately.
-- Self-hosted: a Docker image or a Cloudflare Worker.
+Executor is a single-user, self-hosted tool gateway. The supported product is one Rust binary with an embedded Svelte dashboard and SQLite state. It is available as native Linux and macOS archives, or as a Linux Docker image. Windows is not a native release target. Do not install the archived npm CLI, Electron desktop app, managed cloud service, or Cloudflare Worker.
-How to think about it:
-- Want all your data on your own machine? Go local: the desktop app for a regular environment, the CLI for a headless one.
-- Want to use it from multiple agents (including cloud agents like ChatGPT), or not run anything locally? Go hosted: Executor Cloud is the fastest start; the self-hosted Docker or Cloudflare versions give you full control.
+Help me choose between the native release and Docker:
+- Native: best for a Linux or macOS machine where I want a normal executable and local service management.
+- Docker: best when I already operate containers or need a Linux container deployment.
Terms you'll come across:
-- Integration: anything you add (an MCP server, an OpenAPI spec, a GraphQL API).
-- Connection: one configured instance of an integration. An integration can have many connections, and a connection doesn't have to be authenticated.
-- Policy: whether each tool is always allowed, requires approval, or is blocked. Policies start from a sensible default derived from the imported spec (for example, GET requests on an OpenAPI spec are allowed by default).
+- Source: one configured MCP server, OpenAPI specification, or GraphQL API. A source owns its imported tools and at most one encrypted static credential profile. Eligible sources can also use Managed OAuth.
+- Credential profile: the source-owned API key, bearer token, basic credentials, or manual OAuth token. Saving a replacement profile replaces the old one.
+- Tool mode: Inherit follows the source and built-in defaults. Enabled runs immediately, Ask waits for the administrator, and Disabled is unavailable to gateway clients.
Once you know which form I want:
-1. Walk me through installing it.
-2. Connect Executor to you over MCP. Most MCP clients only load servers at startup, so after adding it I may need to restart the client or open a new chat before the Executor tools appear. Tell me if that's needed and wait for me to do it before continuing.
-3. Once the tools are available, help me add my first integration and get one tool working end to end.
+1. Follow https://executor.sh/docs/self-hosting or https://executor.sh/docs/hosted/docker.
+2. Start the server, open its one-time setup URL, create the administrator, and create an API token in the dashboard.
+3. Connect to the Streamable HTTP MCP endpoint at the instance origin plus `/mcp`, using the API token as a Bearer token. If the client supports only stdio, configure it to launch `executor mcp` with `EXECUTOR_API_TOKEN` set.
+4. Help me add my first source, review its tool modes, and get one tool working end to end.
Docs: https://executor.sh/docs
-Source (and the place to start if something breaks): https://github.com/RhysSullivan/executor
+Source (and the place to start if something breaks): https://github.com/davis7dotsh/executor-fork-testing
```
## Concepts
-- **[Integrations](/concepts/integrations)**: what Executor connects to
-- **[Connections](/concepts/connections)**: configured instances of an integration
-- **[Policies](/concepts/policies)**: what each tool is allowed to do
+- **[Sources](/concepts/sources)**: OpenAPI, GraphQL, MCP HTTP, and trusted MCP stdio upstreams
+- **[Credentials and OAuth](/concepts/credentials)**: source-owned authentication
+- **[Tool modes](/concepts/tool-modes)**: Inherit, Enabled, Ask, and Disabled behavior
diff --git a/apps/docs/local/cli.mdx b/apps/docs/local/cli.mdx
index 062357432..2c2627272 100644
--- a/apps/docs/local/cli.mdx
+++ b/apps/docs/local/cli.mdx
@@ -1,111 +1,41 @@
---
-title: CLI
-description: "Run Executor as a local background service and drive it from the command line."
+title: "Legacy: npm CLI"
+description: "Archived reference for the previous TypeScript npm CLI. It is not the supported Executor product."
---
-The CLI runs a small HTTP runtime on your machine: a durable background service
-that any MCP-compatible agent can connect to. Your integrations, credentials, and
-policies stay local.
-
-## Install
-
-Requires Node.js 20 or newer.
-
-
-
-```bash npm
-npm install -g executor
-```
-
-```bash pnpm
-pnpm add -g executor
-```
-
-```bash bun
-bun add -g executor
-```
-
-```bash yarn
-yarn global add executor
-```
-
-
-
-## Start the service
-
-Install the durable background service, then open the web UI:
-
-```bash
-executor install # install/start the durable background service
-executor web # open the web UI at http://127.0.0.1:17888
-```
-
-`executor install` registers Executor so it keeps running across restarts. For a
-throwaway foreground runtime instead, run `executor web --foreground`.
-
-## Connect an agent
-
-Add Executor to any MCP client (Claude Code, Cursor, OpenCode) with `npx add-mcp`.
-It detects the client and writes its config for you. Connect over HTTP or through
-the CLI:
-
-
-
- Executor serves a streamable-HTTP MCP endpoint at `http://127.0.0.1:17888/mcp`:
-
- ```bash
- npx add-mcp http://127.0.0.1:17888/mcp --transport http --name executor
- ```
-
- The **Connect** card in the web UI shows this command with your exact URL
- (and port, if it differs) already filled in.
-
-
-
- With the `executor` CLI on your PATH, the client launches `executor mcp`
- directly (no URL needed):
-
- ```bash
- npx add-mcp "executor mcp" --name executor
- ```
-
-
-
-
-## Add an integration
-
-From the web UI, click **Add Source** and paste an OpenAPI, GraphQL, or MCP URL.
-Executor detects the type, indexes the tools, and handles auth. Or add one from the
-CLI:
-
-```bash
-executor call executor openapi addSource '{
- "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
- "namespace": "petstore",
- "baseUrl": "https://petstore3.swagger.io/api/v3"
-}'
-```
-
-Use `baseUrl` when the OpenAPI document has relative `servers` entries (e.g. `"/api/v3"`).
-
-Confirm it's live:
-
-```bash
-executor tools sources # lists configured integrations + tool counts
-```
-
-## Call tools
-
-```bash
-executor tools search "send email" # find tools by intent
-executor call github issues --help # browse a namespace
-executor call github issues create '{"owner":"octocat","repo":"Hello-World","title":"Hi"}'
-```
-
-`executor call`, `executor resume`, and `executor tools …` auto-start the local
-daemon if it isn't already running. If an execution pauses for auth or approval,
-resume it:
-
-```bash
-executor resume --execution-id exec_123
-```
+
+ The TypeScript npm CLI is retired and unsupported. Historical registry artifacts may still
+ resolve, but they are no longer updated. Do not use them for a new installation. Install the
+ current Rust binary through [native self-hosting](/self-hosting), then use its [Rust CLI](/cli).
+
+
+This page preserves context for repositories and old configuration files that
+mention the previous npm-distributed CLI. It is an archive, not an installation
+or operations guide.
+
+## What the archived CLI did
+
+The TypeScript CLI previously installed a local daemon, opened a React web UI,
+configured MCP clients with a helper package, and auto-started the daemon for
+some commands. It used the earlier integrations, connections, and policies
+model. Those commands, package releases, and background-service workflows are
+not part of the supported product.
+
+Existing references to the old package should be treated as migration clues
+only. Do not install a cached package or reuse its daemon and web commands for a
+new Executor instance.
+
+## Use the supported product
+
+1. Install and start the current single-user Rust server through [native
+ self-hosting](/self-hosting), or use the supported [Docker image](/hosted/docker).
+2. Open the one-time setup URL, create the administrator, and issue an API token
+ from the dashboard.
+3. Use the current [Rust CLI](/cli) to search and call tools, or connect an agent
+ through the authenticated [MCP endpoint](/mcp-proxy).
+4. Recreate old upstream configuration as OpenAPI, GraphQL, MCP HTTP, or trusted
+ MCP stdio [sources](/concepts/sources). Configure credentials on each source
+ and review its [tool modes](/concepts/tool-modes).
+
+The current Rust CLI does not auto-start the server or reuse the archived local
+daemon. Follow the linked supported guides for exact commands and ports.
diff --git a/apps/docs/local/desktop.mdx b/apps/docs/local/desktop.mdx
index b8a8a822c..2334bb3a2 100644
--- a/apps/docs/local/desktop.mdx
+++ b/apps/docs/local/desktop.mdx
@@ -1,9 +1,14 @@
---
-title: Desktop app
-description: "Run Executor locally with a native desktop app: a graphical console that runs alongside the CLI."
+title: "Legacy: Electron desktop app"
+description: "Archived reference for the previous Electron desktop application."
---
-The desktop app is Executor running locally with a graphical console in a native
+
+ The Electron desktop application is archived and is not a supported release target. Use the
+ current [native Rust binary](/self-hosting), which serves its Svelte dashboard in the browser.
+
+
+The archived desktop app is Executor running locally with a graphical console in a native
window. It's a companion to the [CLI](/local/cli), not a replacement: both drive the
same local background service, so you can use them at the same time.
diff --git a/apps/docs/mcp-proxy.mdx b/apps/docs/mcp-proxy.mdx
index c142f31b3..6810f7367 100644
--- a/apps/docs/mcp-proxy.mdx
+++ b/apps/docs/mcp-proxy.mdx
@@ -1,58 +1,71 @@
---
-title: MCP Proxy
-description: "Executor is one MCP endpoint in front of all your integrations: connect once, and every agent gets the same catalog with shared auth and policies."
+title: MCP endpoint
+description: "Connect an MCP client to the supported self-hosted Executor server over Streamable HTTP or the local stdio bridge."
---
-Executor sits between your agents and your tools as a single MCP endpoint. Your
-agents connect to Executor; Executor connects out to your integrations and
-re-exposes them as one catalog. Every tool call passes through the proxy, which is
-where auth and policy live.
-
-## Why proxy through Executor
-
-- **One endpoint, every agent.** Point Claude Code, Cursor, ChatGPT, or your own
- SDK at the same Executor endpoint instead of configuring each tool in each client.
-- **Credentials stay out of the agent.** A [connection](/concepts/connections)'s
- credentials are stored by Executor and attached to the upstream call. The agent
- runs in a sandbox and never sees them.
-- **Per-tool policies.** Every call is governed by a [policy](/concepts/policies):
- allow, require approval, or block.
-- **Mix [integration types](/concepts/integrations).** Upstream MCP servers, OpenAPI
- specs, and GraphQL endpoints all show up in the same catalog.
-
-## How it works
-
-```mermaid
-flowchart LR
- A[Your agents] -->|MCP| E[Executor]
- E -->|MCP| M[Upstream MCP servers]
- E -->|HTTP| O[OpenAPI APIs]
- E -->|HTTP| G[GraphQL endpoints]
+Executor sits between your agents and your tools as one authenticated MCP
+endpoint. Agents connect to Executor, while Executor connects to OpenAPI,
+GraphQL, and upstream MCP sources. Tool modes, approval, credentials, and
+invocation logs stay under the self-hosted instance's control.
+
+## Streamable HTTP
+
+The endpoint is the instance origin plus `/mcp`, such as
+`http://127.0.0.1:4788/mcp`. Create an API token in the dashboard and send it as
+a Bearer token on every MCP request:
+
+```text
+Authorization: Bearer
```
-Agents speak MCP to Executor. For each tool call, Executor picks the right
-integration, attaches that connection's credentials to the upstream request,
-enforces the tool's policy, and returns the result. The agent only ever talks to
-Executor, and the credentials never reach it.
+MCP client configuration schemas differ, but the common shape is:
-## Proxy an upstream MCP server
+```json
+{
+ "mcpServers": {
+ "executor": {
+ "type": "http",
+ "url": "http://127.0.0.1:4788/mcp",
+ "headers": {
+ "Authorization": "Bearer "
+ }
+ }
+ }
+}
+```
-Add an MCP server as an [integration](/concepts/integrations) and its tools join
-your catalog alongside everything else. Create a [connection](/concepts/connections)
-to it (with credentials if it needs them), and it becomes reachable through your one
-Executor endpoint, governed by the same policies as the rest of your tools.
+Use HTTPS for a remote origin. A private LAN alone does not protect a reusable
+Bearer token.
-This is the core of the proxy: your agents keep talking to a single endpoint while
-you add, swap, or remove upstream servers behind it, with no client-side change.
+## Local stdio bridge
+
+If a client launches only stdio MCP servers, configure it to run the same
+supported binary in bridge mode:
+
+```json
+{
+ "mcpServers": {
+ "executor": {
+ "command": "/absolute/path/to/executor",
+ "args": ["mcp"],
+ "env": {
+ "EXECUTOR_API_TOKEN": ""
+ }
+ }
+ }
+}
+```
-## Connect your agents
+The bridge connects to `http://127.0.0.1:4788` by default. Set
+`EXECUTOR_BASE_URL` when the server uses another origin. The server must already
+be running.
-Agents connect to Executor's MCP endpoint. The exact command depends on how you run
-Executor:
+## What clients see
-- **Local:** see [CLI](/local/cli) for `executor mcp` and the `add-mcp` command.
-- **Hosted:** see [Executor Cloud](/hosted/cloud), or self-host on
- [Docker](/hosted/docker) or [Cloudflare](/hosted/cloudflare).
+Enabled and Ask tools from every configured source appear in the instance's
+global catalog. Disabled tools are not advertised. Ask calls pause for the
+administrator's decision, and credentials are resolved by Executor instead of
+being exposed to the MCP client.
-Once a client is connected, every integration you add to Executor appears in that
-agent automatically.
+Start with [native self-hosting](/self-hosting), [Docker](/hosted/docker), or the
+[Rust CLI](/cli).
diff --git a/apps/docs/self-hosting.mdx b/apps/docs/self-hosting.mdx
new file mode 100644
index 000000000..0c69f103f
--- /dev/null
+++ b/apps/docs/self-hosting.mdx
@@ -0,0 +1,68 @@
+---
+title: Native self-hosting
+description: "Install and run the supported Executor Rust binary on Linux or macOS."
+---
+
+Executor ships as one native binary with the Svelte dashboard embedded. Native
+releases support Linux and macOS on x86-64 and ARM64. Windows is not a native
+release target.
+
+## Install a release
+
+The installer downloads the matching archive, verifies its checksum, and places
+`executor` under `$HOME/.executor/bin` by default:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/davis7dotsh/executor-fork-testing/main/scripts/install.sh | bash
+```
+
+To install a release published by another fork, set the same repository
+override used by the installer:
+
+```bash
+export EXECUTOR_REPOSITORY=owner/repository
+curl -fsSL "https://raw.githubusercontent.com/$EXECUTOR_REPOSITORY/main/scripts/install.sh" | bash
+```
+
+Start a new shell, source its configuration file, or invoke
+`$HOME/.executor/bin/executor` directly until the updated `PATH` is active.
+The installer serializes install, recovery, and uninstall under a private lock,
+and uses stock `sync` durability barriers before publishing or retiring its
+ownership state. Custom install paths cannot contain symbolic-link or writable
+ancestor components. PATH edits use the same portable shell implementation on
+Linux and macOS, with no Python runtime dependency.
+
+## First boot
+
+Choose a private data directory and start the server:
+
+```bash
+mkdir -p "$HOME/.executor-data"
+chmod 0700 "$HOME/.executor-data"
+executor server --data-dir "$HOME/.executor-data"
+```
+
+Executor binds to `127.0.0.1:4788` by default. Open the one-time setup URL
+printed by the server, create the administrator, then create an API token under
+**API tokens**. Token secrets are shown once.
+
+The data directory contains the SQLite database, WAL files when present, and
+`master.key`. Stop Executor before backing up the complete directory, and keep
+the database and key together.
+
+## Expose another origin
+
+Keep plain HTTP on loopback. When a reverse proxy exposes Executor through
+HTTPS, set the exact browser-facing origin before starting the server:
+
+```bash
+executor server \
+ --data-dir "$HOME/.executor-data" \
+ --public-origin https://executor.example.com
+```
+
+The public origin does not secure Executor's own listener. Keep that listener on
+loopback or otherwise isolate it so clients reach it only through the TLS proxy.
+
+Next, use the [Rust CLI](/cli) or connect an agent through the
+[MCP endpoint](/mcp-proxy).
diff --git a/apps/marketing/astro.config.mjs b/apps/marketing/astro.config.mjs
index 877af5b81..aa31603ba 100644
--- a/apps/marketing/astro.config.mjs
+++ b/apps/marketing/astro.config.mjs
@@ -9,7 +9,7 @@ import react from "@astrojs/react";
import cloudflare from "@astrojs/cloudflare";
// Single source of truth for public build-time vars: wrangler.toml `[vars]`.
-// Mirrors apps/cloud, which reads its wrangler.jsonc vars the same way. The
+// Mirrors legacy/cloud, which reads its wrangler.jsonc vars the same way. The
// PUBLIC_ ones are inlined into the client bundle via Vite `define`, so the
// browser PostHog SDK gets the key at build time; they also remain runtime
// Worker bindings.
diff --git a/apps/marketing/package.json b/apps/marketing/package.json
index 6b3592652..74cf3cc8f 100644
--- a/apps/marketing/package.json
+++ b/apps/marketing/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/marketing",
"version": "0.0.5",
+ "private": true,
"type": "module",
"scripts": {
"dev": "bun run dev:proxy && bun run dev:vite",
diff --git a/apps/marketing/src/components/LegalLayout.astro b/apps/marketing/src/components/LegalLayout.astro
index b51755d22..255e3f57f 100644
--- a/apps/marketing/src/components/LegalLayout.astro
+++ b/apps/marketing/src/components/LegalLayout.astro
@@ -58,7 +58,7 @@ const { title, description, lastUpdated } = Astro.props;
Privacy
Terms
GitHub
diff --git a/apps/marketing/src/layouts/Layout.astro b/apps/marketing/src/layouts/Layout.astro
index f7fd865e4..4a9cfd3ec 100644
--- a/apps/marketing/src/layouts/Layout.astro
+++ b/apps/marketing/src/layouts/Layout.astro
@@ -7,8 +7,8 @@ interface Props {
}
const {
- title = 'Executor — The gateway to connect your agent to everything',
- description = 'One place every agent plugs into every tool you already use. Wire it up once, bring the whole team.'
+ title = 'Executor: the self-hosted tool gateway for AI agents',
+ description = 'One Rust binary, one Svelte dashboard, and one MCP endpoint for the tools you run on your own infrastructure.'
} = Astro.props
const ogImage = new URL('/og-image.png', Astro.site ?? Astro.url).toString()
diff --git a/apps/marketing/src/pages/index.astro b/apps/marketing/src/pages/index.astro
index 4e977a998..ff4103be9 100644
--- a/apps/marketing/src/pages/index.astro
+++ b/apps/marketing/src/pages/index.astro
@@ -1,50 +1,40 @@
---
import Layout from "../layouts/Layout.astro";
import { AnimatedBeamDemo } from "../components/animated-beam-demo";
-import { ContextBloatDemo } from "../components/context-bloat-demo";
-import { SelfHostContactModal } from "../components/self-host-contact-modal";
// Imported so Vite emits it as a hashed build asset (served from /_astro/...);
// the deploy pipeline does not pick up newly added /public files.
import ycBackedBy from "../assets/yc-backed-by.svg?url";
-const GH = "https://github.com/RhysSullivan/executor";
+const GH = "https://github.com/davis7dotsh/executor-fork-testing";
// Copied to the clipboard by the "Set up with your agent" hero CTA. A visitor
// pastes it into their coding agent (Claude, Cursor, ...) and the agent picks
// the right form of Executor, installs it, connects over MCP, and gets a first
-// connection working. Keep in sync with the docs "Set up with your agent"
+// source working. Keep in sync with the docs "Set up with your agent"
// section (apps/docs/index.mdx).
-const setupPrompt = `Help me set up Executor and get my first connection working.
+const setupPrompt = `Help me set up Executor and get my first source working.
-Executor is an open source integration layer for AI agents: one place to configure every integration (MCP servers, OpenAPI specs, GraphQL APIs) and connect to them over MCP.
+Executor is an open source gateway for sources and tools. It turns MCP servers, OpenAPI specifications, and GraphQL APIs into one global tool catalog exposed through one MCP endpoint.
-Start by helping me pick the right form to run it in. Chat with me about it rather than jumping straight to a yes/no question, and recommend one. If I just want the fastest path, suggest Executor Cloud (free tier, nothing to install). All forms expose the same functionality, just packaged differently:
+Executor is a single-user, self-hosted tool gateway. The supported product is one Rust binary with an embedded Svelte dashboard and SQLite state. It is available as native Linux and macOS archives, or as a Linux Docker image. Windows is not a native release target. Do not install the archived npm CLI, Electron desktop app, managed cloud service, or Cloudflare Worker.
-Local (everything stays on my machine):
-- Desktop app: a native app for Mac, Windows, and Linux. Best for a regular desktop environment.
-- CLI (\`executor\`): best for a headless or server environment.
-Both run a local HTTP server as a background service that any MCP client can connect to.
-
-Hosted (use it from multiple agents, including cloud ones, with nothing running locally):
-- Executor Cloud: hosted, generous free tier, sign in and start immediately.
-- Self-hosted: a Docker image or a Cloudflare Worker.
-
-How to think about it:
-- Want all your data on your own machine? Go local: the desktop app for a regular environment, the CLI for a headless one.
-- Want to use it from multiple agents (including cloud agents like ChatGPT), or not run anything locally? Go hosted: Executor Cloud is the fastest start; the self-hosted Docker or Cloudflare versions give you full control.
+Help me choose between the native release and Docker:
+- Native: best for a Linux or macOS machine where I want a normal executable and local service management.
+- Docker: best when I already operate containers or need a Linux container deployment.
Terms you'll come across:
-- Integration: anything you add (an MCP server, an OpenAPI spec, a GraphQL API).
-- Connection: one configured instance of an integration. An integration can have many connections, and a connection doesn't have to be authenticated.
-- Policy: whether each tool is always allowed, requires approval, or is blocked. Policies start from a sensible default derived from the imported spec (for example, GET requests on an OpenAPI spec are allowed by default).
+- Source: one configured MCP server, OpenAPI specification, or GraphQL API. A source owns its imported tools and at most one encrypted static credential profile. Eligible sources can also use Managed OAuth.
+- Credential profile: the source-owned API key, bearer token, basic credentials, or manual OAuth token. Saving a replacement profile replaces the old one.
+- Tool mode: Inherit follows the source and built-in defaults. Enabled runs immediately, Ask waits for the administrator, and Disabled is unavailable to gateway clients.
Once you know which form I want:
-1. Walk me through installing it.
-2. Connect Executor to you over MCP. Most MCP clients only load servers at startup, so after adding it I may need to restart the client or open a new chat before the Executor tools appear. Tell me if that's needed and wait for me to do it before continuing.
-3. Once the tools are available, help me add my first integration and get one tool working end to end.
+1. Follow https://executor.sh/docs/self-hosting or https://executor.sh/docs/hosted/docker.
+2. Start the server, open its one-time setup URL, create the administrator, and create an API token in the dashboard.
+3. Connect to the Streamable HTTP MCP endpoint at the instance origin plus \`/mcp\`, using the API token as a Bearer token. If the client supports only stdio, configure it to launch \`executor mcp\` with \`EXECUTOR_API_TOKEN\` set.
+4. Help me add my first source, review its tool modes, and get one tool working end to end.
Docs: https://executor.sh/docs
-Source (and the place to start if something breaks): https://github.com/RhysSullivan/executor`;
+Source (and the place to start if something breaks): https://github.com/davis7dotsh/executor-fork-testing`;
---
@@ -73,11 +63,6 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
>
{/* For agents: hand setup to a coding agent, or read the docs */}
@@ -194,31 +179,51 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
class="text-[clamp(1.5rem,3.4vw,2.2rem)] font-medium tracking-[-0.02em] leading-[1.28] text-ink text-balance"
>
Wiring tools to agents is fiddly, per-client, and easy to get wrong.
- Executor makes every tool, from any protocol, look the
- same : one name, one input
- schema, one output schema, so any agent can call any of them
- the same way.
+ Executor makes every MCP, OpenAPI, and GraphQL tool look the
+ same : one name, one input schema,
+ one output schema, so any agent can call any of them the same way.
- {/* ─── NO CONTEXT BLOAT (interactive) ─── */}
+ {/* ─── ONE GATEWAY ─── */}
-
Context efficiency
+
One catalog
- Thousands of tools, no bloat .
+ Connect once, then decide what can run .
- Connect everything you use and Executor still shows the model a
- single tool. It searches your catalog and loads a tool's schema
- only when the code actually calls it, so the prompt never
- balloons.
+ Every supported client reaches the same catalog through one MCP
+ endpoint. Add sources in the dashboard, review their tools, and
+ keep tool-mode decisions with the server you operate.
-
+
+
+
Stable endpoint
+
+ One MCP address for every source
+
+
+ Agents authenticate to the instance at /mcp. OpenAPI,
+ GraphQL, and upstream MCP tools all join the same global catalog.
+
+
+
+
Human control
+
+ Inherit, Enabled, Ask, or Disabled
+
+
+ Inherit follows source and built-in defaults. Enabled tools can
+ run, Ask tools pause for your decision, and Disabled tools stay
+ out of the advertised catalog.
+
+
+
@@ -246,13 +251,12 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
>
One tool shape
- MCP, OpenAPI, GraphQL, or a custom integration. Under the hood
- they all become a tool name, an input schema, and an output
- schema.
+ MCP, OpenAPI, and GraphQL sources all become tool names, input
+ schemas, and output schemas in one catalog.
- {/* Code-mode */}
+ {/* One supported runtime */}
-
Call it any way
+
One binary, every surface
- Today it is a code-mode MCP. It could just as well be the
- Executor CLI, a one-off script, a gen-UI dashboard, or a reusable
- workflow. Same tools, every surface.
+ The Rust binary serves the Svelte dashboard, MCP endpoint, and
+ command-line client. Every surface uses the same SQLite-backed
+ catalog and tool-mode decisions.
- {/* Trace every call (coming soon) */}
-
-
Coming soon
+ {/* Review every call */}
+
-
Trace every call
+
Review every call
- One place to see every run and tool call. Audit any
- decision after the fact.
+ Inspect runs and tool calls from the dashboard. See inputs,
+ results, failures, and approval decisions after the fact.
@@ -318,7 +321,7 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
- {/* Teams */}
+ {/* Self-hosted control */}
-
Set up once, whole team has it
+
Your gateway, under your control
- Per-user credentials and shared ones. No onboarding ritual, no
- toggling MCPs on and off mid-task.
+ Run one single-user instance on your own Linux or macOS host, or
+ in Docker. State and credentials stay with the server you
+ operate.
@@ -348,8 +352,9 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
Destructive actions pull you back in
Executor keeps the semantics it imported: GET vs DELETE for
- OpenAPI, destructiveHint for MCP, mutations for GraphQL. Agents
- auto-run the safe stuff and ask before the rest.
+ OpenAPI, destructiveHint for MCP, and mutations for GraphQL.
+ Review those defaults as Enabled, Ask, or Disabled before an
+ agent calls them.
@@ -409,85 +414,58 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
- {/* Cloud (recommended: fastest start) */}
+ {/* Native Linux and macOS */}
- Cloud
+ Native
-
executor.sh/cloud
-
- Hosted Executor. Auth, sync, policies, and your whole team online
- in five minutes. Free tier to start.
+ Linux · macOS
+
+ Install one checksum-verified Rust binary with the Svelte
+ dashboard embedded. Releases support x86-64 and ARM64.
-
Try Cloud →
+
Install the binary →
-
+ Windows is not a native release target.
- {/* Desktop (native app, local) */}
+ {/* Docker */}
- Desktop
+ Docker
-
Mac · Windows · Linux
+
linux/amd64 · linux/arm64
- A native app that runs entirely on your machine. Your
- integrations, credentials, and sessions never leave the device.
- MIT licensed.
+ Run the same Rust server and embedded dashboard in the published
+ Linux image, with SQLite state in a persistent volume.
-
+
Run with Docker →
- {/* CLI (headless / servers) */}
+ {/* Source */}
- CLI
+ Build from source
-
npm i -g executor
+
Rust · Svelte · MIT
- Run Executor as a background service and drive it from your
- terminal. Best for headless and server environments. MIT
- licensed.
+ Inspect the implementation, build the embedded dashboard and
+ release binary, or adapt the self-hosted runtime for your own
+ environment.
-
Read the docs →
+
View on GitHub →
- View source → Use the Rust CLI →
@@ -550,9 +528,9 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
>
- Any MCP client (Claude Code, Cursor, Codex, and others), the
- Executor CLI, or a native client you drop in. Because tools share
- one shape, the calling surface is interchangeable.
+ Any Streamable HTTP MCP client can connect to the server with a
+ dashboard-issued API token. The same Rust binary also provides a
+ command-line client and an MCP stdio bridge.
@@ -569,8 +547,8 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
Executor preserves the semantics of whatever it imported: GET vs
DELETE for OpenAPI, destructiveHint for MCP, and mutations for
- GraphQL. That tells the agent what it can run on its own and what
- should pull you back into the loop.
+ GraphQL. Those defaults become explicit Inherit, Enabled, Ask,
+ or Disabled modes that you can review in the dashboard.
@@ -585,146 +563,15 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
>
- Yes. Executor is MIT licensed and built on the SDK we publish to
- npm. Run the desktop app locally, self-host the server, or use
- the hosted cloud. Same code paths, different deployment.
+ Yes. Executor is MIT licensed and self-hosted. Run the supported
+ Rust binary on Linux or macOS, or run the same server and
+ embedded Svelte dashboard through Docker.
- {/* ─── PRICING ─── */}
-
-
-
-
Pricing
-
- Start free, pay as you run .
-
-
-
-
- {/* Free */}
-
-
- Get started
-
-
- Free
-
-
For small teams getting started
-
- $0
- / month
-
-
Start free →
-
- {[
- "Up to 3 members",
- "10,000 included executions per month",
- "$0.20 per 1,000 additional executions",
- "Unlimited integrations",
- ].map((f) => (
-
- {f}
-
- ))}
-
-
-
- {/* Team */}
-
-
- Recommended
-
-
- Team
-
-
For growing organizations
-
- $150
- / org / month
-
-
Start Team →
-
- {[
- "Unlimited members",
- "250,000 included executions per month",
- "5 minute execution timeout",
- "Join by team domain",
- "$0.20 per 1,000 additional executions",
- ].map((f) => (
-
- {f}
-
- ))}
-
-
-
- {/* Enterprise */}
-
-
- Custom needs
-
-
- Enterprise
-
-
For orgs with custom needs
-
- Custom
-
-
Contact us →
-
Everything in Team, plus
-
- {[
- "Self-hosted or dedicated cloud deployment support",
- "SSO / SAML & SCIM provisioning",
- "Audit logs for every tool call",
- "Dedicated support & onboarding",
- "Security reviews, DPA & SOC 2 on request",
- ].map((f) => (
-
- {f}
-
- ))}
-
-
-
-
-
-
{/* ─── FINAL CTA ─── */}
@@ -741,13 +588,6 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
>Star on GitHub →
-
- or try Executor Cloud →
-
@@ -860,85 +700,6 @@ Source (and the place to start if something breaks): https://github.com/RhysSull
}
-
-
break,
+ (None, Some(_)) => panic!("index.html contains a closing script tag without an opener"),
+ (Some(open), Some(close)) if close < open => {
+ panic!("index.html contains a closing script tag without an opener")
+ }
+ (Some(open), _) => {
+ let open_end = index[open + OPEN.len()..]
+ .iter()
+ .position(|byte| *byte == b'>')
+ .map(|offset| open + OPEN.len() + offset)
+ .unwrap_or_else(|| panic!("index.html contains an unclosed script opener"));
+ let body_start = open_end + 1;
+ let close = find_ascii_tag(index, CLOSE, body_start)
+ .unwrap_or_else(|| panic!("index.html contains an unclosed script body"));
+ let close_end = index[close + CLOSE.len()..]
+ .iter()
+ .position(|byte| *byte == b'>')
+ .map(|offset| close + CLOSE.len() + offset)
+ .unwrap_or_else(|| panic!("index.html contains an unclosed script closer"));
+ if !index[close + CLOSE.len()..close_end]
+ .iter()
+ .all(|byte| byte.is_ascii_whitespace())
+ {
+ panic!("index.html contains a malformed closing script tag");
+ }
+
+ hashes.insert(STANDARD.encode(Sha256::digest(&index[body_start..close])));
+ if hashes.len() > MAX_INLINE_SCRIPT_HASHES {
+ panic!(
+ "index.html contains more than {MAX_INLINE_SCRIPT_HASHES} distinct script bodies"
+ );
+ }
+ cursor = close_end + 1;
+ }
+ }
+ }
+ hashes
+}
+
+fn find_ascii_tag(input: &[u8], tag: &[u8], start: usize) -> Option {
+ if tag.len() > input.len() {
+ return None;
+ }
+ (start..=input.len() - tag.len()).find(|position| {
+ input[*position..*position + tag.len()].eq_ignore_ascii_case(tag)
+ && input
+ .get(*position + tag.len())
+ .is_none_or(|byte| byte.is_ascii_whitespace() || matches!(*byte, b'>' | b'/'))
+ })
+}
+
+fn collect_files(root: &Path, directory: &Path, files: &mut Vec) {
+ let mut entries = fs::read_dir(directory)
+ .unwrap_or_else(|error| panic!("could not read {}: {error}", directory.display()))
+ .collect::, _>>()
+ .unwrap_or_else(|error| panic!("could not enumerate {}: {error}", directory.display()));
+ entries.sort_by_key(|entry| entry.file_name());
+
+ for entry in entries {
+ let file_type = entry.file_type().unwrap_or_else(|error| {
+ panic!("could not inspect {}: {error}", entry.path().display())
+ });
+ if file_type.is_symlink() {
+ panic!(
+ "web assets cannot contain symbolic links: {}",
+ entry.path().display()
+ );
+ }
+ if file_type.is_dir() {
+ collect_files(root, &entry.path(), files);
+ } else if file_type.is_file() {
+ let entry_path = entry.path();
+ let relative_path = entry_path
+ .strip_prefix(root)
+ .expect("asset remains below its root");
+ validate_asset_path(relative_path);
+ files.push(
+ relative_path
+ .to_str()
+ .unwrap_or_else(|| {
+ panic!("web asset path is not UTF-8: {}", entry_path.display())
+ })
+ .replace('\\', "/"),
+ );
+ }
+ }
+}
+
+fn validate_asset_path(path: &Path) {
+ for component in path.components() {
+ if !matches!(component, Component::Normal(_)) {
+ panic!(
+ "web asset path contains an unsafe component: {}",
+ path.display()
+ );
+ }
+ let component = component
+ .as_os_str()
+ .to_str()
+ .unwrap_or_else(|| panic!("web asset path is not UTF-8: {}", path.display()));
+ if component.starts_with('.')
+ || component.contains('\\')
+ || component.contains('%')
+ || component.chars().any(char::is_control)
+ {
+ panic!(
+ "web asset path contains a hidden or unsafe component: {}",
+ path.display()
+ );
+ }
+ }
+
+ let extension = path.extension().and_then(|extension| extension.to_str());
+ let allowed = matches!(
+ extension,
+ Some(
+ "html"
+ | "css"
+ | "js"
+ | "mjs"
+ | "json"
+ | "svg"
+ | "png"
+ | "jpg"
+ | "jpeg"
+ | "gif"
+ | "webp"
+ | "avif"
+ | "ico"
+ | "woff"
+ | "woff2"
+ | "ttf"
+ | "otf"
+ | "txt"
+ | "xml"
+ | "webmanifest"
+ | "wasm"
+ )
+ );
+ if !allowed {
+ panic!(
+ "web asset has an unsupported extension (source maps and secret/config files are not embedded): {}",
+ path.display()
+ );
+ }
+}
diff --git a/bun.lock b/bun.lock
index 45eda1c58..a462b61a6 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,19 +5,17 @@
"": {
"name": "executor-workspace",
"devDependencies": {
- "@changesets/changelog-github": "^0.7.0",
- "@changesets/cli": "^2.30.0",
"@effect/language-service": "^0.85.1",
"@effect/tsgo": "^0.5.2",
"@effect/vitest": "catalog:",
"@typescript/native-preview": "^7.0.0-dev.20260410.1",
- "@vitest/expect": "catalog:",
- "@vitest/mocker": "catalog:",
- "@vitest/pretty-format": "catalog:",
- "@vitest/runner": "catalog:",
- "@vitest/snapshot": "catalog:",
- "@vitest/spy": "catalog:",
- "@vitest/utils": "catalog:",
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"atmn": "^1.1.8",
"effect": "catalog:",
"knip": "^6.3.0",
@@ -25,10 +23,118 @@
"oxlint": "^1.56.0",
"turbo": "^2.5.6",
"typescript": "^5.9.3",
+ "vitest": "4.1.9",
+ },
+ },
+ "apps/marketing": {
+ "name": "@executor-js/marketing",
+ "version": "0.0.5",
+ "dependencies": {
+ "@astrojs/cloudflare": "^13.0.0",
+ "@astrojs/react": "^5.0.4",
+ "@executor-js/react": "workspace:*",
+ "@tailwindcss/vite": "^4.2.2",
+ "astro": "^6.1.3",
+ "clsx": "^2.1.1",
+ "motion": "^12.38.0",
+ "posthog-js": "^1.372.5",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "react-tweet": "^3.3.0",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss": "^4.2.2",
+ },
+ "devDependencies": {
+ "@rhyssul/portless": "^0.13.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "wrangler": "^4.0.0",
+ },
+ },
+ "e2e": {
+ "name": "@executor-js/e2e",
+ "version": "0.0.17",
+ "dependencies": {
+ "@executor-js/api": "workspace:*",
+ "@executor-js/emulate": "^0.7.5",
+ "@executor-js/mcporter": "^0.11.4",
+ "@executor-js/plugin-graphql": "workspace:*",
+ "@executor-js/plugin-mcp": "workspace:*",
+ "@executor-js/plugin-microsoft": "workspace:*",
+ "@executor-js/plugin-openapi": "workspace:*",
+ "@executor-js/plugin-toolkits": "workspace:*",
+ "@executor-js/sdk": "workspace:*",
+ "@kitlangton/terminal-control": "^0.3.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
+ "asciinema-player": "^3.15.1",
+ "effect": "catalog:",
+ "monaco-editor": "^0.55.1",
+ "playwright": "^1.60.0",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ },
+ "devDependencies": {
+ "@effect/vitest": "catalog:",
+ "@executor-js/motel": "0.2.5-executor.1",
+ "@types/node": "^25.9.2",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "graphql": "^16.12.0",
+ "iron-webcrypto": "^2.0.0",
+ "typescript": "catalog:",
+ "vite": "catalog:",
"vitest": "catalog:",
},
},
- "apps/cli": {
+ "examples/all-plugins": {
+ "name": "@executor-js/example-all-plugins",
+ "version": "0.0.38",
+ "dependencies": {
+ "@executor-js/plugin-file-secrets": "workspace:*",
+ "@executor-js/plugin-google": "workspace:*",
+ "@executor-js/plugin-graphql": "workspace:*",
+ "@executor-js/plugin-keychain": "workspace:*",
+ "@executor-js/plugin-mcp": "workspace:*",
+ "@executor-js/plugin-onepassword": "workspace:*",
+ "@executor-js/plugin-openapi": "workspace:*",
+ "@executor-js/plugin-workos-vault": "workspace:*",
+ "@executor-js/sdk": "workspace:*",
+ "effect": "catalog:",
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "typescript": "latest",
+ },
+ },
+ "examples/docs-sdk-quickstart": {
+ "name": "@executor-js/example-docs-sdk-quickstart",
+ "version": "0.0.23",
+ "dependencies": {
+ "@executor-js/plugin-openapi": "workspace:*",
+ "@executor-js/sdk": "workspace:*",
+ "effect": "catalog:",
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "typescript": "catalog:",
+ },
+ },
+ "examples/promise-sdk": {
+ "name": "@executor-js/example-promise-sdk",
+ "version": "0.0.2",
+ "dependencies": {
+ "@executor-js/plugin-graphql": "workspace:*",
+ "@executor-js/plugin-mcp": "workspace:*",
+ "@executor-js/plugin-openapi": "workspace:*",
+ "@executor-js/sdk": "workspace:*",
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "typescript": "latest",
+ },
+ },
+ "legacy/cli": {
"name": "executor",
"version": "1.5.20",
"bin": {
@@ -53,7 +159,7 @@
"vitest": "catalog:",
},
},
- "apps/cloud": {
+ "legacy/cloud": {
"name": "@executor-js/cloud",
"version": "1.4.38",
"dependencies": {
@@ -125,7 +231,7 @@
"wrangler": "^4.81.0",
},
},
- "apps/desktop": {
+ "legacy/desktop": {
"name": "@executor-js/desktop",
"version": "1.5.20",
"dependencies": {
@@ -165,7 +271,7 @@
"vitest": "catalog:",
},
},
- "apps/host-cloudflare": {
+ "legacy/host-cloudflare": {
"name": "@executor-js/host-cloudflare",
"dependencies": {
"@effect/atom-react": "catalog:",
@@ -212,7 +318,7 @@
"wrangler": "^4.95.0",
},
},
- "apps/host-selfhost": {
+ "legacy/host-selfhost": {
"name": "@executor-js/host-selfhost",
"version": "0.0.19",
"dependencies": {
@@ -260,7 +366,7 @@
"vitest": "catalog:",
},
},
- "apps/local": {
+ "legacy/local": {
"name": "@executor-js/local",
"version": "1.4.4",
"dependencies": {
@@ -314,114 +420,6 @@
"vitest": "catalog:",
},
},
- "apps/marketing": {
- "name": "@executor-js/marketing",
- "version": "0.0.5",
- "dependencies": {
- "@astrojs/cloudflare": "^13.0.0",
- "@astrojs/react": "^5.0.4",
- "@executor-js/react": "workspace:*",
- "@tailwindcss/vite": "^4.2.2",
- "astro": "^6.1.3",
- "clsx": "^2.1.1",
- "motion": "^12.38.0",
- "posthog-js": "^1.372.5",
- "react": "^19.2.5",
- "react-dom": "^19.2.5",
- "react-tweet": "^3.3.0",
- "tailwind-merge": "^3.5.0",
- "tailwindcss": "^4.2.2",
- },
- "devDependencies": {
- "@rhyssul/portless": "^0.13.0",
- "@types/react": "^19.2.14",
- "@types/react-dom": "^19.2.3",
- "wrangler": "^4.0.0",
- },
- },
- "e2e": {
- "name": "@executor-js/e2e",
- "version": "0.0.17",
- "dependencies": {
- "@executor-js/api": "workspace:*",
- "@executor-js/emulate": "^0.7.5",
- "@executor-js/mcporter": "^0.11.4",
- "@executor-js/plugin-graphql": "workspace:*",
- "@executor-js/plugin-mcp": "workspace:*",
- "@executor-js/plugin-microsoft": "workspace:*",
- "@executor-js/plugin-openapi": "workspace:*",
- "@executor-js/plugin-toolkits": "workspace:*",
- "@executor-js/sdk": "workspace:*",
- "@kitlangton/terminal-control": "^0.3.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
- "asciinema-player": "^3.15.1",
- "effect": "catalog:",
- "monaco-editor": "^0.55.1",
- "playwright": "^1.60.0",
- "react": "catalog:",
- "react-dom": "catalog:",
- },
- "devDependencies": {
- "@effect/vitest": "catalog:",
- "@executor-js/motel": "0.2.5-executor.1",
- "@types/node": "^25.9.2",
- "@types/react": "catalog:",
- "@types/react-dom": "catalog:",
- "@vitejs/plugin-react": "catalog:",
- "graphql": "^16.12.0",
- "iron-webcrypto": "^2.0.0",
- "typescript": "catalog:",
- "vite": "catalog:",
- "vitest": "catalog:",
- },
- },
- "examples/all-plugins": {
- "name": "@executor-js/example-all-plugins",
- "version": "0.0.38",
- "dependencies": {
- "@executor-js/plugin-file-secrets": "workspace:*",
- "@executor-js/plugin-google": "workspace:*",
- "@executor-js/plugin-graphql": "workspace:*",
- "@executor-js/plugin-keychain": "workspace:*",
- "@executor-js/plugin-mcp": "workspace:*",
- "@executor-js/plugin-onepassword": "workspace:*",
- "@executor-js/plugin-openapi": "workspace:*",
- "@executor-js/plugin-workos-vault": "workspace:*",
- "@executor-js/sdk": "workspace:*",
- "effect": "catalog:",
- },
- "devDependencies": {
- "@types/node": "catalog:",
- "typescript": "latest",
- },
- },
- "examples/docs-sdk-quickstart": {
- "name": "@executor-js/example-docs-sdk-quickstart",
- "version": "0.0.23",
- "dependencies": {
- "@executor-js/plugin-openapi": "workspace:*",
- "@executor-js/sdk": "workspace:*",
- "effect": "catalog:",
- },
- "devDependencies": {
- "@types/node": "catalog:",
- "typescript": "catalog:",
- },
- },
- "examples/promise-sdk": {
- "name": "@executor-js/example-promise-sdk",
- "version": "0.0.2",
- "dependencies": {
- "@executor-js/plugin-graphql": "workspace:*",
- "@executor-js/plugin-mcp": "workspace:*",
- "@executor-js/plugin-openapi": "workspace:*",
- "@executor-js/sdk": "workspace:*",
- },
- "devDependencies": {
- "@types/node": "catalog:",
- "typescript": "latest",
- },
- },
"packages/app": {
"name": "@executor-js/app",
"version": "1.4.4",
@@ -1210,6 +1208,27 @@
"vite": "catalog:",
},
},
+ "web": {
+ "name": "@executor-js/web",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@effect/vitest": "4.0.0-beta.59",
+ "@sveltejs/adapter-static": "^3.0.10",
+ "@sveltejs/kit": "^2.67.0",
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
+ "@testing-library/svelte": "^5.4.2",
+ "effect": "4.0.0-beta.59",
+ "jsdom": "^29.1.1",
+ "oxlint": "^1.71.0",
+ "prettier": "^3.8.4",
+ "prettier-plugin-svelte": "^4.1.1",
+ "svelte": "^5.56.4",
+ "svelte-check": "^4.7.0",
+ "typescript": "^6.0.3",
+ "vite": "^8.1.0",
+ "vitest": "^4.1.9",
+ },
+ },
},
"patchedDependencies": {
"libsql@0.5.29": "patches/libsql@0.5.29.patch",
@@ -1288,6 +1307,14 @@
"@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="],
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
+
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
+
+ "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
+
+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
"@astrojs/cloudflare": ["@astrojs/cloudflare@13.1.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/underscore-redirects": "1.0.3", "@cloudflare/vite-plugin": "^1.25.6", "piccolore": "^0.1.3", "tinyglobby": "^0.2.15", "vite": "^7.3.1" }, "peerDependencies": { "astro": "^6.0.0", "wrangler": "^4.61.1" } }, "sha512-Ogl8p4MifPMHbpHKJL78eqEL+I3wbHrYmo83P/FkKZQ9VzzKi2A1RjnbaiiPAwrA8xQOqIUo4zlN7+Mse0aJAQ=="],
"@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="],
@@ -1416,48 +1443,12 @@
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
+ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
+
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
"@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
- "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.0", "", { "dependencies": { "@changesets/config": "^3.1.3", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ=="],
-
- "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="],
-
- "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="],
-
- "@changesets/changelog-github": ["@changesets/changelog-github@0.7.0", "", { "dependencies": { "@changesets/get-github-info": "^0.8.0", "@changesets/types": "^6.1.0", "dotenv": "^8.1.0" } }, "sha512-rBsbRvc4TVn+FvFnOVM3LxlFJfTXXCp8gfVJ+0BubxWNSVnLuAzowi5j+IEraLLP52w8AAs9QfKbPS3MMiXQJA=="],
-
- "@changesets/cli": ["@changesets/cli@2.30.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.0", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.3", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.15", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA=="],
-
- "@changesets/config": ["@changesets/config@3.1.3", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw=="],
-
- "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="],
-
- "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="],
-
- "@changesets/get-github-info": ["@changesets/get-github-info@0.8.0", "", { "dependencies": { "dataloader": "^1.4.0", "node-fetch": "^2.5.0" } }, "sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ=="],
-
- "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.15", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.3", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g=="],
-
- "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="],
-
- "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="],
-
- "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="],
-
- "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="],
-
- "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="],
-
- "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="],
-
- "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="],
-
- "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="],
-
- "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="],
-
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="],
"@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="],
@@ -1494,6 +1485,18 @@
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
+ "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@3.2.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.8", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
+
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.5", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
+
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
@@ -1562,11 +1565,11 @@
"@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="],
- "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
+ "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
- "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
+ "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
- "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="],
@@ -1666,7 +1669,7 @@
"@executor-js/cli": ["@executor-js/cli@workspace:packages/core/cli"],
- "@executor-js/cloud": ["@executor-js/cloud@workspace:apps/cloud"],
+ "@executor-js/cloud": ["@executor-js/cloud@workspace:legacy/cloud"],
"@executor-js/cloudflare": ["@executor-js/cloudflare@workspace:packages/hosts/cloudflare"],
@@ -1674,7 +1677,7 @@
"@executor-js/config": ["@executor-js/config@workspace:packages/core/config"],
- "@executor-js/desktop": ["@executor-js/desktop@workspace:apps/desktop"],
+ "@executor-js/desktop": ["@executor-js/desktop@workspace:legacy/desktop"],
"@executor-js/e2e": ["@executor-js/e2e@workspace:e2e"],
@@ -1690,17 +1693,17 @@
"@executor-js/fumadb": ["@executor-js/fumadb@workspace:packages/core/fumadb"],
- "@executor-js/host-cloudflare": ["@executor-js/host-cloudflare@workspace:apps/host-cloudflare"],
+ "@executor-js/host-cloudflare": ["@executor-js/host-cloudflare@workspace:legacy/host-cloudflare"],
"@executor-js/host-mcp": ["@executor-js/host-mcp@workspace:packages/hosts/mcp"],
- "@executor-js/host-selfhost": ["@executor-js/host-selfhost@workspace:apps/host-selfhost"],
+ "@executor-js/host-selfhost": ["@executor-js/host-selfhost@workspace:legacy/host-selfhost"],
"@executor-js/integrations-registry": ["@executor-js/integrations-registry@workspace:packages/core/integrations-registry"],
"@executor-js/ir": ["@executor-js/ir@workspace:packages/kernel/ir"],
- "@executor-js/local": ["@executor-js/local@workspace:apps/local"],
+ "@executor-js/local": ["@executor-js/local@workspace:legacy/local"],
"@executor-js/marketing": ["@executor-js/marketing@workspace:apps/marketing"],
@@ -1748,6 +1751,10 @@
"@executor-js/vite-plugin": ["@executor-js/vite-plugin@workspace:packages/core/vite-plugin"],
+ "@executor-js/web": ["@executor-js/web@workspace:web"],
+
+ "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
+
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
"@fastify/otel": ["@fastify/otel@0.18.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="],
@@ -2018,10 +2025,6 @@
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
- "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="],
-
- "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="],
-
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
@@ -2388,6 +2391,8 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
+
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
@@ -2636,35 +2641,35 @@
"@rhyssul/portless": ["@rhyssul/portless@0.13.3", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-oPvwXJIIkRg2i1CUEUsz8sAdFSf0Fzzv+NmaacAsi6ZZTHxDOofbO6fGInVnbESIFOu4k8a/rXOZRF0aqLAUgw=="],
- "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.2", "", { "os": "android", "cpu": "arm64" }, "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA=="],
- "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="],
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw=="],
- "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="],
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw=="],
- "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="],
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA=="],
- "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="],
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw=="],
- "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="],
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw=="],
- "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="],
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ=="],
- "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="],
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ=="],
- "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="],
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg=="],
- "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="],
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw=="],
- "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="],
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw=="],
- "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="],
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.2", "", { "os": "none", "cpu": "arm64" }, "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw=="],
- "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="],
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.2", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg=="],
- "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="],
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg=="],
- "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="],
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA=="],
"@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.3", "", { "dependencies": { "picomatch": "^4.0.4" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw=="],
@@ -2792,6 +2797,16 @@
"@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="],
+ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="],
+
+ "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
+
+ "@sveltejs/kit": ["@sveltejs/kit@2.67.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JXHbsDwRes1Wgyof3q5ApzzpbCWvinKXMQCiV67TFO6xlZPYLoK0fq3xQMqSicDMgCtFGqLkrQXkseOcASdZ8A=="],
+
+ "@sveltejs/load-config": ["@sveltejs/load-config@0.2.0", "", {}, "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg=="],
+
+ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.1.2", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA=="],
+
"@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
@@ -2870,6 +2885,12 @@
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/svelte": ["@testing-library/svelte@5.4.2", "", { "dependencies": { "@testing-library/dom": "9.x.x || 10.x.x", "@testing-library/svelte-core": "1.1.3" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vite": "*", "vitest": "*" }, "optionalPeers": ["vite", "vitest"] }, "sha512-4o31E4HGo5BU5KwPkulNRocEden+7Tt9JYm9uhln5ajF7DULeyFA46BBWVfKJ8Ms9B3JmOFPTIiVamH7n3KpuQ=="],
+
+ "@testing-library/svelte-core": ["@testing-library/svelte-core@1.1.3", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g=="],
+
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@turbo/darwin-64": ["@turbo/darwin-64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg=="],
@@ -2886,6 +2907,8 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -2902,6 +2925,8 @@
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
+
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -3066,19 +3091,19 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
- "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="],
+ "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="],
- "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="],
+ "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="],
- "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="],
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="],
- "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="],
+ "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="],
- "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="],
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="],
- "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="],
+ "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
- "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="],
+ "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="],
"@webgpu/types": ["@webgpu/types@0.1.70", "", {}, "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA=="],
@@ -3126,11 +3151,9 @@
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
- "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
-
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
- "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -3162,8 +3185,6 @@
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
- "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
-
"asciinema-player": ["asciinema-player@3.15.1", "", { "dependencies": { "@babel/runtime": "^7.21.0", "solid-js": "^1.3.0", "solid-transition-group": "^0.2.3" } }, "sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw=="],
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
@@ -3220,10 +3241,10 @@
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
- "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
-
"better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="],
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
@@ -3520,7 +3541,7 @@
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
- "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="],
+ "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
@@ -3532,6 +3553,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
@@ -3544,6 +3567,8 @@
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
@@ -3572,8 +3597,6 @@
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
- "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
-
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
@@ -3588,14 +3611,14 @@
"dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="],
- "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
-
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="],
"dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="],
+ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -3608,7 +3631,7 @@
"dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
- "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="],
+ "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
@@ -3670,9 +3693,7 @@
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
- "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
-
- "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+ "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
@@ -3714,7 +3735,9 @@
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
- "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
+ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+ "esrap": ["esrap@2.2.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-On0QbLyaiAkVC4eXtgnXK9Kh2opit+3rcUSOc45DqJ2s/X2eXAHsGOKRSJ6IDagQEW5vPyivANfXUiqgXC67Rw=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
@@ -3746,7 +3769,7 @@
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
- "executor": ["executor@workspace:apps/cli"],
+ "executor": ["executor@workspace:legacy/cli"],
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
@@ -3766,8 +3789,6 @@
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
- "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="],
-
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
@@ -3820,8 +3841,6 @@
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
- "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
-
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
@@ -3856,7 +3875,7 @@
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
- "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
+ "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
@@ -3898,8 +3917,6 @@
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
- "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
-
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
@@ -3962,6 +3979,8 @@
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
+ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
+
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
@@ -3980,8 +3999,6 @@
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
- "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="],
-
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
"iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="],
@@ -4094,11 +4111,13 @@
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
- "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
+ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
- "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="],
+ "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
@@ -4106,8 +4125,6 @@
"is-wayland": ["is-wayland@0.1.0", "", {}, "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA=="],
- "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="],
-
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
@@ -4144,6 +4161,8 @@
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+ "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="],
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -4182,7 +4201,7 @@
"kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
- "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
+ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"knip": ["knip@6.4.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.121.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw=="],
@@ -4238,7 +4257,7 @@
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
- "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
+ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
@@ -4268,8 +4287,6 @@
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
- "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="],
-
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
@@ -4288,6 +4305,8 @@
"lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="],
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
"macos-version": ["macos-version@6.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -4512,7 +4531,7 @@
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
- "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
+ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
@@ -4572,8 +4591,6 @@
"ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="],
- "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="],
-
"oxc-parser": ["oxc-parser@0.121.0", "", { "dependencies": { "@oxc-project/types": "^0.121.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.121.0", "@oxc-parser/binding-android-arm64": "0.121.0", "@oxc-parser/binding-darwin-arm64": "0.121.0", "@oxc-parser/binding-darwin-x64": "0.121.0", "@oxc-parser/binding-freebsd-x64": "0.121.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.121.0", "@oxc-parser/binding-linux-arm64-gnu": "0.121.0", "@oxc-parser/binding-linux-arm64-musl": "0.121.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.121.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.121.0", "@oxc-parser/binding-linux-riscv64-musl": "0.121.0", "@oxc-parser/binding-linux-s390x-gnu": "0.121.0", "@oxc-parser/binding-linux-x64-gnu": "0.121.0", "@oxc-parser/binding-linux-x64-musl": "0.121.0", "@oxc-parser/binding-openharmony-arm64": "0.121.0", "@oxc-parser/binding-wasm32-wasi": "0.121.0", "@oxc-parser/binding-win32-arm64-msvc": "0.121.0", "@oxc-parser/binding-win32-ia32-msvc": "0.121.0", "@oxc-parser/binding-win32-x64-msvc": "0.121.0" } }, "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg=="],
"oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="],
@@ -4584,23 +4601,15 @@
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
- "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="],
-
"p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="],
- "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
-
- "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="],
-
"p-queue": ["p-queue@9.1.2", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw=="],
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
- "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
-
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
- "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="],
+ "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
@@ -4620,7 +4629,7 @@
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
- "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+ "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
@@ -4636,8 +4645,6 @@
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
- "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
-
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -4680,8 +4687,6 @@
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
- "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
-
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
@@ -4708,7 +4713,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
- "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
+ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
@@ -4732,7 +4737,11 @@
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
- "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+ "prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="],
+
+ "prettier-plugin-svelte": ["prettier-plugin-svelte@4.1.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^5.0.0" } }, "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA=="],
+
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
@@ -4770,8 +4779,6 @@
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
- "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
-
"query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="],
"query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="],
@@ -4872,8 +4879,6 @@
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
- "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="],
-
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
@@ -4988,7 +4993,7 @@
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
- "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
+ "rolldown": ["rolldown@1.1.2", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.2", "@rolldown/binding-darwin-arm64": "1.1.2", "@rolldown/binding-darwin-x64": "1.1.2", "@rolldown/binding-freebsd-x64": "1.1.2", "@rolldown/binding-linux-arm-gnueabihf": "1.1.2", "@rolldown/binding-linux-arm64-gnu": "1.1.2", "@rolldown/binding-linux-arm64-musl": "1.1.2", "@rolldown/binding-linux-ppc64-gnu": "1.1.2", "@rolldown/binding-linux-s390x-gnu": "1.1.2", "@rolldown/binding-linux-x64-gnu": "1.1.2", "@rolldown/binding-linux-x64-musl": "1.1.2", "@rolldown/binding-openharmony-arm64": "1.1.2", "@rolldown/binding-wasm32-wasi": "1.1.2", "@rolldown/binding-win32-arm64-msvc": "1.1.2", "@rolldown/binding-win32-x64-msvc": "1.1.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ=="],
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
@@ -5018,6 +5023,8 @@
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
+ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
@@ -5026,6 +5033,8 @@
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
@@ -5078,7 +5087,7 @@
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
- "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
@@ -5088,9 +5097,9 @@
"simple-xml-to-json": ["simple-xml-to-json@1.2.7", "", {}, "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q=="],
- "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
+ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
- "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
@@ -5112,8 +5121,6 @@
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
- "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="],
-
"split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="],
"split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="],
@@ -5154,12 +5161,10 @@
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
- "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
-
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
@@ -5186,10 +5191,16 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+ "svelte": ["svelte@5.56.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw=="],
+
+ "svelte-check": ["svelte-check@4.7.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.0", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-XChCqdDg29UWOWai2I1s2Ss003piZ9mae2y2KXwoOX01W75mg7/fwLLnx9Yruk9asHkxHPDdSfEfsJh5tr9JvQ=="],
+
"svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="],
"swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="],
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
@@ -5212,8 +5223,6 @@
"temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="],
- "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
-
"terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
@@ -5240,7 +5249,7 @@
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
- "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
@@ -5268,7 +5277,11 @@
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
- "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
+
+ "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
+
+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
@@ -5350,7 +5363,7 @@
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
- "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
+ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -5400,11 +5413,11 @@
"virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="],
- "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="],
+ "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
- "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="],
+ "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
@@ -5418,6 +5431,8 @@
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
@@ -5428,15 +5443,15 @@
"web-vitals": ["web-vitals@5.2.0", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="],
- "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
- "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+ "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
- "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+ "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
@@ -5466,6 +5481,8 @@
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -5474,6 +5491,8 @@
"xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="],
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
@@ -5506,6 +5525,8 @@
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
+ "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
+
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
@@ -5514,7 +5535,7 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
- "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
+ "@astrojs/cloudflare/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"@astrojs/cloudflare/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
@@ -5536,10 +5557,6 @@
"@better-auth/core/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
- "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
-
- "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
-
"@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="],
"@cloudflare/vite-plugin/miniflare": ["miniflare@4.20260415.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260415.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng=="],
@@ -5576,8 +5593,6 @@
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
- "@electron/osx-sign/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="],
"@electron/rebuild/node-abi": ["node-abi@4.31.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw=="],
@@ -5626,6 +5641,10 @@
"@executor-js/motel/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
+ "@executor-js/web/oxlint": ["oxlint@1.71.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.71.0", "@oxlint/binding-android-arm64": "1.71.0", "@oxlint/binding-darwin-arm64": "1.71.0", "@oxlint/binding-darwin-x64": "1.71.0", "@oxlint/binding-freebsd-x64": "1.71.0", "@oxlint/binding-linux-arm-gnueabihf": "1.71.0", "@oxlint/binding-linux-arm-musleabihf": "1.71.0", "@oxlint/binding-linux-arm64-gnu": "1.71.0", "@oxlint/binding-linux-arm64-musl": "1.71.0", "@oxlint/binding-linux-ppc64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-musl": "1.71.0", "@oxlint/binding-linux-s390x-gnu": "1.71.0", "@oxlint/binding-linux-x64-gnu": "1.71.0", "@oxlint/binding-linux-x64-musl": "1.71.0", "@oxlint/binding-openharmony-arm64": "1.71.0", "@oxlint/binding-win32-arm64-msvc": "1.71.0", "@oxlint/binding-win32-ia32-msvc": "1.71.0", "@oxlint/binding-win32-x64-msvc": "1.71.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw=="],
+
+ "@executor-js/web/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
+
"@fastify/otel/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
@@ -5636,6 +5655,10 @@
"@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="],
+ "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
+
+ "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -5704,14 +5727,6 @@
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
- "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="],
-
- "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
-
- "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="],
-
- "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
-
"@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
@@ -5804,8 +5819,6 @@
"@opentui/core/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
- "@opentui/core/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
"@opentui/react/react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
@@ -5816,8 +5829,6 @@
"@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
- "@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
-
"@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
@@ -5920,6 +5931,8 @@
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
+ "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
+
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@sentry/cloudflare/@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="],
@@ -5936,6 +5949,10 @@
"@sentry/react/@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="],
+ "@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
+
+ "@sveltejs/kit/devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
+
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
@@ -5956,6 +5973,8 @@
"@tanstack/router-generator/@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="],
+ "@tanstack/router-generator/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+
"@tanstack/router-generator/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@tanstack/router-plugin/@tanstack/router-generator": ["@tanstack/router-generator@1.166.32", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "magic-string": "^0.30.21", "prettier": "^3.5.0", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g=="],
@@ -5964,14 +5983,20 @@
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="],
"@tanstack/start-plugin-core/@tanstack/router-generator": ["@tanstack/router-generator@1.166.32", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "magic-string": "^0.30.21", "prettier": "^3.5.0", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g=="],
+ "@tanstack/start-plugin-core/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
"@types/better-sqlite3/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/cacheable-request/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
@@ -5988,6 +6013,8 @@
"@types/plist/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
+ "@types/prettier/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+
"@types/responselike/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/tedious/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
@@ -6010,24 +6037,22 @@
"app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
- "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"app-builder-lib/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"astro/@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="],
- "astro/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
+ "astro/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"astro/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
"atmn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
- "atmn/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
-
"atmn/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+ "atmn/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+
"better-auth/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
@@ -6036,14 +6061,18 @@
"builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
- "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"bun-types/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
+ "cheerio/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
"cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
+ "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
@@ -6072,49 +6101,49 @@
"dir-compare/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
- "dmg-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"dmg-license/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
+ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
- "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"electron-publish/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
- "electron-publish/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
"electron-store/conf": ["conf@14.0.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.4.0" } }, "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw=="],
- "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"electron-vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"electron-vite/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
+ "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
+
"encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"execa/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
+ "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
+ "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
- "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+ "fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
- "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+ "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
@@ -6122,6 +6151,10 @@
"h3-v2/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="],
+ "hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
@@ -6138,14 +6171,20 @@
"ink/es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
- "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
"ink/type-fest": ["type-fest@5.5.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="],
"ink/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
"ink-confirm-input/ink-text-input": ["ink-text-input@3.3.0", "", { "dependencies": { "chalk": "^3.0.0", "prop-types": "^15.5.10" }, "peerDependencies": { "ink": "^2.0.0", "react": "^16.5.2" } }, "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg=="],
+ "jsdom/lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
+
+ "jsdom/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
+
+ "json-schema-to-typescript/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+
+ "json-schema-to-typescript/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"knip/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
@@ -6172,6 +6211,8 @@
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
+ "node-gyp/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"node-gyp/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="],
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
@@ -6182,17 +6223,17 @@
"ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
- "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
-
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
- "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+ "parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
- "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+ "postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"posthog-js/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="],
@@ -6204,9 +6245,15 @@
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
- "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+ "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
- "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
+
+ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"protobufjs/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
@@ -6222,21 +6269,17 @@
"react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
- "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
-
"readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"recharts/es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
- "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
- "rolldown/@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="],
+ "rolldown/@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="],
- "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
+ "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
@@ -6260,16 +6303,26 @@
"string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+ "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
"subsume/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
+ "sucrase/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
+ "svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
+
+ "svelte/devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
+
+ "svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
- "temp-file/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
-
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"to-regex-range/is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
@@ -6278,6 +6331,8 @@
"tsup/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
+ "tsup/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"typeorm/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"typeorm/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
@@ -6300,14 +6355,22 @@
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+ "@astrojs/cloudflare/vite/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
+
"@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
+ "@astrojs/react/vite/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
+
+ "@astrojs/react/vite/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@cloudflare/vite-plugin/miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="],
@@ -6386,26 +6449,16 @@
"@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
- "@electron/fuses/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+ "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
- "@electron/notarize/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
- "@electron/osx-sign/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "@electron/osx-sign/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
- "@electron/universal/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
"@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
- "@electron/windows-sign/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
@@ -6498,6 +6551,44 @@
"@executor-js/motel/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
+ "@executor-js/web/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.71.0", "", { "os": "android", "cpu": "arm" }, "sha512-ImGmd1njEg4FEJH03jhRnveEegtO3czCtfptvaHivKAZQIYATbVFBrrzbaYMYv0oJioTnxZAZVSyV+oL7W8S2g=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.71.0", "", { "os": "android", "cpu": "arm64" }, "sha512-4A5BEexBrwY1YFF8Kiq/lp/wQPRG79G3BWIE1FuWaM5MvmpYSd+7ZySVcKkHdwo0UDzdQGddp6pD9mpctMqLnw=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.71.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9wJA9GJulLwS2usU3CEisI/ESDO1n1z9eyTCvApMDrAkbJ1ve0mORgTMjcWWsKxkzkeZ2N/Gpra5IQE7x8tYgQ=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.71.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-PlLCjS06V0PeJMAJwzjrExw1sYNW9Gch3JtNlcwwZDXGlTYDuwHNN89zYH8LTXFfgkVtsYvs2nv0FqrzyuFDzg=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.71.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Lhil7bWre0ncxbUoDoxfS0JzpTz17BRQKW7iwoAUY8GJ66+WwJEfYPCFJ1P0WgVZR5/O/b3Q2pENlHOjeXLOGQ=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.71.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Oo9/L58PYD3RC0x05d2upAPLllHytTjHQGsnC06P6Ynn7jKkp5mdImQxXdJ3+FnBaKspNpGogzgVsi6g872LiA=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.71.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mSHfyfgJrEbyIR29ejaeS50BdPk+GoNPlC1dckpDiUZbJAIel68sjSMdOt4WY0/gva+ECC7FNITQkxMJU+vSBw=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.71.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n9yY4M2tiy3aij4AqtlnspzpfdpeT5JQfK2/w2d8oyp5W0FRwOb1dIeX99nORNcxGr08iD9bH8N5XFz3I2iy8w=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.71.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.71.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.71.0", "", { "os": "linux", "cpu": "none" }, "sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.71.0", "", { "os": "linux", "cpu": "none" }, "sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.71.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.71.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.71.0", "", { "os": "linux", "cpu": "x64" }, "sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.71.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.71.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bd5kI8spYwTm3BILDtGhi73zoup5dw8MlPQNT8YB3BD5UIsjNe3K9/4ctrzQMX4SZMoK5HgzVLkLJzacEXB7fA=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.71.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-W4HvOHGzVLHcrmFu+bMrJlho+/yrlX5ZNdJZqGe8MEldkQG+RHYhxxad9P4jvWAYFmIqUA5i9DQ8QsJqSU9GIw=="],
+
+ "@executor-js/web/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.71.0", "", { "os": "win32", "cpu": "x64" }, "sha512-D2kyEIPHk/G/wiZLnwTVC/sVst+T/lKldVOjAFpgTIBUAOlry72e5OiapDbDBF4LfJLkN5ypJb/8Eu6yJzkveQ=="],
+
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="],
"@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
@@ -6506,9 +6597,9 @@
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
- "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+ "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@libsql/kysely-libsql/@libsql/client/@libsql/core": ["@libsql/core@0.8.1", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-u6nrj6HZMTPsgJ9EBhLzO2uhqhlHQJQmVHV+0yFLvfGf3oSP8w7TjZCNUgu1G8jHISx6KFi7bmcrdXW9lRt++A=="],
@@ -6520,8 +6611,6 @@
"@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
- "@malept/flatpak-bundler/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
"@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
@@ -6530,8 +6619,6 @@
"@opentui/core/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
- "@opentui/core/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
"@opentui/react/react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
@@ -6564,6 +6651,8 @@
"@react-grab/cli/ora/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+ "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+
"@sentry/electron/@sentry/browser/@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" } }, "sha512-42bxyRTxnCmYlWnvz4CxikuQNanw8UNma2WJrtxJ0f1MAJV2GhQGSHDLnA+lvFlmiz6qct3pfen/NXGyOTegTA=="],
"@sentry/electron/@sentry/browser/@sentry-internal/feedback": ["@sentry-internal/feedback@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" } }, "sha512-0k9XZF0wn86f77mIO2U3gNNyDZooy139CnEanRzHinrN106vVzvBZ6TUEQoHtoO1fqQxr+nWWVrqV/PXUqk47w=="],
@@ -6594,8 +6683,14 @@
"@tanstack/router-generator/@tanstack/router-core/seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="],
+ "@tanstack/router-generator/@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
+ "@tanstack/router-plugin/@tanstack/router-generator/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+
"@tanstack/start-plugin-core/@tanstack/router-generator/@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
+ "@tanstack/start-plugin-core/@tanstack/router-generator/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+
"@types/better-sqlite3/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"@types/cacheable-request/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
@@ -6632,10 +6727,6 @@
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "app-builder-lib/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "app-builder-lib/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"astro/@clack/prompts/@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="],
@@ -6644,18 +6735,20 @@
"astro/@clack/prompts/fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="],
+ "astro/vite/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
+
"builder-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"builder-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
- "builder-util/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "builder-util/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"bun-types/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
+ "cheerio/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -6674,10 +6767,6 @@
"dir-compare/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
- "dmg-builder/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "dmg-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
@@ -6736,22 +6825,10 @@
"electron-builder/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
- "electron-builder/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "electron-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"electron-publish/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"electron-publish/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
- "electron-publish/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "electron-publish/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
- "electron-updater/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
-
- "electron-updater/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
"electron-vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"electron-vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
@@ -6806,12 +6883,22 @@
"electron-vite/vite/esbuild": ["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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
+ "electron-vite/vite/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
+
+ "electron-vite/vite/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
+ "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
+
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
+ "hast-util-from-html/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "hast-util-raw/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
"hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
@@ -6836,6 +6923,10 @@
"ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
+ "parse5-htmlparser2-tree-adapter/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "parse5-parser-stream/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="],
@@ -6848,8 +6939,6 @@
"protobufjs/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
- "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
-
"rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"run-jxa/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
@@ -6860,17 +6949,13 @@
"run-jxa/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
- "run-jxa/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
"run-jxa/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"shiki-stream/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
- "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "temp-file/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
+ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "temp-file/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+ "svelte-check/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -6934,12 +7019,18 @@
"wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "@astrojs/cloudflare/vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "@astrojs/react/vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260415.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg=="],
"@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260415.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ=="],
@@ -7042,7 +7133,7 @@
"@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "@libsql/kysely-libsql/@libsql/client/@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+ "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.3.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rmOqsLcDI65zzxlUOoEiPJLhqmbFsZF6p4UJQ2kMqB+Kc0Rt5/A1OAdOZ/Wo8fQfJWjR1IbkbpEINFioyKf+nQ=="],
@@ -7062,8 +7153,6 @@
"@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
- "@opentui/core/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
"@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
"@react-grab/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
@@ -7072,18 +7161,22 @@
"@react-grab/cli/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
- "@react-grab/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
"agents/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"agents/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"agents/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+ "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
+
"astro/@clack/prompts/fast-string-width/fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="],
+ "astro/vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
"dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ "electron-vite/vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -7094,29 +7187,35 @@
"iconv-corefoundation/cli-truncate/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+ "iconv-corefoundation/cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"ink-confirm-input/ink-text-input/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ink-confirm-input/ink-text-input/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
+ "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="],
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
- "read-yaml-file/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
-
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
+ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "@executor-js/mcporter/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
"@executor-js/motel/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
"@react-grab/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
- "agents/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "@react-grab/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
- "agents/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "iconv-corefoundation/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 000000000..218d01df9
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,65 @@
+name: executor
+
+services:
+ executor:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ image: "${EXECUTOR_IMAGE:-executor:local}"
+ command:
+ - server
+ - --bind
+ - 0.0.0.0:4788
+ - --allow-unsafe-http-non-loopback
+ restart: unless-stopped
+ init: true
+ read_only: true
+ mem_limit: "${EXECUTOR_MEMORY_LIMIT:-2g}"
+ pids_limit: "${EXECUTOR_PIDS_LIMIT:-512}"
+ cap_drop:
+ - ALL
+ security_opt:
+ - no-new-privileges:true
+ ports:
+ - "127.0.0.1:4788:4788"
+ environment:
+ EXECUTOR_DATA_DIR: /var/lib/executor
+ # When changing the published port, also set EXECUTOR_PUBLIC_ORIGIN to
+ # the exact URL used in the browser. Keep plain HTTP on host loopback.
+ EXECUTOR_PUBLIC_ORIGIN: "${EXECUTOR_PUBLIC_ORIGIN:-http://127.0.0.1:4788}"
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE: /etc/executor/mcp-stdio.json
+ # Null means pass through only when the host variable is set.
+ EXECUTOR_TRUSTED_PROXIES:
+ RUST_LOG: "${RUST_LOG:-executor=info}"
+ volumes:
+ - executor-data:/var/lib/executor
+ - ./docker/mcp-stdio-templates.example.json:/etc/executor/mcp-stdio.json:ro
+ # By default, Executor creates master.key with mode 0600 in the data
+ # volume. To supply an external exact 32-byte key instead, make it
+ # readable by container UID 10001, mount it read-only, and add
+ # EXECUTOR_MASTER_KEY_FILE=/run/secrets/executor-master.key above.
+ # - ./secrets/executor-master.key:/run/secrets/executor-master.key:ro
+ # Mount only fully trusted, self-contained Linux stdio MCP executables.
+ # They run as Executor's UID and can read the data volume and master key.
+ # Use the absolute container paths from mcp-stdio.json. The base image
+ # does not include Node or Bun. Writeable state must remain under a
+ # mounted volume or /tmp.
+ # - /absolute/host/mcp:/opt/executor/mcp:ro
+ tmpfs:
+ - /tmp:rw,noexec,nosuid,nodev,size=64m
+ healthcheck:
+ test:
+ - CMD
+ - curl
+ - --fail
+ - --silent
+ - --show-error
+ - http://127.0.0.1:4788/healthz
+ interval: 30s
+ timeout: 5s
+ start_period: 10s
+ retries: 3
+ stop_grace_period: 2m
+
+volumes:
+ executor-data:
diff --git a/docker/mcp-stdio-templates.example.json b/docker/mcp-stdio-templates.example.json
new file mode 100644
index 000000000..da024f706
--- /dev/null
+++ b/docker/mcp-stdio-templates.example.json
@@ -0,0 +1,3 @@
+{
+ "templates": []
+}
diff --git a/docker/mcp-stdio-templates.full.example.json b/docker/mcp-stdio-templates.full.example.json
new file mode 100644
index 000000000..0e1afe76f
--- /dev/null
+++ b/docker/mcp-stdio-templates.full.example.json
@@ -0,0 +1,14 @@
+{
+ "templates": [
+ {
+ "name": "filesystem",
+ "executable": "/opt/executor/mcp/filesystem-server",
+ "cwd": "/var/lib/executor",
+ "arguments": ["--stdio"],
+ "environment": {
+ "LOG_LEVEL": "warn"
+ },
+ "secretEnvironment": ["API_TOKEN"]
+ }
+ ]
+}
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 000000000..619ec72bb
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,384 @@
+# Executor rewrite architecture
+
+## Product boundary
+
+The rewrite targets a single-user, local and self-hosted Executor. Linux and
+macOS native binaries plus Docker are first-class release targets. The current
+cloud and desktop products remain legacy surfaces and do not constrain the new
+architecture.
+
+The production artifact is one Rust binary. It owns the control API, gateway
+API, embedded SQLite state, upstream connections, sandboxed TypeScript runtime,
+approval coordination, and static Svelte application. The production runtime
+does not require Node.js. Packaging builds the `web/` application first and
+embeds its static output at the `src/web_assets.rs` boundary. Cargo checks and
+tests do not depend on generated web assets.
+
+Version one supports these upstream source types:
+
+- MCP servers over local stdio and remote Streamable HTTP
+- OpenAPI-described HTTP APIs
+- GraphQL endpoints
+
+It supports API key, bearer token, basic authentication, and OAuth credential
+flows. Each source has one active credential profile. Integrations and existing
+data migration are intentionally out of scope.
+
+## Control and gateway planes
+
+The two authentication planes are deliberately disjoint:
+
+- Control APIs accept only an administrator session cookie. Unsafe requests
+ also require an origin match and CSRF token.
+- Gateway APIs accept only dashboard-generated API tokens.
+
+API tokens initially share one global enabled-tool set. Interactive approval
+rules are evaluated separately and remain available for sensitive calls. An API
+token can never authorize an administrator route.
+
+First boot prints a one-time, high-entropy setup token in a fragment URL. Only a
+keyed digest is stored. The atomic setup claim creates exactly one administrator
+whose password is hashed with Argon2id. Administrator sessions and API tokens
+are opaque, high-entropy values stored only as keyed digests. API token secrets
+are shown once.
+
+Reverse-proxy client addresses are disabled by default. `--trusted-proxy `
+may be repeated, or `EXECUTOR_TRUSTED_PROXIES` may contain a comma-separated
+list. Executor honors `X-Forwarded-For` only when the direct peer is trusted,
+then walks the chain from right to left through configured trusted proxies.
+Malformed, missing, or all-trusted chains are rejected before password work so
+they cannot create a proxy-wide login-rate-limit bucket. Every intermediary
+proxy must be listed and must append or replace `X-Forwarded-For` correctly.
+
+## Storage and key management
+
+SQLite is opened with foreign keys, WAL mode, a five-second busy timeout, and
+`synchronous=FULL`. FULL is the initial durability choice because this local
+control plane stores credential and approval state, while the expected write
+volume is modest. This can be revisited with benchmarks. Network work must
+never occur inside a database transaction.
+
+Migrations are append-only SQL files embedded into the binary. The instance
+master key comes from `EXECUTOR_MASTER_KEY_FILE`, or is atomically created as a
+0600 file inside a 0700 data directory. An initialized database without its key
+fails closed. A boot sentinel detects a wrong key or modified ciphertext.
+Protected values use XChaCha20-Poly1305. HKDF derives purpose-specific subkeys,
+and associated data binds ciphertext to its purpose and record identity.
+
+## Delivery slices
+
+1. Foundation: one Cargo package and binary, SQLite migrations, master-key
+ lifecycle, setup, administrator sessions, CSRF and origin checks, API token
+ management, a gateway authentication proof, and the Svelte SPA shell.
+2. Sources: MCP stdio and Streamable HTTP, OpenAPI, GraphQL, credential profiles,
+ OAuth callbacks, connectivity testing, and source lifecycle UI.
+3. Tool catalog: normalized discovery, search and describe, global enable state,
+ bulk and filtered enable workflows, and request-aware tool testing.
+4. Runtime and approvals: sandboxed TypeScript execution, concurrent tool calls,
+ interactive approvals, resume semantics, and CLI parity.
+5. Operations: request logs, redaction, retention, embedded web assets, Docker,
+ Linux and macOS packaging, upgrade safety, e2e recordings, and archival of
+ the old TypeScript products under `legacy/` after parity was proven.
+
+The prior TypeScript application entry points now live under `legacy/` as
+manual compatibility surfaces. Shared protocol, runtime, SDK, host, app, and
+React packages remain under `packages/` where the native product and package
+compatibility workflows can retain them independently.
+
+## Source and tool catalog contract
+
+The catalog is global to the single-user instance. Every API token sees the
+same source and tool set. A source has one immutable ID, one collision-safe
+slug, one non-secret configuration object, and at most one encrypted credential
+payload. Source kinds are limited to `openapi`, `graphql`, `mcp_http`, and
+`mcp_stdio`. Credential payloads carry a schema version and are encrypted with
+record-bound associated data, so ciphertext copied between sources cannot be
+decrypted.
+
+Tools retain an immutable ID and upstream stable key separately from their
+display name and callable name. The full callable path is
+`tools..`. Sandboxed TypeScript omits the leading
+`tools.` because that is the proxy root. Local names are normalized and
+collision suffixes are allocated in stable-key order. Existing and tombstoned
+names stay reserved, which keeps paths stable when upstream discovery reorders,
+removes, or restores a tool. The source roots `tools`, `search`, `describe`,
+`sources`, and `executor` are reserved for the sandbox and built-in catalog helpers.
+
+A source may expose at most 100,000 active tools and retain at most 25,000
+tombstoned tool identities, for a hard 125,000-row history ceiling. Refreshes
+that would exceed either ceiling fail atomically with `catalog_too_large`.
+Executor never silently deletes tombstones because they carry stable IDs,
+callable names, and administrator overrides. Intentionally discarding that
+history requires deleting and recreating the source. Administrator tool lists
+apply filters, counts, ordering, and pagination in SQLite so reads remain
+memory-bounded at the history ceiling.
+
+Gateway search uses bounded word, trigram, and encoded short-gram indexes. SQL
+first excludes tombstones and effectively disabled tools, then caps the
+deduplicated candidate set at 4,096 before the reference lexical scorer runs.
+This preserves token-prefix, substring, and reverse-prefix recall without
+hydrating the complete historical table.
+
+Each tool has an intrinsic mode. A source and a tool may each add an override.
+The effective mode is always derived in this order:
+
+1. Tool override
+2. Source override
+3. Tool intrinsic mode
+
+The visible modes are `enabled`, `ask`, and `disabled`. Disabled tools remain
+visible in the administrator catalog, but gateway search and describe omit
+them. A direct invocation lookup returns `tool_disabled`, which prevents a
+caller with an old path from bypassing the catalog. Ask tools remain
+discoverable and carry approval-required metadata.
+
+Protocol discovery and schema normalization happen before a catalog write.
+Initial creation uses a create-only staged snapshot with no meaningless CAS
+fields. Refresh snapshots record the source and credential revisions they used.
+The commit rechecks both revisions, serializes catalog writers, replaces source
+artifacts, upserts tools by `(source_id, stable_key)`, tombstones missing tools,
+and advances the source and global catalog revisions in one transaction. Create
+and refresh share the same transaction-scoped artifact, tool, binding, and
+search-index application helpers. A failed stage or stale revision leaves the
+last known good catalog untouched. Network work never occurs inside this
+transaction.
+
+Tool bindings use a closed Rust enum and a generic persisted wire vocabulary:
+protocol, positive binding version, and private JSON definition. SQLite reserves
+the supported source protocol names, while Rust rejects protocol/version pairs
+that have no implemented typed variant. Generic source creation and gateway
+invocation routes own authentication, limits, logging, and typed dispatch;
+OpenAPI owns only its preview, compiler, credential adapter, and invocation
+adapter.
+
+Invocation admission first reads tool presence, effective mode, the input
+schema, typed-binding identity, and credential revision in one coherent SQLite
+snapshot without decrypting credentials. Arguments are validated before policy
+handling. Enabled calls reacquire a full catalog read lease, decrypt credentials
+only in the parent process, and retain the lease through outbound completion.
+Ask calls persist an encrypted approval and release the preflight lease without
+building an outbound request. Approval execution reacquires a fresh lease and
+revalidates every source, tool, binding, catalog, and credential revision.
+
+Administrator catalog reads require a session cookie. Catalog mutations also
+require the matching Origin, CSRF cookie, and CSRF header. Gateway discovery,
+description, and invocation lookup accept API tokens only. The initial routes
+are:
+
+- `GET /api/v1/sources` and `GET /api/v1/sources/{id}`
+- `DELETE /api/v1/sources/{id}`
+- `PATCH /api/v1/sources/{id}/mode`
+- `GET /api/v1/tools` and `GET /api/v1/tools/{id}`
+- `PATCH /api/v1/tools/{id}/mode` and `PATCH /api/v1/tools/modes`
+- `GET /api/v1/request-logs` and `GET /api/v1/request-logs/{id}`
+- `POST /api/v1/gateway/tools/search`
+- `POST /api/v1/gateway/tools/describe`
+- `POST /api/v1/gateway/tools/lookup`
+- `POST /api/v1/sources/openapi/preview`
+- `POST /api/v1/sources` and `POST /api/v1/sources/{id}/refresh`
+- `GET`, `PUT`, and `DELETE /api/v1/sources/{id}/credentials`
+- `POST /api/v1/gateway/tools/invoke`
+
+Request logs store metadata only: request ID, nullable actor token ID, surface,
+nullable source and tool IDs, an immutable callable-path snapshot, outcome,
+stable error code, duration, nullable approval ID, and timestamp. They never
+store request headers, credentials, arguments, or results. Source and tool
+deletion clears their foreign keys while retaining the history and path
+snapshot.
+
+## OpenAPI import and invocation
+
+OpenAPI 3.0 and 3.1 documents can be previewed from pasted JSON or YAML and
+from an HTTP URL. Swagger 2 documents and external references fail closed.
+Local references have cycle and depth limits. Tool identities are derived from
+the HTTP method and exact path, so operation ID and display-name changes do not
+change tool IDs, callable names, or administrator mode overrides on refresh.
+
+The persisted tool binding contains only typed protocol metadata. Static API keys,
+bearer tokens, basic credentials, manually supplied OAuth access tokens, and
+query-bearing source URLs live only in the source-bound encrypted credential
+envelope. The source configuration and admin response expose a query-stripped
+display URL. Refresh
+performs network and compilation work before the catalog transaction, then
+commits artifacts, tools, tombstones, bindings, and health together under
+source and credential revision checks. A failed refresh leaves the last good
+callable catalog in place.
+
+Outbound requests resolve and validate every DNS answer, pin the validated
+addresses into a proxy-free client, and reject mixed safe and unsafe answers.
+Private and loopback targets require a per-source administrator opt-in.
+Link-local, metadata, reserved, documentation, and multicast targets remain
+blocked even with that opt-in. Spec redirects are manual, bounded, revalidated
+at every hop, and cannot downgrade HTTPS. Tool invocation never follows
+redirects. Request and response headers, bodies, time, DNS answers, redirects,
+and document sizes have hard limits.
+
+The first invocation surface deliberately supports this exact serialization
+subset:
+
+- Path and header parameters use `simple` style.
+- Query parameters use `form`, `spaceDelimited`, `pipeDelimited`, or
+ `deepObject`; `allowReserved` is not accepted.
+- Cookie parameters use `form`, including scalar, array, and object explode
+ behavior.
+- Request bodies support JSON and `+json`, string-valued `text/plain`, and
+ closed scalar-object `application/x-www-form-urlencoded` schemas.
+- Parameter `content`, multipart bodies, arbitrary media types, and other
+ styles fail during import instead of producing tools that cannot execute.
+
+Enabled tools execute immediately, disabled or removed tools cannot reach the
+transport, and Ask tools return HTTP 202 with an opaque approval ID, expiry,
+and owner-only status URL. Request logs remain metadata-only and never contain
+arguments, credentials, upstream bodies, or results.
+
+## Persistent approvals and tool invocation
+
+`ToolCallService` is the protocol-neutral parent-side invocation boundary used
+by the HTTP gateway and designed for the TypeScript runtime, CLI, and MCP host.
+A tool call carries its request, actor token, surface, execution, call, path,
+and argument snapshots. The sandbox can request a path and arguments, but it
+cannot select credentials, bindings, policy, actor identity, or approval state.
+
+Ask arguments, input schemas, internal invocation snapshots, and terminal
+results are encrypted with approval-ID-bound associated data. SQLite stores
+only metadata snapshots and revision columns in plaintext. Arguments and
+results never enter audit metadata or request logs. Administrator detail
+responses decrypt only a conservative schema-shaped redaction. Only the active
+API token that created an approval can poll or cancel it and retrieve its
+terminal result.
+
+The fixed approval TTL is ten minutes. The persisted state machine is:
+
+```text
+pending -> approved -> executing -> succeeded | failed | interrupted
+ -> denied | expired | canceled
+approved -> stale | canceled
+```
+
+Administrator decisions use cookie authentication plus Origin and CSRF checks.
+The decision is a SQLite compare-and-swap and its metadata-only audit event is
+committed in the same transaction. Repeating the same decision is idempotent;
+a conflicting decision or revision loses with a stable conflict. The gateway
+owner may cancel only pending or approved work. Executing work is never labeled
+canceled because bytes may already have reached the upstream service.
+Runtime cancellation and approval claims share an execution gate. If
+cancellation marks the continuation lost first, no later approval can claim
+network work. If the claim commits first, the server owns that already-executing
+side effect through truthful terminal completion.
+
+Every terminal transition also inserts a bounded, metadata-only request-log
+event into a transactional outbox. A single background drainer sends those
+events through the shared bounded log sink and acknowledges an outbox row only
+after SQLite confirms the request-log write. Stable event IDs make a crash
+between persistence and acknowledgement idempotent. Delivery retries never
+delay an approval decision or approved tool execution.
+
+Approval acceptance starts a detached server-owned task. That task reacquires
+the exact invocation revision, atomically verifies the original API token is
+still active, commits `executing`, then performs network work while holding the
+fresh catalog lease. No database transaction spans network activity. A source,
+tool, mode, binding, credential, or relevant catalog change makes the approval
+stale without dispatch. API token revocation and cancellation of its pending or
+approved work happen in one immediate transaction.
+
+On startup, only generation-zero direct pending approvals remain decidable and
+generation-zero approved calls are safely queued. Sandbox pending or approved
+approvals tied to a lost worker generation become canceled or stale. Any row
+left executing is marked interrupted and is never retried, because its external
+side effect may have completed before the process stopped.
+A persisted nondecreasing clock high-water mark prevents a backward wall-clock
+adjustment from reviving expired work. Active counts, aggregate ciphertext,
+individual payloads, and terminal history all have hard bounds. Terminal
+retention uses insertion order rather than wall-clock order.
+
+Caller correlation is scoped by typed actor, execution ID, and call ID. A
+nonterminal approval retains that correlation without expiry. On a terminal
+transition, its correlation receives a fixed 24-hour expiry. Identical retries
+during that window recover the original approval or its retained tombstone;
+identity mismatches fail with a conflict. After the full 24 hours, and only
+after any linked gateway idempotency response also expires, the same IDs may
+represent a new call. Cleanup runs before the bounded correlation-cap check and
+never removes active or unexpired entries.
+
+Approval APIs are:
+
+- `GET /api/v1/approvals` and `GET /api/v1/approvals/{id}`
+- `POST /api/v1/approvals/{id}/decision`
+- `GET` and `DELETE /api/v1/gateway/approvals/{id}`
+
+OAuth2 and OpenID Connect operations may still use a manually supplied access
+token in the encrypted credential envelope, labeled
+`manual_oauth_access_token` in metadata. Managed OAuth is available for
+OpenAPI authorization-code security schemes, GraphQL's `default` credential,
+and HTTP MCP's `default` credential. It uses provider-neutral discovery, PKCE,
+administrator-session-bound callback transactions, encrypted token storage,
+refresh-token rotation, and just-in-time access tokens. Managed token values
+are not copied into source credentials, approvals, or request logs. Static
+credentials take precedence when both forms are configured.
+
+## MCP transport boundary
+
+Executor exposes one stateful Streamable HTTP MCP endpoint at `/mcp`. It
+requires dashboard API-token bearer authentication and protocol version
+`2025-11-25`. A session belongs to its creating token and is removed when that
+token is revoked. Explicit deletion, token revocation, and lazy idle-session
+expiry terminate the session, cancel its scoped waits, and prevent a pending
+Ask released by that termination from executing later. The MCP surface
+intentionally exposes five stable virtual tools, `execute`, `call`, `search`,
+`describe`, and `sources`, instead of mirroring an unbounded upstream catalog
+into `tools/list`. For virtual-tool invocations, JSON-RPC request IDs provide
+the idempotency and cancellation correlation. Downstream GET streaming is
+intentionally unavailable and returns 405 after session validation. Body
+admission and detached execution each have independent instance-wide and
+per-token concurrency caps, leaving cancellation handling separate from tool
+execution admission. Pending Ask calls follow the durable ten-minute approval
+TTL with expiry settlement checked at intervals of at most 30 seconds. They do
+not use an independent MCP transport wait timeout.
+
+The Rust package pins `rmcp` exactly at `1.8.0` for MCP model types. Executor
+owns the HTTP and stdio transport implementations so protocol handling remains
+inside the hardened outbound, process, authentication, and lifecycle
+boundaries.
+
+Upstream MCP sources use the same catalog and invocation boundary as OpenAPI.
+Streamable HTTP endpoints use the hardened outbound client, while stdio sources
+can select only administrator-approved templates loaded at server startup.
+Executables, arguments, working directories, and static environment values are
+not accepted from source API requests. Only declared secret environment fields
+can be supplied through the encrypted source credential envelope.
+
+Successful creation with complete credentials and successful refresh complete
+initialization and bounded paginated discovery before an atomic catalog commit.
+Required-secret stdio creation may instead commit the documented deferred empty
+source without starting the child. Upstreams that advertise
+`tools.listChanged` receive one coalescing watcher per source. Watchers are
+restored on startup, replaced after relevant changes, stopped before deletion,
+and drained during shutdown. Candidate HTTP and stdio credentials are used for
+discovery before one transaction replaces the encrypted credential and catalog.
+An HTTP notification-stream GET that returns 405 is treated as permanently
+unsupported only after the current revision-fenced reconciliation commits. The
+watcher then exits without reconnecting, so later catalog changes require a
+manual refresh.
+
+Clearing HTTP authentication tries anonymous discovery; clearing required stdio
+secrets instead retains the last catalog and marks health unknown. A zero-secret
+stdio template treats its empty overlay as a complete credential and refreshes
+normally.
+
+Imported MCP tools are intrinsically `enabled` only when the upstream explicitly
+marks them read-only without marking them destructive. Other MCP tools are
+intrinsically `ask`. Administrator source and tool overrides still use the global
+catalog precedence rules. Invocation creates a fresh upstream connection,
+performs one tool call, and closes it. Outcome-unknown tool calls are never
+automatically replayed. Long-lived HTTP notification streams have a five-minute
+idle-read timeout plus per-event and parser-state bounds, not a fixed lifetime.
+The complete configuration, API payloads, lifecycle, and limit contract is
+documented in [MCP support](mcp.md).
+
+Audit history is bounded to the most recently inserted 10,000 events for a
+single-user instance. Each event's serialized metadata is capped at 64 KiB.
+Insertion and oldest-insertion compaction happen in the same catalog
+transaction, so a committed mutation retains its actor, correlation ID, target
+snapshots, and metadata while never leaving an over-cap audit table. Retention
+uses SQLite insertion order rather than wall-clock timestamps, so clock
+corrections cannot discard the audit for a newly committed mutation.
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 000000000..84ad361a7
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,135 @@
+# Rust CLI
+
+The `executor` binary is both the self-hosted server and its local client. Client
+commands connect to a running server. They do not open the SQLite database or
+start another server. The server never auto-starts for a client command.
+
+## Connection
+
+The default server is `http://127.0.0.1:4788`. Override it with
+`--base-url` or `EXECUTOR_BASE_URL`. Sign in as the administrator, create an
+API token under **API tokens**, and provide it through `EXECUTOR_API_TOKEN`:
+
+```sh
+export EXECUTOR_API_TOKEN='copy-the-token-from-the-dashboard'
+executor tools search calendar
+```
+
+You can also use `--api-token`, but the environment variable avoids placing a
+secret in shell history or process arguments. Tokens are sent only in the
+Authorization header. Executor never puts them in URLs.
+
+Plain HTTP is accepted only for localhost and literal loopback addresses. Use
+HTTPS for another host. `--allow-insecure-http` is an explicit escape hatch for
+an authenticated and encrypted tunnel or equivalent transport that protects
+the complete path. It sends the reusable bearer token over clear HTTP, so a
+private LAN by itself is not sufficient protection.
+
+Use `--json` for machine-readable output. Errors and approval instructions go
+to stderr, leaving stdout available for JSON and MCP protocol messages.
+
+Connection, authentication, validation, and protocol failures exit nonzero. An
+upstream tool failure is a completed call: `--json` prints its result envelope
+and the process exits successfully. Automation must inspect the envelope's
+`ok` and `error` fields instead of treating exit status alone as tool success.
+
+## Service lifecycle
+
+The release binary embeds the supported systemd and launchd assets. No source
+checkout is needed:
+
+```sh
+executor service install [--no-start]
+executor service status
+executor service start
+executor service stop
+executor service restart
+executor service remove
+```
+
+On Linux, `install`, `start`, `stop`, `restart`, and `remove` manage the system
+unit and must run through `sudo`. Status is unprivileged. On macOS these commands
+manage the logged-in user's LaunchAgent and reject `sudo`. Windows service
+management is not supported.
+
+Status output is intentionally stable for scripts. It prints exactly `active`
+and exits 0, or prints exactly `inactive` and exits 3. A command or platform
+failure exits 1. The global connection and `--json` options do not change
+service output.
+
+Install is idempotent and replaces only files tracked by a private ownership
+manifest. On first install it may adopt an identical copy of its currently
+running binary, but rejects other preexisting service files. It preserves
+existing data, configuration, template registry, and a valid master key.
+Remove unloads the service and removes its managed executable, service
+definition, and ownership manifest while preserving persistent state. Every
+non-install mutation verifies the recorded file hashes before touching a loaded
+service. Install verifies an existing manifest, or applies the first-install
+rules described above. Unsafe paths, symbolic links, hard links, permission
+changes, and replaced file contents fail closed. If manifest publication is
+interrupted, a private recovery marker authorizes only a new `service install`;
+other lifecycle mutations remain blocked until that rerun completes.
+
+On macOS, the service owns `$HOME/.executor/service/bin/executor` and its
+control files under `$HOME/.executor/service`. The archive installer separately
+owns `$HOME/.executor/bin`. Reinstalling the LaunchAgent with no configuration
+environment variables preserves the exact stored data directory, template
+file, public origin, and trusted proxy list. Supplying one variable updates
+only that field, and explicitly empty public-origin or trusted-proxy variables
+clear those fields. Service removal preserves this configuration for a later
+reinstall and does not change the archive installation.
+
+## Tools
+
+Search the non-disabled global catalog. Enabled and Ask tools are discoverable;
+Disabled tools are omitted:
+
+```sh
+executor tools search issue
+executor tools search issue --namespace github --limit 25
+executor tools describe github.issues_create
+executor tools sources
+```
+
+Call a tool with a dotted path or two path segments. Arguments must be one JSON
+object. Prefix a filename with `@` to read the object from disk.
+
+```sh
+executor call github.issues_create '{"owner":"acme","repo":"app","title":"Bug"}'
+executor call github issues_create @arguments.json
+```
+
+Paths contain exactly the source slug and local tool name. A leading `tools.`
+is accepted for a dotted path, but deeper legacy resource or method segments
+are not.
+
+Each call gets one idempotency key that remains stable across safe HTTP retries. An
+Ask-mode tool prints and opens the clean dashboard approval URL, then polls as
+the owning API token until the approval completes. Press Ctrl-C to request
+cancellation.
+
+## MCP stdio bridge
+
+Configure a local MCP client to launch:
+
+```sh
+executor mcp
+```
+
+The bridge forwards newline-delimited MCP JSON-RPC over stdio to the running
+authenticated `/mcp` endpoint. It preserves the server session, writes only MCP
+messages to stdout, and closes the session on EOF or Ctrl-C. The server must
+already be running.
+
+## Dashboard
+
+Open the dashboard without putting credentials in the URL. This command does
+not require an API token. The browser still requires the administrator login
+cookie:
+
+```sh
+executor open
+```
+
+This uses `open` on macOS and `xdg-open` on Linux. If no graphical opener is
+available, the CLI prints the clean URL so it can be copied manually.
diff --git a/docs/docker.md b/docs/docker.md
new file mode 100644
index 000000000..680140492
--- /dev/null
+++ b/docs/docker.md
@@ -0,0 +1,139 @@
+# Run Executor with Docker
+
+The root `Dockerfile` builds the Svelte application and embeds it into the Rust
+binary. Its runtime image contains that one binary plus CA certificates and the
+healthcheck client. Project and dependency license notices are installed under
+`/usr/share/licenses/executor`. It supports Linux `amd64` and `arm64` images.
+
+## Start the local instance
+
+Published releases from the current repository use the root Rust image at
+`ghcr.io/davis7dotsh/executor`. A fork's workflow derives the image owner from
+that fork's repository. Stable releases publish `vX.Y.Z`, `X.Y.Z`, and
+`sha-` plus `latest`; prereleases publish their immutable release,
+version, and commit tags plus `beta`. Pin a version for repeatable deployments:
+
+```sh
+docker pull ghcr.io/davis7dotsh/executor:v0.1.0
+```
+
+The container package must be public in GHCR package settings. The release
+workflow performs an unauthenticated pull by exact digest and refuses to
+publish the GitHub release while anonymous access is unavailable.
+
+The included Compose file builds the same root `Dockerfile` locally:
+
+```sh
+docker compose up --detach --build
+docker compose logs executor
+```
+
+To run the already-built `runtime-prebuilt` image published by the release
+workflow, choose an immutable tag and tell Compose not to build locally:
+
+```sh
+export EXECUTOR_IMAGE=ghcr.io/davis7dotsh/executor:v0.1.0
+docker compose pull executor
+docker compose up --detach --no-build executor
+docker compose ps executor
+curl --fail --silent --show-error http://127.0.0.1:4788/healthz
+docker compose logs executor
+```
+
+Keep `EXECUTOR_IMAGE` set for later `docker compose` commands that operate on
+that deployment. Unset it to return to the default `executor:local` image and
+the local build flow.
+
+Open `http://127.0.0.1:4788`. On first boot, the logs contain the one-time setup
+URL. The compose configuration publishes the port only on host loopback. Keep
+that binding when using plain HTTP.
+
+The named `executor-data` volume contains SQLite state, its WAL files, and the
+generated `master.key`. Back up the whole volume while the service is stopped.
+The database and key are one recovery unit. Losing the key makes encrypted
+credentials and protected state unrecoverable, and inventing a new key beside
+an existing database is intentionally rejected.
+
+```sh
+docker compose stop executor
+# Snapshot the complete executor-data volume with your normal volume backup tool.
+docker compose start executor
+```
+
+If you configure `EXECUTOR_MASTER_KEY_FILE`, the mounted key must contain
+exactly 32 cryptographically random raw bytes from a secure random source or
+secret manager and be readable by UID `10001`. Do not use a password, repeated
+bytes, hand-authored content, hex text, or base64 text. Include that external
+file in the same stopped recovery snapshot as the volume. Executor does not
+create, chmod, rotate, or recover an external key.
+
+The shipped Compose file does not pass this optional variable by default. Add
+an override such as:
+
+```yaml
+services:
+ executor:
+ environment:
+ EXECUTOR_MASTER_KEY_FILE: /run/secrets/executor-master.key
+ volumes:
+ - ./secrets/executor-master.key:/run/secrets/executor-master.key:ro
+```
+
+The container runs as UID and GID `10001`, uses a read-only root filesystem,
+drops Linux capabilities, and receives a private temporary filesystem. If an
+external backup or secret manager creates mounted files, make them readable by
+UID `10001` without making them writable by other users.
+
+## HTTPS reverse proxy
+
+Leave the compose port bound to host loopback and set the exact browser-facing
+origin before starting the service:
+
+```sh
+EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com docker compose up --detach
+```
+
+Terminate TLS in a reverse proxy on the same host and forward to
+`127.0.0.1:4788`. The compose file passes through
+`EXECUTOR_TRUSTED_PROXIES` only when that host variable is set. Configure it
+only when Executor must use forwarded client addresses, and include only the
+proxy CIDRs that directly append or replace `X-Forwarded-For`.
+
+## MCP stdio templates
+
+The default mounted registry is empty and boot-safe. Start a private registry
+from the full example:
+
+```sh
+mkdir -p .executor-local
+cp docker/mcp-stdio-templates.full.example.json \
+ .executor-local/mcp-stdio-templates.json
+```
+
+Edit the `compose.yaml` bind mount so its host side is
+`./.executor-local/mcp-stdio-templates.json`, then mount each referenced Linux
+executable and working directory at its exact absolute container path.
+
+The runtime image does not include Node.js or Bun. Mounted stdio servers must be
+self-contained Linux executables for the image architecture, or you must derive
+a runtime image containing their dependencies. Treat every configured stdio
+executable as fully trusted: it runs as Executor's UID and can read the data
+volume and master key.
+
+## Multi-platform image check
+
+Release automation can build both supported architectures from the same file:
+
+```sh
+docker buildx build --platform linux/amd64,linux/arm64 --file Dockerfile .
+```
+
+The default root target remains self-contained and builds Svelte and Rust. The
+manual release workflow instead uses the `runtime-prebuilt` target: it copies
+the already-tested Linux binaries and license notices from the native release
+archives, pushes each architecture by digest, then assembles the multi-platform
+manifest. This keeps the embedded UI and notices identical between archives
+and release images. The release target pins its Debian runtime and apt snapshot
+and normalizes image timestamps to the release commit. Existing immutable
+release, version, or commit tags may be reused only when their digest already
+matches; only `latest` or `beta` may move.
diff --git a/docs/install.md b/docs/install.md
new file mode 100644
index 000000000..091257249
--- /dev/null
+++ b/docs/install.md
@@ -0,0 +1,268 @@
+# Install and run Executor
+
+Executor ships as one native binary with its Svelte dashboard embedded. The
+supported native targets are Linux and macOS on x86-64 and ARM64. Windows is not
+supported. The macOS archives require macOS 14 Sonoma or newer. Docker images
+support Linux `amd64` and `arm64`.
+
+## Prerequisites
+
+Building from a checkout requires Bun 1.3.x, Rust 1.96 or newer, a C toolchain,
+and the normal SQLite build prerequisites for the platform. The workspace
+declares Bun 1.3.11 and the release workflow currently uses Rust 1.96.0.
+
+Run `bun run bootstrap` once in a fresh checkout. It installs workspace
+dependencies, prepares retained TypeScript packages, and installs Playwright
+Chromium for browser scenarios.
+
+## Release archive
+
+The manual native release workflow publishes checksum-verified archives to the
+matching GitHub release. Install the latest supported archive with:
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/davis7dotsh/executor-fork-testing/main/scripts/install.sh | bash
+```
+
+The installer and native release workflow use
+`davis7dotsh/executor-fork-testing` as the current publishing origin. Export
+`EXECUTOR_REPOSITORY` before running the script to install from another fork:
+
+```sh
+export EXECUTOR_REPOSITORY=owner/repository
+curl -fsSL "https://raw.githubusercontent.com/$EXECUTOR_REPOSITORY/main/scripts/install.sh" | bash
+```
+
+Forward installer options after `bash -s --`, for example:
+
+```sh
+curl -fsSL "https://raw.githubusercontent.com/$EXECUTOR_REPOSITORY/main/scripts/install.sh" \
+ | bash -s -- --version 2.0.0 --no-modify-path
+```
+
+The installer defaults to
+`$HOME/.executor/bin`. Start a new shell, source its configuration file, or use
+`$HOME/.executor/bin/executor` until the updated PATH is active.
+
+On macOS, `executor service install` creates a separate managed copy at
+`$HOME/.executor/service/bin/executor`. Archive install, upgrade, and uninstall
+preserve the complete `$HOME/.executor/service` subtree. Service lifecycle
+commands likewise leave the archive-owned `$HOME/.executor/bin` subtree alone.
+
+The macOS command-line archive is not Developer ID signed or notarized. Use the
+curl installer above, which verifies the release checksum before installation,
+or download with `gh` and perform the checksum and immutable GitHub Release
+verification checks below.
+Browser-downloaded quarantined binaries are not a supported Gatekeeper
+distribution path. The workflow sets `MACOSX_DEPLOYMENT_TARGET=14.0` and rejects
+an archive whose Mach-O minimum does not match that support floor.
+
+### Uninstall a user-level archive installation
+
+If a managed service was installed, remove it while the binary is still
+available. Skip the matching command when no service was installed:
+
+```sh
+# Linux system service
+sudo "$(command -v executor)" service remove
+
+# macOS per-user LaunchAgent, never use sudo
+executor service remove
+```
+
+Then run the same installer in uninstall mode:
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/davis7dotsh/executor-fork-testing/main/scripts/install.sh \
+ | bash -s -- --uninstall
+```
+
+For a custom location, export the same absolute `EXECUTOR_INSTALL_DIR` used at
+install time before running that command. Uninstall removes only `executor`,
+the three installed notice files, and the exact `# Executor` PATH block added
+by the installer. It preserves all databases, master keys, service definitions,
+other files in the install directory, and every other shell configuration line.
+Use `--no-modify-path` with `--uninstall` to leave shell configuration untouched.
+The operation is safe to repeat. Each successful install records the exact
+owned filenames and SHA-256 hashes in a private manifest. Installation refuses
+to overwrite an unowned file, and uninstall fails closed if an owned file or the
+manifest was replaced. Review and resolve such a replacement manually instead
+of deleting it as though the installer still owned it. Before changing owned
+files, installation publishes private rollback state. A later install or
+uninstall automatically restores that state after an interrupted mutation,
+while a partially completed uninstall resumes around already-removed owned
+files. The ownership state also records every exact shell-config path and PATH
+command the installer added, so uninstall cleans the original block even when
+the current shell or `ZDOTDIR` has changed.
+
+Install, recovery, PATH updates, and uninstall hold one private lock under the
+validated install directory. A custom install path must be normalized, contain
+no symbolic-link components, and have only root- or current-user-owned
+ancestors with no group or world write permission. The installer pins the
+parent and leaf directory identities before any owned-file mutation. A
+concurrent invocation exits with the live owner PID. A lock left by a crashed
+process is reclaimed only after its recorded process-birth identity no longer
+matches, which also protects against PID reuse. Managed file renames, rollback
+restoration, manifest publication, recovery retirement, and uninstall deletion
+are separated by stock `sync` durability barriers. Shell-configuration edits
+use a portable adjacent ownership lock, atomic replacement, and content checks
+so different install roots cannot lose one another's PATH updates. The release
+installer has no Python runtime dependency.
+
+The release artifact workflow creates these archives and matching `.sha256`
+sidecars:
+
+- `executor-x86_64-unknown-linux-gnu.tar.gz`
+- `executor-aarch64-unknown-linux-gnu.tar.gz`
+- `executor-x86_64-apple-darwin.tar.gz`
+- `executor-aarch64-apple-darwin.tar.gz`
+
+Each archive also contains the project `LICENSE`, the generated Rust
+`THIRD_PARTY_LICENSES.html` inventory, and Vite's bundle-derived embedded-web
+`THIRD_PARTY_JAVASCRIPT_LICENSES.json` inventory. The Linux binaries require
+glibc 2.35 or newer, matching the Ubuntu 22.04 release baseline. The release
+workflow inspects symbol versions and rejects a binary above that ceiling. Use
+Docker on older Linux distributions.
+
+The `executor` binary embeds the maintained systemd unit, LaunchAgent template,
+bounded log helper, and hardened installer logic. A downloaded release binary
+can therefore install its service without a source checkout.
+
+The workflow creates `SHA256SUMS`, runs every target natively, smoke-tests each
+binary, and publishes the GitHub release as immutable. Verify both the checksum
+manifest and GitHub's immutable release records before installing a downloaded
+archive:
+
+```sh
+tag=v2.0.0
+repository=OWNER/REPOSITORY
+mkdir release-assets
+gh release download "$tag" --repo "$repository" --dir release-assets
+if command -v sha256sum >/dev/null 2>&1; then
+ (cd release-assets && sha256sum --check SHA256SUMS)
+else
+ (cd release-assets && shasum -a 256 --check SHA256SUMS)
+fi
+gh release verify "$tag" --repo "$repository"
+for archive in release-assets/executor-*.tar.gz; do
+ gh release verify-asset "$tag" "$archive" --repo "$repository"
+done
+```
+
+The release workflow accepts an exact full commit SHA and tag, checks that the
+tag is `v`, and verifies that the remote tag resolves to the
+same commit before publishing. The GitHub release remains a draft until the
+archives, checksums, and root Docker image have all succeeded. Publishing the
+draft creates the immutable release record checked by the commands above.
+
+## Build a local release binary
+
+Install the prerequisites above, then run from the repository root:
+
+```sh
+bun run bootstrap
+bun run --cwd web build
+cargo build --locked --release
+./scripts/install.sh --binary ./target/release/executor
+```
+
+The web build must happen before the release Cargo build so `build.rs` can
+embed the static application. Node.js or Bun is not required after compilation.
+
+## First boot
+
+For an easy-to-inspect local instance, choose an explicit private data
+directory:
+
+```sh
+mkdir -p "$HOME/.executor-data"
+chmod 0700 "$HOME/.executor-data"
+"$HOME/.executor/bin/executor" server --data-dir "$HOME/.executor-data"
+```
+
+Executor binds to `127.0.0.1:4788` by default. Open the one-time setup URL
+printed at startup and create the administrator login. The dashboard immediately
+signs in with those credentials. If that follow-up login fails, sign in manually,
+then generate API tokens under **API tokens**. Token secrets are shown once.
+The setup token expires after 15 minutes and is replaced on a later boot until
+an administrator is created.
+
+The data directory contains the SQLite database, WAL and SHM files when
+present, process lock, and generated `master.key`. Back up the complete
+directory only while Executor is stopped. Keep the database and key together
+and private.
+
+To use an externally managed key, supply an exact 32-byte cryptographically
+random raw regular file from a secure random source or secret manager, give it
+exactly one hard link, and pass `--master-key-file` or
+`EXECUTOR_MASTER_KEY_FILE`. A key owned by Executor's effective user must use
+mode `0400` or `0600`. A root-owned secret-manager file may use `0400`, `0440`,
+`0600`, or `0640`; group-readable modes work only when Executor belongs to that
+group. This setting is a file path, never key material.
+Do not use a password, repeated bytes, hand-authored content, hex text, or
+base64 text as the key. Executor does not create, chmod, rotate, or recover an
+external key. Files owned by another user, symbolic links, non-regular files,
+additional hard links, and broader permissions fail closed. Include that file
+in the same stopped recovery snapshot as the data directory. Losing the key
+makes protected credentials, OAuth tokens, approvals, and results
+unrecoverable.
+
+Use `--public-origin https://executor.example.com` when a local HTTPS reverse
+proxy exposes the dashboard at another origin. An HTTPS public origin does not
+encrypt or firewall Executor's own listener. Keep the listener on loopback, or
+enforce network isolation so clients can reach it only through the TLS proxy.
+Do not expose the raw plaintext port.
+
+The one-time setup URL is a secret until setup completes. It may appear in the
+terminal, journal, or private service log, so protect those logs accordingly.
+
+## Server configuration
+
+| Environment variable | Server flag | Purpose |
+| ----------------------------------- | ----------------------- | ---------------------------------------------- |
+| `EXECUTOR_DATA_DIR` | `--data-dir` | SQLite, lock, and default master-key directory |
+| `EXECUTOR_MASTER_KEY_FILE` | `--master-key-file` | Existing raw 32-byte master-key file |
+| `EXECUTOR_MCP_STDIO_TEMPLATES_FILE` | `--mcp-stdio-templates` | Trusted local MCP template registry |
+| `EXECUTOR_PUBLIC_ORIGIN` | `--public-origin` | Exact browser-facing HTTP or HTTPS origin |
+| `EXECUTOR_TRUSTED_PROXIES` | `--trusted-proxy` | Comma-separated trusted reverse-proxy CIDRs |
+| `RUST_LOG` | none | Rust tracing filter, default `executor=info` |
+
+`--bind` and `--allow-unsafe-http-non-loopback` intentionally have no
+environment equivalents. Do not put credentials, API tokens, or raw key
+material in service environment files.
+
+Configure trusted proxies only when Executor must derive client addresses from
+`X-Forwarded-For`. List only the actual proxy CIDRs, include every hop in the
+chain, and configure each proxy to append or replace the header correctly. A
+broad trust range can permit spoofed login identities, while an incomplete
+chain causes forwarded logins to be rejected.
+
+## Managed services and containers
+
+After installing the release binary, install the native service directly:
+
+```sh
+# Linux system service
+sudo "$(command -v executor)" service install
+
+# macOS per-user LaunchAgent, never use sudo
+executor service install
+```
+
+Use `--no-start` to install while leaving the service stopped. The same binary
+provides `service status`, `start`, `stop`, `restart`, and `remove`. Removal
+preserves persistent data and configuration. A private ownership manifest
+binds later lifecycle mutations to the exact service files installed by this
+workflow, so replacing a managed file outside `service install` fails closed.
+On macOS, reinstall preserves the stored data directory, template file, public
+origin, and trusted proxy list unless the corresponding environment variable
+is explicitly supplied.
+
+- [Linux systemd service](systemd.md)
+- [macOS LaunchAgent](launchd.md)
+- [Docker Compose](docker.md)
+
+MCP stdio templates are machine-admin allowlists. Every approved executable is
+fully trusted because it runs with Executor's operating-system identity and can
+access its data and master key. See [MCP support](mcp.md) for the exact template
+schema and transport limits.
diff --git a/docs/launchd.md b/docs/launchd.md
new file mode 100644
index 000000000..cdae5b3db
--- /dev/null
+++ b/docs/launchd.md
@@ -0,0 +1,137 @@
+# Run Executor with launchd
+
+The native macOS release supports both Apple Silicon and Intel Macs. Install the
+release binary, then install its per-user LaunchAgent:
+
+```sh
+executor service install
+```
+
+Do not use `sudo`. The binary embeds the LaunchAgent template, bounded log
+helper, and installer, copies itself to
+`$HOME/.executor/service/bin/executor`, and uses private temporary files. A
+source checkout is not required. The archive-installed command remains at
+`$HOME/.executor/bin/executor`, with independent ownership and upgrade state.
+Pass `--no-start` to install the files while leaving the agent unloaded.
+
+The CLI records hashes for the installed binary, plist, wrapper, log helper,
+and persisted service configuration in
+`$HOME/.executor/service/service-install.manifest`, owned by the user with mode
+`0600`. Lifecycle mutations fail closed if a managed file is replaced outside
+`service install`. A private recovery marker makes an interrupted manifest
+publication safely rerunnable while keeping the LaunchAgent unloaded.
+
+LaunchAgent settings are stored in the private, non-executable control file
+`$HOME/.executor/service/service-config`. Reinstalling with no related
+environment variables preserves the exact existing data directory, template
+file, public origin, and trusted proxy list. Setting one variable changes only
+that field. An explicitly empty `EXECUTOR_PUBLIC_ORIGIN` or
+`EXECUTOR_TRUSTED_PROXIES` clears that setting.
+
+Custom data and template paths must be absolute. Every existing directory from
+the user's home boundary, or from the filesystem root for paths outside the
+home directory, must be owned by root or the current user and must not be
+group-writable or world-writable. Symbolic-link ancestors are rejected. The
+managed wrapper and bounded logger stay under `$HOME/.executor/service`, while
+the database, key, templates, and log remain under their configured paths.
+The template file also cannot equal, contain, or be nested beneath a managed
+service path or a reserved database, key, lock, or log path. These checks are
+case-insensitive so they remain safe on the default macOS filesystem.
+
+The LaunchAgent runs only while that user is logged in. It binds Executor to
+`127.0.0.1:4788`, keeps persistent state under
+`~/Library/Application Support/Executor`, and stops it with `SIGTERM` so the
+server can drain active work before launchd's 30-second exit deadline.
+
+Executor creates `master.key` inside the data directory on first boot. Back up
+the data directory and master key together. Losing the key makes encrypted
+credentials and protected state unrecoverable. Keep the directory private and
+never copy the key into a shell command, plist, log, or source-control file.
+
+The installer creates an empty, mode `0600` MCP stdio template registry at
+`~/Library/Application Support/Executor/mcp-stdio-templates.json`. Edit that
+file to add trusted local executables, then restart Executor. Do not make the
+template file, referenced executable, working directory, or any ancestor path
+writable by untrusted users.
+
+Treat every configured stdio executable as fully trusted. launchd runs it as
+the same user as Executor, so it can read that user's Executor data directory
+and master key. The template allowlist prevents dashboard users from selecting
+arbitrary commands, but it is not a privilege boundary between Executor and an
+approved executable.
+
+## Operate the service
+
+```sh
+executor service status
+executor service start
+executor service stop
+executor service restart
+```
+
+`service status` prints exactly `active` or `inactive`, exits 0 for active, and
+exits 3 for inactive. Operational failures exit 1. Restart uses a graceful
+`bootout` followed by `bootstrap`. The CLI never uses force-killing
+`kickstart -k`.
+
+Executor output is written to `executor.log` inside its mode `0700` data
+directory. The generated LaunchAgent helper keeps the current log and three
+rotated generations, each capped at 1 MiB and mode `0600`. It copies input in
+fixed-size chunks, so a large stream with no newline cannot make the helper's
+memory grow with the stream. This keeps the first-boot setup token in the
+user's private data directory without allowing normal tracing output to grow
+without a bound.
+
+```sh
+tail -f "$HOME/Library/Application Support/Executor/executor.log"
+```
+
+The one-time setup URL appears in that output on first boot.
+
+To serve the dashboard through an HTTPS reverse proxy, reinstall the agent with
+the exact browser-facing origin:
+
+```sh
+EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com executor service install
+```
+
+Leave Executor on its default loopback bind. The local reverse proxy should be
+the only process that exposes it to other machines. Configure trusted proxy
+CIDRs only when forwarded client addresses are required:
+
+```sh
+EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com \
+EXECUTOR_TRUSTED_PROXIES=127.0.0.1/32,::1/128 \
+executor service install
+```
+
+`EXECUTOR_TRUSTED_PROXIES` accepts the same CIDRs as a comma-separated list.
+
+To remove the service without deleting data:
+
+```sh
+executor service remove
+```
+
+Removal checks `launchctl bootout` errors, then removes the managed plist,
+wrapper, log helper, service-private binary, and ownership manifest. It
+preserves the persisted service configuration, the archive installation under
+`$HOME/.executor/bin`, and the database, master key, template registry, and
+logs under Application Support. A later service install reuses the preserved
+settings unless explicit environment overrides are supplied.
+
+## Back up and restore
+
+Stop Executor before copying its SQLite database, WAL files, and master key.
+Back up the whole data directory as one unit:
+
+```sh
+executor service stop
+cp -pR "$HOME/Library/Application Support/Executor" "$HOME/Executor-backup"
+executor service start
+```
+
+To restore, stop the service, replace the complete data directory from one
+backup, preserve its private permissions, then start the service. Never create
+a replacement key beside an existing `executor.db`; Executor intentionally
+fails closed when the initialized database and its original key are separated.
diff --git a/docs/mcp.md b/docs/mcp.md
new file mode 100644
index 000000000..5169e4bfb
--- /dev/null
+++ b/docs/mcp.md
@@ -0,0 +1,412 @@
+# MCP support
+
+Executor can sit on both sides of MCP:
+
+- MCP clients connect to Executor's stateful Streamable HTTP endpoint at
+ `/mcp`.
+- Executor imports tools from upstream MCP servers over Streamable HTTP or
+ local stdio.
+- `executor mcp` adapts a local stdio-only MCP client to Executor's `/mcp`
+ endpoint.
+
+All three surfaces currently require MCP protocol version `2025-11-25`.
+Executor does not negotiate an older protocol version.
+
+The Rust package pins `rmcp` exactly at `1.8.0` for MCP model types. Executor
+implements its HTTP and stdio transport boundaries locally so the gateway can
+apply its own authentication, SSRF controls, process isolation, limits, and
+lifecycle rules.
+
+This slice supports MCP tools only. The downstream endpoint does not advertise
+resources, prompts, completions, tasks, sampling, or elicitation. Upstream
+discovery imports `tools/list`; other upstream capabilities are retained as
+metadata but are not bridged into Executor surfaces.
+
+## Connect an MCP client to Executor
+
+The downstream endpoint is the instance public origin plus `/mcp`, for example
+`http://127.0.0.1:4788/mcp`. Authenticate every non-preflight request with a
+dashboard-generated API token:
+
+```text
+Authorization: Bearer
+```
+
+The endpoint uses the stateful Streamable HTTP lifecycle:
+
+1. Send `initialize` without `MCP-Session-Id`.
+2. Read the new `MCP-Session-Id` response header.
+3. Send `notifications/initialized` with that session ID and
+ `MCP-Protocol-Version: 2025-11-25`.
+4. Include both headers on subsequent requests.
+5. Send `DELETE /mcp` with both headers to close the session.
+
+`POST /mcp` requires `Content-Type: application/json` and an `Accept` value
+that permits both `application/json` and `text/event-stream`. Responses are
+currently JSON request-response messages. `GET /mcp` does not open a server
+event stream. After validating an initialized session it returns HTTP 405 with
+`Allow: POST, DELETE, OPTIONS`.
+
+Sessions belong to the API token that initialized them. A different token sees
+the session as missing. Sending `DELETE /mcp`, revoking the owning API token,
+or lazily pruning an idle-expired session terminates that session and cancels
+its active waits. A pending Ask released by session termination cannot execute
+later. Initialized sessions expire after eight idle hours; incomplete
+handshakes expire after five idle minutes. The server allows 32 sessions per
+API token and 4,096 sessions for the instance, returning service unavailable
+when either capacity is full. Downstream session IDs are at most 128 printable
+ASCII bytes.
+
+Browser-origin requests must use the configured public origin. Executor checks
+both `Host` and `Origin` before authentication and emits narrowly scoped CORS
+headers for that origin. Repeated security-sensitive headers are rejected.
+
+### Virtual tools
+
+`tools/list` always returns five Executor-owned tools. It does not return every
+upstream tool as a separate MCP tool.
+
+| Tool | Purpose |
+| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `execute` | Run up to 1 MiB of sandboxed TypeScript that can discover and call multiple Executor tools concurrently. Accepts `code` and optional `timeoutMs` from 1 through 300,000. |
+| `call` | Call one catalog tool by a 1 through 512 byte `path`, with an optional `arguments` object. Ask-mode tools wait for administrator approval. |
+| `search` | Search the enabled global catalog by `query`, optional `namespace`, `limit`, and `offset`. The limit is 1 through 100 and defaults to 20. |
+| `describe` | Return schemas and metadata for one enabled catalog tool by a 1 through 512 byte `path`. |
+| `sources` | List sources that currently expose tools through the global catalog. |
+
+Tool paths use `tools..`. Disabled tools remain
+unavailable. Imported tools are intrinsically `enabled` only when the upstream
+explicitly marks them read-only without also marking them destructive. All
+other imported MCP tools default to `ask` mode.
+
+For `tools/call`, the MCP JSON-RPC request ID is part of the idempotency identity
+and cancellation correlation. Numeric and string IDs are distinct; string IDs
+are at most 256 bytes and cannot contain a NUL character. Reusing an ID for a
+different virtual-tool invocation is rejected. `notifications/cancelled` is
+scoped to an in-flight `tools/call` in the same session. A call that may already
+have reached an upstream MCP server is never silently replayed.
+
+### Local stdio bridge
+
+For clients that can launch only a stdio MCP server, configure them to run:
+
+```sh
+EXECUTOR_API_TOKEN='' \
+ executor --base-url http://127.0.0.1:4788 mcp
+```
+
+The bridge keeps stdout protocol-only, forwards requests concurrently, reserves
+capacity for cancellation notifications, and closes the HTTP session on exit.
+It allows eight ordinary requests in flight, with a separate ceiling of 16
+while admitting cancellations. Non-loopback plaintext HTTP is rejected unless
+the client command also receives `--allow-insecure-http`.
+
+## Add upstream MCP sources
+
+MCP source management uses the same administrator routes as other protocols:
+
+- `POST /api/v1/sources`
+- `POST /api/v1/sources/{id}/refresh`
+- `GET`, `PUT`, and `DELETE /api/v1/sources/{id}/credentials`
+- `GET /api/v1/mcp/stdio/templates`
+
+Reads require the administrator session cookie. Mutations additionally require
+the matching Origin and CSRF cookie/header pair. The dashboard is the normal
+way to satisfy these controls. The JSON below documents the protocol payloads
+for API clients that already implement administrator authentication.
+
+### Streamable HTTP source
+
+Create a remote HTTP source with `kind: "mcp_http"`:
+
+```json
+{
+ "kind": "mcp_http",
+ "displayName": "Issue tracker",
+ "preferredSlug": "issues",
+ "description": "Internal issue tools",
+ "endpoint": "https://mcp.example.com/mcp",
+ "allowPrivateNetwork": false,
+ "credential": {
+ "type": "bearer",
+ "token": "secret"
+ }
+}
+```
+
+`preferredSlug`, `description`, and `credential` are optional.
+`allowPrivateNetwork` defaults to `false`. The endpoint must be an absolute
+HTTP or HTTPS URL without user information or a fragment. A query string is
+allowed, but it is encrypted with the credential state and removed from the
+public source configuration.
+
+The HTTP credential is one of:
+
+```json
+{ "type": "bearer", "token": "secret" }
+```
+
+```json
+{ "type": "basic", "username": "user", "password": "secret" }
+```
+
+```json
+{ "type": "api_key_header", "name": "X-Api-Key", "value": "secret" }
+```
+
+```json
+{ "type": "oauth_access_token", "accessToken": "secret" }
+```
+
+The OAuth form above is a manually supplied access token. For managed OAuth,
+leave the static credential empty and configure the source's `default`
+credential in the dashboard's Managed OAuth panel. HTTP MCP sources use
+protected-resource and authorization-server discovery, PKCE authorization-code
+callbacks, encrypted refresh-token storage, and just-in-time access tokens.
+Static credentials take precedence when one is configured. Stdio MCP sources
+do not expose managed OAuth.
+
+Transport-owned headers cannot be used for `api_key_header`. This includes
+`Authorization`, `Host`, `Content-Type`, `Accept`, length and connection
+headers, `MCP-Session-Id`, `MCP-Protocol-Version`, `Origin`, `Referer`, and
+proxy headers.
+
+Replace the HTTP credential with optimistic concurrency:
+
+```json
+{
+ "expectedRevision": 3,
+ "credential": {
+ "credential": {
+ "type": "bearer",
+ "token": "replacement"
+ }
+ }
+}
+```
+
+For a non-null replacement, Executor discovers with the candidate credential
+first. One SQLite transaction then replaces the encrypted credential and
+refreshes artifacts, bindings, tools, health, and revisions. Discovery or
+commit failure leaves the prior credential, catalog, and watcher unchanged.
+
+Use `"credential": null` inside the protocol credential object to remove HTTP
+authentication without changing the endpoint. Executor first attempts
+anonymous discovery. On success, one SQLite transaction clears the credential
+and refreshes the catalog. After commit, Executor installs a replacement
+watcher only when the refreshed capabilities advertise `tools.listChanged`. If
+anonymous discovery fails, Executor still commits the credential clear together
+with unknown source health, preserves the last known catalog, and leaves its
+watcher stopped. A failed clear commit preserves the old database state and
+reinstalls its prior watcher. `GET` returns only the revision and configured
+credential type, never secret values.
+
+### Stdio source templates
+
+Arbitrary commands are never accepted through the source API. The machine
+administrator must first approve every executable, argument, working directory,
+and non-secret environment value in a strict JSON template file. Start the
+server with either form:
+
+```sh
+executor server --mcp-stdio-templates /absolute/path/to/mcp-stdio.json
+```
+
+```sh
+EXECUTOR_MCP_STDIO_TEMPLATES_FILE=/absolute/path/to/mcp-stdio.json executor server
+```
+
+If neither is set, the registry is empty and HTTP MCP sources remain
+available. If a path is set, an unreadable or invalid file fails server
+startup.
+
+The file has exactly this top-level shape:
+
+```json
+{
+ "templates": [
+ {
+ "name": "filesystem",
+ "executable": "/absolute/path/to/mcp-server",
+ "cwd": "/absolute/path/to/approved-directory",
+ "arguments": ["--stdio"],
+ "environment": {
+ "LOG_LEVEL": "warn"
+ },
+ "secretEnvironment": ["API_TOKEN"]
+ }
+ ]
+}
+```
+
+`cwd`, `arguments`, `environment`, and `secretEnvironment` are optional. Unknown
+fields fail startup. Template names contain only ASCII letters, digits, `_`, or
+`-` and are at most 64 bytes. Template names must be unique. Executables and
+working directories must be absolute and are canonicalized when the registry
+loads. The executable must be a regular executable file, and `cwd` must be a
+directory.
+
+The configuration file is limited to 1 MiB and 256 templates. Each template
+allows at most 128 arguments. The final static-plus-secret environment allows
+at most 128 entries. An argument is at most 8 KiB, an environment value is at
+most 16 KiB, and the combined argument or environment data for a template is
+at most 128 KiB. Environment names use shell-style identifiers. A secret field
+cannot duplicate a static environment entry.
+
+Child processes receive a clean environment containing only the approved
+static entries plus the exact secret overlay. On Unix, each child receives its
+own process group. Graceful shutdown escalates to killing that process group,
+including descendants that remain alive.
+
+Executor does not enforce ownership or write permissions for the template
+file, executable, or their ancestor directories. The machine administrator is
+responsible for protecting those paths from untrusted modification.
+
+After server startup, discover the non-secret template descriptors at
+`GET /api/v1/mcp/stdio/templates`:
+
+```json
+{
+ "templates": [
+ {
+ "name": "filesystem",
+ "secretFields": ["API_TOKEN"]
+ }
+ ]
+}
+```
+
+Create a source with a template name, never an executable path:
+
+```json
+{
+ "kind": "mcp_stdio",
+ "displayName": "Local filesystem",
+ "preferredSlug": "files",
+ "description": "Approved local file tools",
+ "templateName": "filesystem",
+ "secretValues": {
+ "API_TOKEN": "secret"
+ }
+}
+```
+
+`preferredSlug`, `description`, and `secretValues` are optional. If a template
+declares secrets and `secretValues` is omitted or empty, Executor creates an
+empty source without starting the process. A later credential update must
+supply every declared secret and no others, then discovery runs. Credential
+replacement uses:
+
+```json
+{
+ "expectedRevision": 0,
+ "credential": {
+ "secretValues": {
+ "API_TOKEN": "replacement"
+ }
+ }
+}
+```
+
+Credential replacement discovers first, then commits the encrypted credential
+and refreshed catalog atomically. A discovery failure leaves both unchanged.
+Clear credentials with
+`DELETE /api/v1/sources/{id}/credentials?expectedRevision=`. For HTTP
+sources, the nested `"credential": null` PUT form described above has the same
+clear semantics.
+
+Clearing credentials for a template that declares secrets stops its catalog
+watcher and leaves both the approved template selection and existing catalog
+entries in place. The empty encrypted overlay and unknown source health commit
+together; calls cannot start until complete credentials are restored.
+Templates without secret fields treat the empty overlay as complete, discover
+first, and atomically commit the empty credential plus refreshed catalog so the
+source remains healthy.
+
+## Discovery, refresh, and invocation lifecycle
+
+When credentials are complete, source creation initializes the upstream, walks
+every `tools/list` page, validates and normalizes the complete result, then
+commits the source, encrypted credential envelope, capabilities artifact,
+bindings, and tools atomically. A stdio source created without its required
+secrets follows the deferred empty-source behavior described above. Discovery
+permits at most 1,000 pages, 100,000 tools, 32 MiB of aggregate tool metadata,
+5 MiB for one tool, 2 MiB for one schema, and 8 MiB of capability metadata.
+Cursor cycles, duplicate tool names, malformed schemas, and unstable catalogs
+fail the operation.
+
+Refresh performs all upstream work before opening the catalog transaction. It
+commits only if the source and credential revisions still match. A failed or
+stale refresh leaves the existing catalog untouched. When the retained
+credential state remains complete, that catalog remains callable. Upstream tool
+names are stable keys, so refresh preserves Executor tool IDs, callable paths,
+and administrator mode overrides.
+
+If an upstream advertises `tools.listChanged`, Executor maintains a watcher and
+coalesces notifications into bounded refreshes. Watchers are restored at
+startup and run a full revision-fenced catalog reconciliation on every
+successful initial or reconnect handshake before entering steady notification
+handling. They are replaced after relevant credential changes, stopped before
+source deletion, and drained during shutdown. Stdio watchers also heartbeat the
+transport lifecycle so an idle child exit reconnects even without a
+notification. If an HTTP upstream returns 405 to the notification-stream GET,
+Executor lets the current revision-fenced reconciliation commit, then treats
+live notifications as unsupported and exits that watcher without reconnecting.
+Use manual refresh for later catalog changes from that upstream.
+
+An invocation uses a fresh upstream connection and MCP session. Executor
+initializes it, calls the named tool, then terminates or shuts down the session.
+An upstream `isError: true` result becomes a failed Executor tool result with
+code `upstream_tool_error`. Disconnects after a stdio tool call or transport
+failures during an HTTP tool call are treated as outcome-unknown and are not
+replayed.
+
+## Transport safety and limits
+
+Remote HTTP MCP uses the shared hardened outbound client. It allows only HTTP
+and HTTPS, disables environment proxies and redirects, validates and pins DNS
+answers, and blocks private targets unless the source explicitly opts in.
+Link-local, cloud metadata, multicast, documentation, and reserved addresses
+remain blocked even with private-network access enabled.
+
+An MCP endpoint is capped at 8 KiB. The shared client limits a resolved host to
+32 addresses, request bodies to 8 MiB, collected request-response bodies to 16
+MiB, and request or response headers to 64 KiB. Connection setup has a
+five-second timeout. The HTTP MCP transport adds a 30-second deadline to
+request-response exchanges, a 16 MiB aggregate ceiling across request-response
+SSE resumptions, at most 1,024 SSE events per exchange, at most three
+resumptions, and session IDs capped at 1,024 visible ASCII bytes.
+
+Long-lived notification streams have no cumulative byte or lifetime limit.
+They use a five-minute per-read idle timeout; declared `Content-Length`, each
+delivered body chunk, each undelimited parser line, and each accumulated SSE
+event are independently capped at 16 MiB. A connection accepts at most 1,024
+completed SSE events. Event 1,025 fails the listener, after which the source
+watcher reconnects with backoff and a fresh MCP initialization. The transport
+accepts only JSON or SSE response media types. Session changes, malformed
+JSON-RPC, unsupported server requests, and invalid content types fail closed.
+Server `ping` requests are answered; other server-initiated requests receive
+method not found.
+
+The stdio transport defaults to a 16 MiB message limit, 64 KiB stderr
+accounting ceiling, 60-second request timeout, two-second shutdown grace, and a
+256-command queue. Stderr is continuously drained and is not exposed or stored;
+only the exit code and whether the accounting ceiling was exceeded survive
+internally. The transport restarts a failed process with bounded exponential
+backoff, but a restart requires a new MCP initialization. It does not retry a
+tool call after bytes may have reached the child.
+
+The downstream `/mcp` body limit is the 8 MiB tool-argument ceiling plus a
+64 KiB protocol envelope. Bearer tokens are capped at 512 bytes. Body admission
+is limited to 16 requests for the instance and four per token. Detached
+execution is independently limited to 16 for the instance and four per token,
+leaving a control lane available for cancellation notifications. Body
+saturation returns HTTP 429 with `mcp_busy`; execution saturation returns a
+JSON-RPC `-32603` error. The cancellation registry is capped at 4,096 active
+requests, with at most 128 short-lived pre-cancellation records per session.
+Pre-cancellation records expire after 60 seconds. Pending Ask calls use the
+durable ten-minute approval TTL, and expiry settlement is checked at intervals
+of at most 30 seconds. There is no separate MCP transport wait timeout.
+`notifications/cancelled` explicitly cancels the matching request wait;
+`DELETE /mcp`, API-token revocation, and lazy idle-session expiry cancel all
+session-scoped waits.
diff --git a/docs/runtime.md b/docs/runtime.md
new file mode 100644
index 000000000..e7e4c41b5
--- /dev/null
+++ b/docs/runtime.md
@@ -0,0 +1,133 @@
+# Sandboxed TypeScript runtime
+
+Executor runs each TypeScript execution in a fresh hidden worker process. The
+server process owns authentication, policy, approvals, credentials, network
+access, tool dispatch, cancellation, and request logging. The worker receives
+only source code, public limits, and sanitized tool results.
+
+## Integration contract
+
+`RuntimeManager::execute` accepts an `ExecutionRequest`, a
+`HostToolDispatcher`, and an `ExecutionCancellation`. The worker can request
+only a tool path and one JSON argument. The parent adds the execution identity
+before calling `HostToolDispatcher::dispatch`. Implementations must select on
+the supplied cancellation handle while waiting for an interactive approval.
+They must also treat a dropped dispatch future as cancellation and remove any
+pending approval for that execution.
+
+The application adapter captures the authenticated actor and surface outside
+the worker, maps each runtime `ToolCall` into `invocation::ToolCallService`, and
+waits for an Ask decision while selecting on cancellation. When an execution
+ends, it calls `cancel_execution(execution_id)` so pending approval calls do not
+outlive their QuickJS continuation.
+
+The adapter assigns a distinct request-log ID to every tool call while keeping
+the HTTP request ID and execution ID as parent-side correlation. Worker
+generations are positive signed-range integers. They are included in approval
+records but expose no authority. Approval decisions start at most one
+server-owned background invocation. The worker waiter only observes the stored
+terminal result and never dispatches the upstream call itself.
+
+Expected upstream tool failures use `ToolResult::Failure`. They resolve in
+TypeScript as `{ error: { code, message } }`, so a script can inspect and react
+to them. IPC, worker, and internal bridge failures reject the tool promise or
+fail the execution with a stable public code.
+
+The result contract keeps the final JSON result, emitted JSON items, captured
+console entries, and parent-side tool-call correlation separate. Neither IPC
+nor runtime output contains credentials, request headers, source bindings, or
+approval records.
+
+## Source recovery and tool proxy
+
+Executor uses the first fenced code block when one is present. It accepts an
+async body with top-level `await` and `return`, a named function declaration, a
+function or arrow expression, a callable variable declaration, or a default
+exported callable. Oxc parses and removes TypeScript syntax inside the worker.
+Imports, module loading, decorators, enums, namespaces, and JSX fail closed.
+
+`tools` is a lazy recursive frozen proxy. Dotted and bracket access are
+equivalent. `then` and symbol property reads return `undefined`, which prevents
+accidental thenable behavior. A call forwards only its first argument and that
+argument must be JSON serializable. `Promise.all` creates concurrent parent
+dispatches, with at most 16 active calls and 128 total calls per execution.
+
+Three protocol-neutral discovery calls are handled entirely by the parent:
+`tools.search`, `tools.describe`, and `tools.sources`. They use the same catalog
+visibility rules as gateway discovery. `tools.sources` returns only public
+source metadata and never returns source configuration or credentials.
+
+## Gateway execution API
+
+`POST /api/v1/gateway/execute` is bearer-token only. Its JSON body is
+`{ "code": string, "timeoutMs"?: number }`. Authentication runs before JSON
+body parsing. Code is limited to 1 MiB, and the timeout defaults to 30 seconds
+with a five-minute maximum.
+
+The response contains `executionId`, `result`, `emits`, `console`, and `calls`.
+The call records contain only worker call IDs, public paths, and sanitized
+results. The endpoint holds the HTTP request open while Ask tools await an
+administrator decision. It does not provide persistence or replay. A client
+disconnect cancels the worker and all pending or approved calls that have not
+started upstream execution. Already-executing approved work remains owned by
+the server because an upstream side effect may already have occurred.
+
+The application-level `ExecutionService` owns actor context, runtime admission,
+disconnect cancellation, worker construction, and active execution tracking.
+HTTP only authenticates, parses, and maps the protocol response. CLI and MCP
+can reuse the same service without rebuilding lifecycle logic. Graceful
+shutdown stops new runtime admission, cancels active workers, waits for their
+parent-side cleanup, then stops approval and request-log tasks before closing
+SQLite.
+
+Before disconnect, revocation, shutdown, timeout, or worker-failure cancellation
+reaches the runtime, execution records an in-memory lost-continuation barrier.
+Approval decisions and approved-work claims share that gate, so cancellation
+and the `approved -> executing` transition have one linearized winner. If
+immediate SQLite cleanup repeatedly fails, a tracked retry task keeps the
+barrier active. Shutdown aborts that retry deterministically, and startup
+recovery terminalizes every pending or approved sandbox approval whose worker
+generation was lost.
+
+The worker has no module loader and exposes no filesystem, environment,
+process, FFI, or network API. `fetch`, `XMLHttpRequest`, `WebSocket`, `process`,
+`require`, `module`, `Deno`, and `Bun` are absent.
+
+## Isolation and limits
+
+IPC uses a private inherited Unix socket, never standard output. Every
+length-prefixed JSON frame has a strict version, execution generation, tagged
+schema, 18 MiB frame ceiling, and 32 MiB aggregate ceiling. Call IDs are
+monotonic. Unknown fields, duplicate or unknown IDs, wrong generations,
+partial frames, invalid lengths, and excessive JSON depth or node counts fail
+closed before serde allocation.
+
+QuickJS has a 64 MiB heap limit and 1 MiB engine stack limit. Source and
+transformed code are each capped at 1 MiB. Arguments and final results are
+capped at 8 MiB. Console capture is capped at 1,000 entries and 256 KiB. Emits
+are capped at 100 items and 8 MiB. The server admits at most eight workers at
+once. Additional executions fail immediately with `runtime_busy` rather than
+forming an unbounded waiter queue.
+
+The child starts with an empty environment, null standard streams, a private
+empty working directory, umask `077`, a new process group, and resource limits
+for CPU, address space, process stack, open files, output files, and core
+dumps. Executor intentionally does not set `RLIMIT_NPROC`, which would affect
+the shared user account rather than one worker. Cancellation and timeout kill
+the worker process group and reap the child before releasing its worker slot.
+Linux also sets `no_new_privs` and a parent-death kill signal before exec.
+
+Linux applies the complete resource-limit contract. macOS uses the
+same process, descriptor, environment, QuickJS, and IPC boundaries, but its
+kernel may treat address-space and CPU limits less strictly. Executor does not
+claim a macOS Seatbelt profile or container boundary.
+
+This is a capability, crash, and resource boundary for JavaScript, not a
+multi-tenant native-code sandbox. A QuickJS, Oxc, or Rust memory-safety escape
+would still run as the service account on both Linux and macOS. Executor does
+not currently install a Linux seccomp or Landlock policy.
+
+The caller chooses a positive wall deadline of at most five minutes. The same
+deadline drives parent timeout handling and the QuickJS interrupt handler. The
+kernel CPU limit is the deadline rounded up to a whole second, with exact whole
+seconds left unchanged and subsecond deadlines receiving a one-second minimum.
diff --git a/docs/sources.md b/docs/sources.md
new file mode 100644
index 000000000..30ea6370c
--- /dev/null
+++ b/docs/sources.md
@@ -0,0 +1,194 @@
+# Sources, credentials, and managed OAuth
+
+Source management is available only to the signed-in administrator. API tokens
+can discover and call the resulting global catalog, but cannot create sources,
+change credentials, or alter tool modes.
+
+## Before connecting
+
+Open **Sources** in the dashboard. Give each source a clear name and, when
+offered, a stable preferred slug. Tool paths use
+`tools..`.
+
+Outbound connections ignore ambient HTTP proxy variables and do not follow
+tool-call redirects. Public targets are allowed by default. Enable **Allow
+private network addresses** only when the source intentionally runs on
+loopback or a private network. Link-local, cloud metadata, multicast, and
+reserved targets remain blocked.
+
+For a managed OAuth source, that same opt-in also covers issuer discovery, MCP
+protected-resource discovery, authorization metadata, token exchange, and
+refresh endpoints. It can permit loopback HTTP for those OAuth endpoints. Keep
+the opt-in off unless both the source and its OAuth provider intentionally need
+private-network reachability.
+
+Credentials are encrypted with the instance master key and are never shown
+again. Enter secrets in the dashboard, not in endpoint URLs. Static credentials
+take precedence over a managed OAuth connection for the same credential key.
+
+## OpenAPI
+
+1. Choose **OpenAPI service**.
+2. Select **URL** or **Paste**, then provide an OpenAPI 3.0 or 3.1 document in
+ JSON or YAML.
+3. Select **Preview specification** and review the detected operations and
+ security schemes.
+4. Enter any API key, bearer, basic, or manual OAuth access-token credentials.
+5. Import the source, then review its effective tool modes under **Tools**.
+
+Swagger 2 and external references are not supported. For a URL import, the
+document's server information determines the upstream request target. Query
+parameters in a source URL are treated as protected credential state and are
+not shown in source metadata.
+
+Managed OAuth becomes available after import for supported authorization-code
+security schemes. Configure each eligible scheme separately in the source's
+**Managed OAuth** panel.
+
+## GraphQL
+
+1. Choose **GraphQL API**.
+2. Enter the GraphQL endpoint and source name.
+3. Select static authentication when you intend to use it. For managed OAuth,
+ choose **None**.
+4. Connect the source and review its imported queries and mutations.
+
+Introspection must succeed before tools can be imported and on later refreshes.
+With no static credential, a recognized authentication-required response
+stages an empty source so you can configure its managed OAuth `default`
+credential, authorize it, and then refresh the catalog.
+Queries start Enabled, mutations start Ask, and deprecated operations start
+Disabled. Managed OAuth uses the GraphQL source's `default` credential.
+
+## MCP Streamable HTTP
+
+1. Choose **MCP Streamable HTTP**.
+2. Enter the endpoint and source name.
+3. Select no authentication, bearer, basic, API-key header, or a manually
+ managed OAuth access token.
+4. Connect, then review imported tools and their modes.
+
+Executor currently requires MCP protocol version `2025-11-25`. It imports MCP
+tools only. An upstream tool starts Enabled only when the upstream explicitly
+marks it read-only without also marking it destructive. All other imported MCP
+tools start Ask.
+
+For managed OAuth, first create the HTTP MCP source without a static
+credential, then configure its `default` credential in **Managed OAuth**. See
+[MCP support](mcp.md) for transport limits and lifecycle details.
+
+## MCP stdio templates
+
+Dashboard users cannot submit arbitrary commands. A machine administrator must
+approve the executable, arguments, working directory, static environment, and
+names of secret environment fields in a JSON template file. Start Executor
+with:
+
+```sh
+executor server \
+ --mcp-stdio-templates /absolute/path/to/mcp-stdio-templates.json
+```
+
+The equivalent environment variable is
+`EXECUTOR_MCP_STDIO_TEMPLATES_FILE`. The file format is:
+
+```json
+{
+ "templates": [
+ {
+ "name": "filesystem",
+ "executable": "/absolute/path/to/mcp-server",
+ "cwd": "/absolute/path/to/approved-directory",
+ "arguments": ["--stdio"],
+ "environment": {
+ "LOG_LEVEL": "warn"
+ },
+ "secretEnvironment": ["API_TOKEN"]
+ }
+ ]
+}
+```
+
+Restart Executor after editing the registry. In **Sources**, choose **MCP local
+template**, select the approved template, and enter every required secret. The
+template allowlist is a command-selection control, not a sandbox between
+Executor and the selected process. Every approved executable runs with
+Executor's operating-system identity and must be fully trusted.
+
+Protect the template file, executable, working directory, and every ancestor
+path from untrusted writes. Executor canonicalizes and validates configured
+targets when it loads the registry, but it does not enforce their ownership or
+permissions.
+
+The systemd and launchd installers create empty template registries at their
+documented service paths. Docker mounts the repository's empty example by
+default. Container templates must reference Linux executables available at the
+same absolute path inside the container.
+
+## Managed OAuth
+
+Managed OAuth supports eligible OpenAPI authorization-code schemes, the
+GraphQL `default` credential, and the HTTP MCP `default` credential. It uses
+authorization-code flow with PKCE and stores access and refresh tokens
+encrypted. This is OAuth 2 only. OpenID Connect and the `openid` scope are not
+supported.
+
+Before configuring OAuth, start Executor with the exact browser-facing origin:
+
+```sh
+executor server --public-origin https://executor.example.com
+```
+
+Or set:
+
+```sh
+EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com
+```
+
+The value must be an origin only, with no path, query, fragment, or embedded
+credentials. It controls control-mutation Origin checks, MCP Host and Origin
+validation, cookie security, setup links, and OAuth callbacks. Keep Executor
+bound to loopback and terminate TLS at a local reverse proxy.
+
+Then:
+
+1. Import the source.
+2. Open its **Managed OAuth** panel.
+3. Choose the eligible credential and enter the provider issuer, client ID,
+ client authentication method, optional client secret, and requested scopes.
+ For HTTP MCP, provider discovery can begin from the MCP protected resource.
+4. Save the connection.
+5. Copy the callback URL displayed by Executor and register that exact URL with
+ the provider.
+6. Select **Connect OAuth** and finish authorization in the provider window.
+7. Return to the source and refresh its catalog if it was created before
+ authorization completed.
+
+Callback paths are connection-specific:
+
+```text
+/api/v1/oauth/callback/
+```
+
+Do not replace them with one shared callback path. If the provider revokes the
+grant or refresh fails, the dashboard marks the connection for
+reauthorization. Disconnecting removes active token material without deleting
+the source.
+
+## Tool modes and refresh
+
+Each tool has an intrinsic mode, an optional source override, and an optional
+tool override. Effective mode precedence is:
+
+1. Tool override
+2. Source override
+3. Intrinsic mode
+
+Enabled calls run immediately. Ask calls wait for the administrator under
+**Approvals**. Disabled tools stay visible to the administrator but are hidden
+from gateway search and cannot be invoked.
+
+Use **Refresh** after an upstream OpenAPI document, GraphQL schema, or MCP tool
+list changes. A failed refresh leaves the last successfully imported catalog
+in place. Deleting and recreating a source intentionally discards its stable
+tool identity and tombstone history.
diff --git a/docs/systemd.md b/docs/systemd.md
new file mode 100644
index 000000000..fa664d2e8
--- /dev/null
+++ b/docs/systemd.md
@@ -0,0 +1,210 @@
+# Linux systemd service
+
+Executor ships a hardened system service for Linux distributions that use
+systemd. It runs as a dedicated unprivileged `executor` account and listens on
+`127.0.0.1:4788` by default.
+
+## Install
+
+Install or download the Executor release binary, then run its embedded service
+installer:
+
+```sh
+sudo "$(command -v executor)" service install
+```
+
+Using the resolved path matters on first install because `sudo` commonly omits
+the user's `$HOME/.executor/bin` directory from its secure PATH. After the
+managed copy exists at `/usr/local/bin/executor`, ordinary `sudo executor ...`
+commands work.
+
+The release binary contains the hardened installer, unit, environment template,
+and master-key helper. A source checkout is not required.
+
+Use `--no-start` to inspect or customize the installed files before the first
+boot. If Executor is already running, the installer stops it before inspecting
+managed directories or files. `--no-start` deliberately leaves that service
+stopped.
+
+The installer is safe to run again during an upgrade. It replaces the binary
+and unit but never replaces the master key, environment file, or MCP stdio
+template registry. An existing master key must already be a non-symlinked,
+32-byte regular file owned by `executor:executor` with mode `0600` and exactly
+one hard link. A valid key is preserved byte-for-byte. A new key is generated
+inside a private root-only staging directory on the same filesystem. It gets
+its final ownership and mode there before an exact-destination hard link
+atomically publishes it at `master.key`.
+
+If installation or the readiness check fails after the service lifecycle is
+under its control, the installer leaves the service stopped. If systemd cannot
+stop it, the installer reports that explicitly instead of claiming a safe
+state. A private recovery marker distinguishes an interrupted managed-file
+replacement from unrelated files, so a manifest-publication failure can be
+fixed and safely retried. Rerun the installer after correcting the reported
+problem; do not start a partially installed service manually.
+
+The installed paths are:
+
+| Path | Purpose | Ownership and mode |
+| ---------------------------------------- | -------------------------------- | --------------------------- |
+| `/usr/local/bin/executor` | Rust server and CLI binary | `root:root`, `0755` |
+| `/etc/systemd/system/executor.service` | systemd unit | `root:root`, `0644` |
+| `/etc/executor/executor.env` | Non-secret service configuration | `root:executor`, `0640` |
+| `/etc/executor/mcp-stdio-templates.json` | Approved local MCP commands | `root:executor`, `0640` |
+| `/etc/executor/service-install.manifest` | Managed-file ownership hashes | `root:root`, `0600` |
+| `/var/lib/executor` | SQLite state and protected data | `executor:executor`, `0700` |
+| `/var/lib/executor/master.key` | Raw 32-byte instance master key | `executor:executor`, `0600` |
+
+For an external secret-manager key configured with
+`EXECUTOR_MASTER_KEY_FILE`, a root-owned `root:executor` file with mode `0440`
+or `0640` is also supported. Executor opens the file before accepting that
+shape, so the service account must actually have access through its groups.
+Symbolic links, non-regular files, extra hard links, other owners, and broader
+permissions are rejected.
+
+For the first boot with an external key, install without starting, place the
+key, configure its path, then start Executor:
+
+```sh
+sudo "$(command -v executor)" service install --no-start
+sudo install --owner root --group executor --mode 0640 \
+ /secure/source/executor-master.key /etc/executor/external-master.key
+sudoedit /etc/executor/executor.env
+# Set EXECUTOR_MASTER_KEY_FILE=/etc/executor/external-master.key
+sudo executor service start
+```
+
+The unit leaves `--master-key-file` unset, so this environment setting selects
+the external file. Without it, Executor uses `/var/lib/executor/master.key`.
+The example uses persistent protected configuration storage, so the key remains
+available after reboot. A secret manager may use an ephemeral runtime path only
+when its boot-time provisioning unit is ordered before `executor.service`.
+
+Back up the SQLite database and its selected key together. For the default key,
+stop the service and copy the complete `/var/lib/executor` directory, then start
+it again. Include an external key in that same stopped recovery snapshot when
+configured. Losing the selected key makes encrypted credentials and initialized
+instance state unrecoverable. Replacing the key while keeping the database
+makes startup fail closed. The installer refuses to invent a default key for an
+existing database.
+
+## First boot and operation
+
+The one-time setup URL is written to the journal on first boot:
+
+```sh
+sudo journalctl -u executor.service -b
+```
+
+Complete setup in a browser, then create API tokens in the dashboard. Routine
+service commands are:
+
+```sh
+executor service status
+sudo executor service restart
+sudo executor service stop
+sudo executor service start
+sudo journalctl -u executor.service -f
+```
+
+`service status` does not require root. It prints exactly `active` or
+`inactive`, exits 0 for active, and exits 3 for inactive. Operational failures
+exit 1. All mutating Linux service commands require `sudo` and fail before
+running systemctl when invoked without it.
+
+`systemctl stop` sends `SIGTERM`. Executor stops accepting work, cancels owned
+waits, drains upstream MCP processes and background tasks, closes SQLite, and
+then exits. systemd allows 90 seconds for this graceful path before forcing
+termination.
+
+## Public origin and reverse proxies
+
+The default unit is intentionally loopback-only. For a browser-facing reverse
+proxy, keep the bind address on loopback and set the exact external HTTPS
+origin in `/etc/executor/executor.env`:
+
+```sh
+EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com
+```
+
+This origin controls browser origin checks, setup links, and managed OAuth
+callback URLs. It must contain only a scheme, host, and optional port.
+
+Only configure `EXECUTOR_TRUSTED_PROXIES` when Executor must use forwarded
+client addresses. List every proxy CIDR in the chain:
+
+```sh
+EXECUTOR_TRUSTED_PROXIES=127.0.0.1/32,::1/128
+```
+
+Restart the service after changing the environment file. Do not bind plaintext
+HTTP to a non-loopback address. Terminate HTTPS at the local reverse proxy or
+another explicitly trusted transport boundary.
+
+## Local MCP stdio templates
+
+Treat every approved MCP stdio executable as fully trusted with the complete
+Executor instance. It runs under the same operating-system account as Executor
+and can read the master key and database. The service-level sandbox limits what
+the combined service can reach, but it is not a security boundary between the
+Rust server and its stdio children. Do not approve untrusted executables.
+
+The installer creates an empty template registry at
+`/etc/executor/mcp-stdio-templates.json`. Start from
+`packaging/systemd/mcp-stdio-templates.example.json` when approving a local MCP
+server. Executables and working directories must use absolute paths and must be
+readable or executable by the `executor` account. Put secret values in the
+dashboard, not in the template or environment file.
+
+The unit applies `ProtectSystem=strict`, `ProtectHome=yes`, and `PrivateTmp=yes`
+to Executor and its MCP children. A child can read system paths, has private
+temporary directories, and can persist writes only within `/var/lib/executor`
+by default. Home directories are hidden. Grant only the paths a reviewed
+template needs with a systemd drop-in:
+
+```sh
+sudo systemctl edit executor.service
+```
+
+For example:
+
+```ini
+[Service]
+ReadOnlyPaths=/srv/reference-data
+ReadWritePaths=/srv/executor-workspace
+```
+
+Create those paths with ownership and modes appropriate for the `executor`
+account, then run:
+
+```sh
+sudo systemctl daemon-reload
+sudo systemctl restart executor.service
+```
+
+The service-level sandbox is inherited by every MCP stdio child. Do not weaken
+the whole unit when one child needs a narrow filesystem exception. Any
+supplemental groups assigned to the `executor` account are inherited too, so
+review those memberships as part of every template approval.
+
+## Upgrade or remove
+
+To upgrade, run `sudo "$(command -v executor)" service install` from the
+replacement binary. The service is stopped before managed state is inspected
+and started only after the new binary and unit are installed and the master key
+passes validation.
+
+To remove the unit and managed binary while preserving `/etc/executor`,
+`/var/lib/executor`, and the service account:
+
+```sh
+sudo executor service remove
+```
+
+The ownership manifest records hashes for the installed binary and unit.
+Lifecycle mutations fail before touching systemd if either file was replaced
+outside `service install`. Removal deletes the manifest but preserves all other
+configuration and state.
+
+Remove `/etc/executor`, `/var/lib/executor`, and the `executor` account only
+after making and verifying any required backup.
diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md
index a77694871..dc9d6c011 100644
--- a/e2e/AGENTS.md
+++ b/e2e/AGENTS.md
@@ -106,14 +106,20 @@ expect(span.span.tags["executor.tool.outcome"]).toBe("fail");
```sh
cd e2e
-bun run test # boots both dev servers, runs everything
-bun run test:cloud # one target
+bun run test # runs the prepared Rust local-selfhost binary
+bun run test:legacy:cloud # archived cloud target, explicit opt-in
bun run ports # print THIS checkout's derived ports
# attach to an already-running server while iterating (use `bun run ports` URLs):
-E2E_CLOUD_URL=http://127.0.0.1: ../node_modules/.bin/vitest run --project cloud
-E2E_SELFHOST_URL=http://localhost: ../node_modules/.bin/vitest run --project selfhost
+E2E_CLOUD_URL=http://127.0.0.1: ../node_modules/.bin/vitest run --config vitest.legacy.config.ts --project cloud
+E2E_SELFHOST_URL=http://localhost: ../node_modules/.bin/vitest run --config vitest.legacy.config.ts --project selfhost
```
+From the repository root, `bun run test:e2e` is the complete active command:
+it builds the Svelte assets, compiles the Rust binary with those assets, and
+runs only the `local-selfhost` project. Do not use bare `vitest` for the active
+suite. Archived projects live in `vitest.legacy.config.ts` and are reachable
+only through the explicit legacy scripts.
+
Ports are claimed at boot (see `src/ports.ts`): each checkout hashes its repo
root to a preferred block, atomically locks it (a held lock port makes races
impossible), and walks to the next free block if it's locked or squatted — so
diff --git a/e2e/cloud/auth-session.test.ts b/e2e/cloud/auth-session.test.ts
index 7d5e96be9..da9707484 100644
--- a/e2e/cloud/auth-session.test.ts
+++ b/e2e/cloud/auth-session.test.ts
@@ -19,7 +19,7 @@ const setCookieFor = (response: Response, name: string): string => {
};
// state = base64url(JSON { nonce, returnTo? }) — the app's login-state
-// envelope (apps/cloud/src/auth/login-state.ts).
+// envelope (legacy/cloud/src/auth/login-state.ts).
const decodeLoginState = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({ nonce: Schema.String, returnTo: Schema.optional(Schema.String) }),
diff --git a/e2e/cloud/mcp-client-sessions.test.ts b/e2e/cloud/mcp-client-sessions.test.ts
index 43f44eec6..b4eb29f40 100644
--- a/e2e/cloud/mcp-client-sessions.test.ts
+++ b/e2e/cloud/mcp-client-sessions.test.ts
@@ -4,7 +4,7 @@
// session continuity here is real Durable Object state surviving across
// client connections, not a stub.
//
-// Ported from apps/cloud/src/mcp-miniflare.e2e.node.test.ts (unstable_dev +
+// Ported from legacy/cloud/src/mcp-miniflare.e2e.node.test.ts (unstable_dev +
// test-seam bearers) onto the e2e dev server with real OAuth bearers.
// Telemetry-span assertions from that file required injecting an OTLP
// receiver into the worker env and were NOT carried (not black-box).
diff --git a/e2e/cloud/mcp-protocol.test.ts b/e2e/cloud/mcp-protocol.test.ts
index df81e067d..edff45420 100644
--- a/e2e/cloud/mcp-protocol.test.ts
+++ b/e2e/cloud/mcp-protocol.test.ts
@@ -5,7 +5,7 @@
// real McpSessionDO, and real bearers minted from the authorization server the
// product itself advertises (discovery → DCR → authorize → token).
//
-// Ported from apps/cloud/src/mcp-flow.test.ts (workerd-pool SELF.fetch with
+// Ported from legacy/cloud/src/mcp-flow.test.ts (workerd-pool SELF.fetch with
// test-seam bearers). DO-internal coverage from that file (forced runtime
// eviction, idle-alarm firing, alarm scheduling, storage seeding) is clock /
// internals dependent and intentionally NOT carried — only black-box
diff --git a/e2e/cloud/oauth-connections.test.ts b/e2e/cloud/oauth-connections.test.ts
index ff0a9c4d8..6d02c5261 100644
--- a/e2e/cloud/oauth-connections.test.ts
+++ b/e2e/cloud/oauth-connections.test.ts
@@ -5,7 +5,7 @@
// product API while a real OAuth authorization server runs inside the scenario
// on 127.0.0.1 (the dev server exchanges the code against it directly).
//
-// Ported from apps/cloud/src/mcp/mcp-oauth.node.test.ts, extended to cover
+// Ported from legacy/cloud/src/mcp/mcp-oauth.node.test.ts, extended to cover
// `complete` (the original stopped at the redirect).
import { randomBytes } from "node:crypto";
diff --git a/e2e/cloud/sources-api.test.ts b/e2e/cloud/sources-api.test.ts
index 785fe3d09..f43365287 100644
--- a/e2e/cloud/sources-api.test.ts
+++ b/e2e/cloud/sources-api.test.ts
@@ -6,7 +6,7 @@
// execution proves the whole chain: catalog row → connection → stamped tool →
// QuickJS execution → live upstream request.
//
-// Ported from apps/cloud/src/api/sources-api.node.test.ts. Cross-user
+// Ported from legacy/cloud/src/api/sources-api.node.test.ts. Cross-user
// isolation of personal connections (alice/bob in one org) is NOT covered
// here: minting a second member of an existing org has no public API surface.
import { randomBytes } from "node:crypto";
diff --git a/e2e/cloud/sources-refresh.test.ts b/e2e/cloud/sources-refresh.test.ts
index fe2b70afd..2a736a291 100644
--- a/e2e/cloud/sources-refresh.test.ts
+++ b/e2e/cloud/sources-refresh.test.ts
@@ -5,7 +5,7 @@
// whether the catalog row can be refreshed at all: a spec registered from a
// URL can be re-fetched, a pasted blob cannot.
//
-// Ported from apps/cloud/src/api/sources-refresh.node.test.ts. The upstream
+// Ported from legacy/cloud/src/api/sources-refresh.node.test.ts. The upstream
// MCP server is a real HTTP server started inside the scenario on 127.0.0.1.
import { randomBytes } from "node:crypto";
import { createServer } from "node:http";
diff --git a/e2e/cloud/unauthenticated-skeleton.test.ts b/e2e/cloud/unauthenticated-skeleton.test.ts
index 77005df21..b940ccd21 100644
--- a/e2e/cloud/unauthenticated-skeleton.test.ts
+++ b/e2e/cloud/unauthenticated-skeleton.test.ts
@@ -4,7 +4,7 @@
// AuthGate used to SSR the AUTHENTICATED app-shell skeleton (sidebar + card
// grid) for every visitor and only swap to a login page after a client-side
// `/account/me` 401 — signed-out users were shown an app they'd never reach.
-// Now the SSR auth gate (apps/cloud/src/auth/ssr-gate.ts) verifies the sealed
+// Now the SSR auth gate (legacy/cloud/src/auth/ssr-gate.ts) verifies the sealed
// session cookie in the worker and 302s signed-out document requests to
// /login (carrying ?returnTo=), so the app shell never exists for them.
import { expect } from "@effect/vitest";
diff --git a/e2e/desktop/local-auth-mcp.test.ts b/e2e/desktop/local-auth-mcp.test.ts
index a0ca14ee5..99f3919ca 100644
--- a/e2e/desktop/local-auth-mcp.test.ts
+++ b/e2e/desktop/local-auth-mcp.test.ts
@@ -33,7 +33,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import { scenario } from "../src/scenario";
import { RunDir } from "../src/services";
-const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
+const appDir = fileURLToPath(new URL("../../legacy/desktop/", import.meta.url));
const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string;
const APPROVAL_TARGET_TOOL = "executor.coreTools.policies.list";
diff --git a/e2e/desktop/reset-state.test.ts b/e2e/desktop/reset-state.test.ts
index 534b28401..f778ab5a7 100644
--- a/e2e/desktop/reset-state.test.ts
+++ b/e2e/desktop/reset-state.test.ts
@@ -19,7 +19,7 @@ import { _electron } from "playwright";
import { scenario } from "../src/scenario";
import { RunDir } from "../src/services";
-const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
+const appDir = fileURLToPath(new URL("../../legacy/desktop/", import.meta.url));
const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string;
const CORRUPT_MARKER = "executor-e2e-corrupted-db";
diff --git a/e2e/desktop/sidecar-crash-screen.test.ts b/e2e/desktop/sidecar-crash-screen.test.ts
index 66433064c..9872f42ec 100644
--- a/e2e/desktop/sidecar-crash-screen.test.ts
+++ b/e2e/desktop/sidecar-crash-screen.test.ts
@@ -20,10 +20,10 @@ import { _electron } from "playwright";
import { scenario } from "../src/scenario";
import { RunDir } from "../src/services";
-const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
+const appDir = fileURLToPath(new URL("../../legacy/desktop/", import.meta.url));
// require("electron") resolves to the binary path (electron's index.js
-// exports it) — resolved from apps/desktop so we get the app's pinned
+// exports it), resolved from legacy/desktop so we get the app's pinned
// version out of the workspace store.
const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string;
diff --git a/e2e/desktop/traffic-light-overlap.test.ts b/e2e/desktop/traffic-light-overlap.test.ts
index 7d05cc9ac..db7c28ccf 100644
--- a/e2e/desktop/traffic-light-overlap.test.ts
+++ b/e2e/desktop/traffic-light-overlap.test.ts
@@ -1,6 +1,6 @@
// Regression for issue #1125: on the macOS desktop app the native traffic-light
// window controls are drawn over the frameless web content at
-// trafficLightPosition {x:16,y:17} (apps/desktop/src/main/index.ts) — three
+// trafficLightPosition {x:16,y:17} (legacy/desktop/src/main/index.ts) — three
// 12px buttons with 20px center spacing, occupying x ∈ [16, 68].
//
// The original bug: the window minWidth was 720 but the web layout switches to
@@ -40,7 +40,7 @@ import { _electron, type ElectronApplication, type Locator, type Page } from "pl
import { scenario } from "../src/scenario";
import { RunDir } from "../src/services";
-const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
+const appDir = fileURLToPath(new URL("../../legacy/desktop/", import.meta.url));
const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string;
// Rightmost edge of the traffic-light cluster (left edges 16/36/56, +12px) plus
diff --git a/e2e/fixtures/stdio-mcp-server.mjs b/e2e/fixtures/stdio-mcp-server.mjs
new file mode 100644
index 000000000..ed950db09
--- /dev/null
+++ b/e2e/fixtures/stdio-mcp-server.mjs
@@ -0,0 +1,29 @@
+import { createHash } from "node:crypto";
+
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+
+const server = new McpServer(
+ { name: "executor-e2e-stdio", version: "1.0.0" },
+ { capabilities: {} },
+);
+
+server.registerTool(
+ "secret_status",
+ {
+ description: "Reports whether Executor supplied the configured template secret",
+ inputSchema: {},
+ },
+ async () => ({
+ content: [
+ {
+ type: "text",
+ text: `secret-sha256:${createHash("sha256")
+ .update(process.env.EXECUTOR_E2E_STDIO_SECRET ?? "")
+ .digest("hex")}`,
+ },
+ ],
+ }),
+);
+
+await server.connect(new StdioServerTransport());
diff --git a/e2e/local-selfhost/core-journeys.test.ts b/e2e/local-selfhost/core-journeys.test.ts
new file mode 100644
index 000000000..040308768
--- /dev/null
+++ b/e2e/local-selfhost/core-journeys.test.ts
@@ -0,0 +1,2657 @@
+import { createHash, randomBytes, randomUUID } from "node:crypto";
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+import { expect } from "@effect/vitest";
+import { createEmulator, type Emulator, type LedgerEntry } from "@executor-js/emulate";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+import {
+ makeGreetingGraphqlSchema,
+ serveGraphqlTestServer,
+} from "@executor-js/plugin-graphql/testing";
+import { makeGreetingMcpServer, serveMcpServer } from "@executor-js/plugin-mcp/testing";
+import { serveOpenApiEchoTestServer } from "@executor-js/plugin-openapi/testing";
+import { Effect, Predicate } from "effect";
+import type { Page } from "playwright";
+
+import { scenario } from "../src/scenario";
+import { LocalAdminClient, type LocalSource, type LocalToken } from "../src/local-admin";
+import {
+ expectLocatorChecked,
+ expectLocatorCount,
+ expectLocatorDisabled,
+ expectLocatorFocused,
+ expectLocatorText,
+ expectLocatorValue,
+ expectLocatorVisible,
+} from "../src/locator-assertions";
+import { approveOAuthTestAuthorization, serveOAuthTestProvider } from "../src/oauth-test-provider";
+import { e2ePort } from "../src/ports";
+import { Browser, Target } from "../src/services";
+import { openConnectSourcePanel } from "../src/source-panel";
+import { LOCAL_ADMIN, LOCAL_SETUP_BASE_URL } from "../targets/local-selfhost";
+
+const unique = (prefix: string) => `${prefix}-${randomBytes(4).toString("hex")}`;
+const executeFile = promisify(execFile);
+const emulatorPortA = e2ePort("E2E_LOCAL_EMULATOR_A_PORT", 6);
+const emulatorPortB = e2ePort("E2E_LOCAL_EMULATOR_B_PORT", 7);
+const emulatorPortC = e2ePort("E2E_LOCAL_EMULATOR_C_PORT", 8);
+const sourceCreateStorageKey = "executor.source-create.idempotency-key.v1";
+
+const sourceCreateStorageValue = (page: Page) =>
+ page.evaluate((key) => sessionStorage.getItem(key), sourceCreateStorageKey);
+
+const isSuccessfulMcpToolCall = (entry: LedgerEntry) => {
+ const body = entry.request.body;
+ return (
+ entry.path === "/mcp" &&
+ entry.response.status === 200 &&
+ typeof body === "object" &&
+ body !== null &&
+ "method" in body &&
+ body.method === "tools/call"
+ );
+};
+
+const adminClient = (baseUrl: string) =>
+ Effect.promise(() =>
+ LocalAdminClient.signIn(baseUrl, LOCAL_ADMIN.username, LOCAL_ADMIN.password),
+ );
+
+const isLocalToken = (value: unknown): value is LocalToken =>
+ typeof value === "object" &&
+ value !== null &&
+ "id" in value &&
+ typeof value.id === "string" &&
+ "token" in value &&
+ typeof value.token === "string";
+
+const resendEmulator = Effect.acquireRelease(
+ Effect.promise(() => createEmulator({ service: "resend", port: emulatorPortA })),
+ (emulator: Emulator) => Effect.promise(() => emulator.close()).pipe(Effect.ignore),
+);
+
+const githubEmulator = Effect.acquireRelease(
+ Effect.promise(() => createEmulator({ service: "github", port: emulatorPortA })),
+ (emulator: Emulator) => Effect.promise(() => emulator.close()).pipe(Effect.ignore),
+);
+
+const deleteSources = (client: LocalAdminClient, sources: readonly LocalSource[]) =>
+ Effect.forEach(sources, (source) =>
+ Effect.promise(() => client.deleteSource(source.id)).pipe(Effect.ignore),
+ ).pipe(Effect.asVoid);
+
+const deleteSourcesNamed = (client: LocalAdminClient, names: readonly string[]) =>
+ Effect.gen(function* () {
+ const sources = yield* Effect.promise(() => client.listSources());
+ yield* deleteSources(
+ client,
+ sources.sources.filter((source) => names.includes(source.displayName)),
+ );
+ });
+
+const acquireToken = (client: LocalAdminClient, baseUrl: string, name: string) =>
+ Effect.acquireRelease(
+ Effect.promise(() => client.createToken(name)),
+ (token) =>
+ Effect.gen(function* () {
+ yield* Effect.promise(() =>
+ client.revokeToken(token.id).then(
+ () => undefined,
+ () => undefined,
+ ),
+ );
+ const gatewayStatus = yield* Effect.promise(() =>
+ fetch(new URL("/api/v1/gateway/tools/invoke", baseUrl), {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ path: "tools.revoked.probe", arguments: {} }),
+ }).then(
+ (response) => response.status,
+ () => 0,
+ ),
+ );
+ if (gatewayStatus !== 401) {
+ return yield* Effect.die(`token cleanup left ${token.id} authorized`);
+ }
+ }),
+ );
+
+const prepareInlineOpenApiSource = async (page: Page, document: string, displayName: string) => {
+ await page.goto("/sources");
+ await openConnectSourcePanel(page);
+ await page.getByRole("group", { name: "Source type" }).getByLabel("OpenAPI service").check();
+ await page.getByLabel("Paste document").check();
+ await page.getByLabel("OpenAPI JSON or YAML").fill(document);
+ await page.getByLabel("Allow private network addresses for this source").check();
+ await page.getByRole("button", { name: "Preview tools" }).click();
+ await expectLocatorVisible(page.getByLabel("Source name"));
+ await page.getByLabel("Source name").fill(displayName);
+};
+
+const minimalOpenApiDocument = (origin: string, title: string) =>
+ JSON.stringify({
+ openapi: "3.1.0",
+ info: { title, version: "1.0.0" },
+ servers: [{ url: origin }],
+ paths: {
+ "/zen": {
+ get: {
+ operationId: "readZen",
+ summary: "Read Zen",
+ responses: { "200": { description: "OK" } },
+ },
+ },
+ },
+ });
+
+const staticAuthOpenApiDocument = (githubOrigin: string, spotifyOrigin: string) =>
+ JSON.stringify({
+ openapi: "3.1.0",
+ info: { title: "Executor authentication evidence", version: "1.0.0" },
+ servers: [{ url: githubOrigin }],
+ components: {
+ securitySchemes: {
+ basicAuth: { type: "http", scheme: "basic" },
+ apiKeyAuth: { type: "apiKey", in: "header", name: "X-Executor-E2E-Key" },
+ manualOAuth: {
+ type: "oauth2",
+ flows: {
+ authorizationCode: {
+ authorizationUrl: `${githubOrigin}/login/oauth/authorize`,
+ tokenUrl: `${githubOrigin}/login/oauth/access_token`,
+ scopes: { repo: "Read repositories" },
+ },
+ },
+ },
+ },
+ },
+ paths: {
+ "/api/token": {
+ post: {
+ operationId: "exchangeClientCredentialsWithBasic",
+ summary: "Exchange client credentials with Basic",
+ servers: [{ url: spotifyOrigin }],
+ security: [{ basicAuth: [] }],
+ requestBody: {
+ required: true,
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ properties: {
+ grant_type: { type: "string", enum: ["client_credentials"] },
+ },
+ required: ["grant_type"],
+ additionalProperties: false,
+ },
+ },
+ },
+ },
+ responses: { "200": { description: "OK" } },
+ },
+ },
+ "/meta": {
+ get: {
+ operationId: "readMetaWithApiKey",
+ summary: "Read metadata with API key",
+ security: [{ apiKeyAuth: [] }],
+ responses: { "200": { description: "OK" } },
+ },
+ },
+ "/user": {
+ get: {
+ operationId: "readCurrentUserWithManualToken",
+ summary: "Read current user with manual token",
+ security: [{ manualOAuth: ["repo"] }],
+ responses: { "200": { description: "OK" } },
+ },
+ },
+ },
+ });
+
+const registerMcpOAuthClient = async (issuer: string, redirectUri: string) => {
+ const response = await fetch(`${issuer}/register`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ client_name: "Executor browser e2e",
+ redirect_uris: [redirectUri],
+ grant_types: ["authorization_code"],
+ response_types: ["code"],
+ token_endpoint_auth_method: "none",
+ }),
+ });
+ if (!response.ok) throw new Error(`MCP emulator client registration failed (${response.status})`);
+ const registration = (await response.json()) as { readonly client_id?: string };
+ if (!registration.client_id) {
+ throw new Error("MCP emulator client registration returned no client_id");
+ }
+ return registration.client_id;
+};
+
+scenario(
+ "First boot · the one-time setup link creates the only administrator",
+ {},
+ Effect.gen(function* () {
+ const browser = yield* Browser;
+ const setupToken = process.env.E2E_LOCAL_SETUP_TOKEN;
+ if (!setupToken) return yield* Effect.die("the fresh setup instance did not publish a token");
+
+ yield* browser.privateSession({ label: "first-boot administrator" }, async ({ page, step }) => {
+ await step("Open the one-time setup link and create the administrator", async () => {
+ await page.goto(`${LOCAL_SETUP_BASE_URL}/setup#token=${encodeURIComponent(setupToken)}`);
+ await expect.poll(() => new URL(page.url()).hash).toBe("");
+ await page.getByLabel("Administrator username").fill("first-boot-admin");
+ await page.getByLabel(/^Password\b/).fill("first-boot-password-123");
+ await page.getByLabel("Confirm password").fill("first-boot-password-123");
+ await page.getByRole("button", { name: "Create administrator" }).click();
+ await page.waitForURL((url) => url.pathname === "/sources");
+ await expectLocatorVisible(page.getByRole("heading", { name: "Sources" }));
+ });
+ });
+ const replay = yield* Effect.promise(() =>
+ fetch(new URL("/api/v1/setup", LOCAL_SETUP_BASE_URL), {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ origin: new URL(LOCAL_SETUP_BASE_URL).origin,
+ },
+ body: JSON.stringify({
+ setupToken,
+ username: "second-admin",
+ password: "second-admin-password-123",
+ }),
+ }),
+ );
+ expect(replay.status, "the consumed setup token cannot create another administrator").toBe(409);
+ }),
+);
+
+scenario(
+ "Administrator access · sign-in creates a gateway token without exposing it twice",
+ {},
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const tokenNames = [unique("E2E local agent"), unique("E2E local agent")] as const;
+ const createdTokens: Array = [];
+ const tokenCreationGates = tokenNames.map(() => {
+ let release = () => {};
+ const wait = new Promise((resolve) => {
+ release = resolve;
+ });
+ let requestStarted = false;
+ let markStarted = () => {};
+ const started = new Promise((resolve) => {
+ markStarted = resolve;
+ });
+ let markSettled = () => {};
+ const settled = new Promise((resolve) => {
+ markSettled = resolve;
+ });
+ return {
+ wait,
+ started,
+ settled,
+ release: () => release(),
+ markStarted: () => {
+ requestStarted = true;
+ markStarted();
+ },
+ markSettled: () => markSettled(),
+ hasStarted: () => requestStarted,
+ };
+ });
+
+ expect(tokenNames[0], "sequential runs use different exact names").not.toBe(tokenNames[1]);
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ yield* browser.privateSession(
+ { label: "signed-out administrator" },
+ async ({ page, step }) => {
+ const sessionDeletes: string[] = [];
+ let tokenCreationIndex = 0;
+ let unexpectedTokenPosts = 0;
+
+ page.on("request", (request) => {
+ const url = new URL(request.url());
+ if (request.method() === "DELETE" && url.pathname === "/api/v1/session") {
+ sessionDeletes.push(request.url());
+ }
+ });
+ await page.route("**/api/v1/tokens", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ const index = tokenCreationIndex++;
+ const gate = tokenCreationGates[index];
+ if (gate === undefined) {
+ unexpectedTokenPosts += 1;
+ await route.abort("failed");
+ return;
+ }
+ gate.markStarted();
+ try {
+ await gate.wait;
+ const response = await route.fetch();
+ const body: unknown = await response.json();
+ if (!isLocalToken(body)) {
+ throw new Error("token creation returned an invalid response");
+ }
+ createdTokens[index] = body;
+ expect(response.status(), "the token API created one credential").toBe(201);
+ await route.fulfill({ response });
+ } finally {
+ gate.markSettled();
+ }
+ });
+
+ await step("Sign in and open gateway token management", async () => {
+ await page.goto("/login");
+ await page.getByLabel("Username").fill(LOCAL_ADMIN.username);
+ await page.getByLabel("Password").fill(LOCAL_ADMIN.password);
+ await page.getByRole("button", { name: "Sign in" }).click();
+ await page.waitForURL((url) => url.pathname === "/sources");
+ await page.goto("/tokens");
+ await expectLocatorVisible(page.getByRole("heading", { name: "API tokens" }));
+ });
+
+ const createAndDismissToken = async (index: number) => {
+ const tokenName = tokenNames[index];
+ const gate = tokenCreationGates[index];
+ if (tokenName === undefined || gate === undefined) {
+ throw new Error("token creation run is missing its unique fixture");
+ }
+ await page.getByLabel("Token name").fill(tokenName);
+ try {
+ await page.getByRole("button", { name: "Create token" }).click();
+ await gate.started;
+ await expectLocatorDisabled(
+ page.getByRole("button", { name: "Creating token..." }),
+ );
+ await page.getByRole("button", { name: "Sign out" }).click();
+ await expectLocatorVisible(
+ page.getByRole("status").filter({
+ hasText:
+ "Wait for token creation to finish before leaving this page or signing out.",
+ }),
+ );
+ expect(
+ sessionDeletes,
+ "blocked sign-out does not destroy the administrator session",
+ ).toHaveLength(0);
+ expect(
+ new URL(page.url()).pathname,
+ "the guarded page stays open during creation",
+ ).toBe("/tokens");
+ } finally {
+ gate.release();
+ if (gate.hasStarted()) await gate.settled;
+ }
+
+ await expectLocatorVisible(page.getByLabel("API token"));
+ const secret = await page.getByLabel("API token").inputValue();
+ const created = createdTokens[index];
+ if (created === undefined) throw new Error("created token ID was not captured");
+ expect(secret.slice(0, 4), "the revealed token has the gateway prefix").toBe("exr_");
+ expect(
+ createHash("sha256").update(secret).digest("hex"),
+ "the one-time reveal matches the captured response without logging the secret",
+ ).toBe(createHash("sha256").update(created.token).digest("hex"));
+ expect(
+ created.id.length,
+ "the exact created token ID is retained for cleanup",
+ ).toBeGreaterThan(0);
+
+ await page.getByRole("button", { name: "Sign out" }).click();
+ await expectLocatorVisible(
+ page.getByRole("status").filter({
+ hasText:
+ "Save the token and choose “I saved it” before leaving this page or signing out.",
+ }),
+ );
+ expect(sessionDeletes, "the unsaved reveal blocks session deletion too").toHaveLength(
+ 0,
+ );
+ expect(
+ new URL(page.url()).pathname,
+ "the revealed secret remains on the guarded page",
+ ).toBe("/tokens");
+ await page.getByRole("button", { name: "I saved it" }).click();
+ await expectLocatorCount(page.getByLabel("API token"), 0);
+ };
+
+ await step("Create and safely dismiss the first unique token", async () => {
+ await createAndDismissToken(0);
+ });
+ await step("Create and safely dismiss the second unique token", async () => {
+ await createAndDismissToken(1);
+ });
+ await step("Sign out only after both one-time reveals are gone", async () => {
+ const sessionDelete = page.waitForRequest((request) => {
+ const url = new URL(request.url());
+ return request.method() === "DELETE" && url.pathname === "/api/v1/session";
+ });
+ await page.getByRole("button", { name: "Sign out" }).click();
+ await sessionDelete;
+ await page.waitForURL((url) => url.pathname === "/login");
+ expect(
+ sessionDeletes,
+ "acknowledgement permits exactly one session deletion",
+ ).toHaveLength(1);
+ expect(unexpectedTokenPosts, "two runs issued exactly two token POSTs").toBe(0);
+ });
+ },
+ );
+
+ expect(
+ createdTokens.filter(Predicate.isNotUndefined),
+ "both sequential runs captured their exact token IDs",
+ ).toHaveLength(2);
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ try {
+ await step("See only masked metadata for both exact unique token rows", async () => {
+ await page.goto("/tokens");
+ for (const tokenName of tokenNames) {
+ const exactName = page.getByText(tokenName, { exact: true });
+ const row = page.locator("tbody tr").filter({ has: exactName });
+ await expectLocatorCount(row, 1, `${tokenName} selects one repeat-safe row`);
+ await expectLocatorText(row.locator('td[data-label="Status"]'), "Active");
+ await expectLocatorText(row.locator('td[data-label="Token"] code'), "...");
+ }
+ });
+ await step("Revoke one exact token from the dashboard", async () => {
+ const tokenName = tokenNames[0];
+ const token = createdTokens[0];
+ if (token === undefined) throw new Error("the first created token was not captured");
+ const row = page.locator("tbody tr").filter({
+ has: page.getByText(tokenName, { exact: true }),
+ });
+ await row.getByRole("button", { name: `Revoke ${tokenName}` }).click();
+ const confirmation = page.getByRole("dialog", {
+ name: `Stop using ${tokenName}?`,
+ });
+ await expectLocatorVisible(confirmation);
+ await confirmation.getByRole("button", { name: "Revoke token" }).click();
+ await expectLocatorText(row.locator('td[data-label="Status"]'), "Revoked");
+
+ const rejected = await fetch(
+ new URL("/api/v1/gateway/tools/invoke", target.baseUrl),
+ {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ path: "tools.revoked.probe", arguments: {} }),
+ },
+ );
+ expect(rejected.status, "dashboard revocation immediately rejects the token").toBe(
+ 401,
+ );
+ });
+ } finally {
+ await step("End the recorded administrator session", async () => {
+ const csrf = identity.cookies?.find(
+ (cookie) => cookie.name === "executor_csrf",
+ )?.value;
+ if (csrf === undefined) {
+ throw new Error("the recorded administrator identity has no CSRF cookie");
+ }
+ const response = await fetch(new URL("/api/v1/session", target.baseUrl), {
+ method: "DELETE",
+ headers: {
+ ...identity.headers,
+ origin: new URL(target.baseUrl).origin,
+ "x-executor-csrf": csrf,
+ },
+ });
+ expect(
+ response.status,
+ "the traced administrator cookie is invalidated before artifacts close",
+ ).toBe(204);
+ await page.goto("/login");
+ await expectLocatorVisible(page.getByLabel("Username"));
+ });
+ }
+ });
+ }),
+ Effect.gen(function* () {
+ for (const gate of tokenCreationGates) gate.release();
+ yield* Effect.promise(() =>
+ Promise.all(
+ tokenCreationGates.filter((gate) => gate.hasStarted()).map((gate) => gate.settled),
+ ),
+ );
+ const captured = createdTokens.filter(Predicate.isNotUndefined);
+ const outcomes = yield* Effect.forEach(
+ captured,
+ (token) =>
+ Effect.promise(async () => {
+ await client.revokeToken(token.id).then(
+ () => undefined,
+ () => undefined,
+ );
+ const gatewayStatus = await fetch(
+ new URL("/api/v1/gateway/tools/invoke", target.baseUrl),
+ {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ path: "tools.revoked.probe", arguments: {} }),
+ },
+ ).then(
+ (response) => response.status,
+ () => 0,
+ );
+ return { id: token.id, gatewayStatus };
+ }),
+ { concurrency: "unbounded" },
+ );
+ createdTokens.length = 0;
+ for (const outcome of outcomes) {
+ expect(
+ outcome.gatewayStatus,
+ `captured token ${outcome.id} is revoked and unauthorized`,
+ ).toBe(401);
+ }
+ }),
+ );
+ }),
+);
+
+scenario(
+ "Sources · OpenAPI, GraphQL, and MCP join one local catalog",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const resend = yield* resendEmulator;
+ const graphql = yield* serveGraphqlTestServer({
+ schema: makeGreetingGraphqlSchema({ includeMutation: false }),
+ });
+ const mcp = yield* serveMcpServer(() =>
+ makeGreetingMcpServer({ name: unique("mcp"), text: "hello from local MCP" }),
+ );
+ const suffix = randomBytes(3).toString("hex");
+ const created: LocalSource[] = [];
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ created.push(
+ yield* Effect.promise(() =>
+ client.createSource({
+ kind: "openapi",
+ displayName: `Resend emulator ${suffix}`,
+ spec: { type: "url", url: resend.openapiUrl },
+ allowPrivateNetwork: true,
+ }),
+ ),
+ );
+ created.push(
+ yield* Effect.promise(() =>
+ client.createSource({
+ kind: "graphql",
+ displayName: `GraphQL greeting ${suffix}`,
+ endpoint: graphql.endpoint,
+ allowPrivateNetwork: true,
+ }),
+ ),
+ );
+ created.push(
+ yield* Effect.promise(() =>
+ client.createSource({
+ kind: "mcp_http",
+ displayName: `MCP greeting ${suffix}`,
+ endpoint: mcp.endpoint,
+ allowPrivateNetwork: true,
+ }),
+ ),
+ );
+
+ const token = yield* acquireToken(client, target.baseUrl, unique("source-agent"));
+ for (const source of created.slice(1)) {
+ const catalog = yield* Effect.promise(() => client.listTools({ sourceId: source.id }));
+ expect(
+ catalog.items.length,
+ `${source.displayName} discovers at least one tool`,
+ ).toBeGreaterThan(0);
+ const enabled = yield* Effect.promise(() =>
+ client.setToolMode(catalog.items[0]!, "enabled"),
+ );
+ const argumentsForTool = source.id === created[1]?.id ? { name: "Ada" } : {};
+ const invoked = yield* Effect.promise(() =>
+ fetch(new URL("/api/v1/gateway/tools/invoke", target.baseUrl), {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ path: enabled.callablePath, arguments: argumentsForTool }),
+ }),
+ );
+ expect(invoked.status, `${source.displayName} executes through the gateway`).toBe(200);
+ }
+ const graphqlRequests = yield* graphql.requests;
+ expect(
+ graphqlRequests.filter((request) => !request.payload.query?.includes("__schema")),
+ "the GraphQL resolver received a non-introspection call",
+ ).not.toHaveLength(0);
+ const mcpRequests = yield* mcp.requests;
+ expect(
+ mcpRequests.length,
+ "the MCP server received discovery and invocation traffic",
+ ).toBeGreaterThanOrEqual(2);
+
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Open Sources and see every supported protocol", async () => {
+ await page.goto("/sources");
+ const sourceProtocols = [
+ [created[0], /^OpenAPI · /],
+ [created[1], /^GraphQL · /],
+ [created[2], /^MCP HTTP · /],
+ ] as const;
+ for (const [source, protocol] of sourceProtocols) {
+ if (source === undefined) {
+ throw new Error("the protocol source fixture was not created");
+ }
+ const card = page.getByRole("article", {
+ name: source.displayName,
+ exact: true,
+ });
+ const label = card.getByText(protocol);
+ await label.scrollIntoViewIfNeeded();
+ await expectLocatorVisible(label);
+ }
+ });
+ });
+ }),
+ deleteSources(client, created),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Sources dashboard · every protocol, static auth family, and managed OAuth work end to end",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const githubToken = unique("github-token");
+ const mcpToken = unique("mcp-token");
+ const spotifyClientId = unique("spotify-client");
+ const basicPassword = unique("basic-password");
+ const apiKey = unique("api-key");
+ const stdioSecret = unique("stdio-secret");
+ const stdioSecretDigest = createHash("sha256").update(stdioSecret).digest("hex");
+ const github = yield* Effect.acquireRelease(
+ Effect.promise(() =>
+ createEmulator({
+ service: "github",
+ port: emulatorPortA,
+ seed: {
+ tokens: { [githubToken]: { login: "octocat", scopes: ["repo"] } },
+ github: {
+ users: [{ login: "octocat", name: "The Octocat", email: "octocat@example.test" }],
+ },
+ },
+ }),
+ ),
+ (emulator) => Effect.promise(() => emulator.close()).pipe(Effect.ignore),
+ );
+ const staticMcp = yield* Effect.acquireRelease(
+ Effect.promise(() =>
+ createEmulator({
+ service: "mcp",
+ port: emulatorPortB,
+ baseUrl: `http://127.0.0.1:${emulatorPortB}`,
+ seed: {
+ tokens: { [mcpToken]: { login: "admin", scopes: ["repo", "read:user"] } },
+ },
+ }),
+ ),
+ (emulator) => Effect.promise(() => emulator.close()).pipe(Effect.ignore),
+ );
+ const spotify = yield* Effect.acquireRelease(
+ Effect.promise(() =>
+ createEmulator({
+ service: "spotify",
+ port: emulatorPortC,
+ seed: {
+ spotify: {
+ clients: [
+ {
+ client_id: spotifyClientId,
+ client_secret: basicPassword,
+ name: "Executor browser e2e",
+ },
+ ],
+ },
+ },
+ }),
+ ),
+ (emulator) => Effect.promise(() => emulator.close()).pipe(Effect.ignore),
+ );
+ const provider = {
+ issuer: staticMcp.url,
+ endpoint: `${staticMcp.url}/mcp`,
+ registerClient: (redirectUri: string) => registerMcpOAuthClient(staticMcp.url, redirectUri),
+ approveAuthorization: (authorizationUrl: string, login: string) =>
+ approveOAuthTestAuthorization(staticMcp.url, authorizationUrl, login),
+ ledger: () => staticMcp.ledger.list(),
+ };
+ const suffix = randomBytes(3).toString("hex");
+ const names = {
+ openapi: `Browser OpenAPI ${suffix}`,
+ graphql: `Browser GraphQL ${suffix}`,
+ mcpHttp: `Browser MCP HTTP ${suffix}`,
+ mcpStdio: `Browser MCP stdio ${suffix}`,
+ managedOAuth: `Browser managed OAuth ${suffix}`,
+ } as const;
+ const createdNames = Object.values(names);
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.privateSession(identity, async ({ page, step }) => {
+ await step("Import OpenAPI with Basic, API key, and a manual OAuth token", async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ staticAuthOpenApiDocument(github.url, spotify.url),
+ names.openapi,
+ );
+ await page.getByRole("checkbox", { name: /basicAuth/ }).check();
+ await page.getByLabel("Username").fill(spotifyClientId);
+ await page.getByLabel("Basic auth", { exact: true }).fill(basicPassword);
+ await page.getByRole("checkbox", { name: /apiKeyAuth/ }).check();
+ await page.getByLabel("API key", { exact: true }).fill(apiKey);
+ await page.getByRole("checkbox", { name: /manualOAuth/ }).check();
+ await page
+ .getByLabel("OAuth access token (manual, advanced)", { exact: true })
+ .fill(githubToken);
+ await page.getByRole("button", { name: "Import source" }).click();
+ await expectLocatorVisible(page.getByRole("heading", { name: names.openapi }));
+ });
+
+ await step("Connect GraphQL with a bearer token", async () => {
+ await page.goto("/sources");
+ await openConnectSourcePanel(page);
+ await page
+ .getByRole("group", { name: "Source type" })
+ .getByLabel("GraphQL API")
+ .check();
+ await page.getByLabel("Endpoint").fill(`${github.url}/graphql`);
+ await page.getByLabel("Source name").fill(names.graphql);
+ await page.getByLabel("Allow private network addresses for this source").check();
+ await page.getByLabel("Method").selectOption("bearer");
+ await page.getByLabel("Bearer token", { exact: true }).fill(githubToken);
+ await page.getByRole("button", { name: "Connect source" }).click();
+ await expectLocatorVisible(page.getByRole("heading", { name: names.graphql }));
+ });
+
+ await step("Connect MCP HTTP with a manually managed access token", async () => {
+ await page.goto("/sources");
+ await openConnectSourcePanel(page);
+ await page
+ .getByRole("group", { name: "Source type" })
+ .getByLabel("MCP over HTTP")
+ .check();
+ await page.getByLabel("Endpoint").fill(`${staticMcp.url}/mcp`);
+ await page.getByLabel("Source name").fill(names.mcpHttp);
+ await page.getByLabel("Allow private network addresses for this source").check();
+ await page.getByLabel("Method").selectOption("oauth_access_token");
+ await page.getByLabel("OAuth access token", { exact: true }).fill(mcpToken);
+ const sourceCreation = page.waitForResponse((response) => {
+ const request = response.request();
+ return (
+ request.method() === "POST" &&
+ new URL(response.url()).pathname === "/api/v1/sources"
+ );
+ });
+ await page.getByRole("button", { name: "Connect source" }).click();
+ const sourceResponse = await sourceCreation;
+ const sourceBody = await sourceResponse.text();
+ expect(
+ sourceResponse.ok(),
+ `MCP HTTP source creation failed (${sourceResponse.status()}): ${sourceBody}`,
+ ).toBe(true);
+ await expectLocatorVisible(page.getByRole("heading", { name: names.mcpHttp }));
+ });
+
+ await step("Connect the trusted local MCP stdio template", async () => {
+ await page.goto("/sources");
+ await openConnectSourcePanel(page);
+ await page
+ .getByRole("group", { name: "Source type" })
+ .getByLabel("Trusted local MCP template")
+ .check();
+ await page
+ .getByLabel("Trusted template")
+ .selectOption({ label: "executor-e2e-stdio" });
+ await page.getByLabel("Source name").fill(names.mcpStdio);
+ await page.getByLabel("EXECUTOR_E2E_STDIO_SECRET").fill(stdioSecret);
+ await page.getByRole("button", { name: "Connect source" }).click();
+ await expectLocatorVisible(page.getByRole("heading", { name: names.mcpStdio }));
+ });
+
+ await step("Configure and connect managed OAuth from the dashboard", async () => {
+ await page.goto("/sources");
+ await openConnectSourcePanel(page);
+ await page
+ .getByRole("group", { name: "Source type" })
+ .getByLabel("MCP over HTTP")
+ .check();
+ await page.getByLabel("Endpoint").fill(provider.endpoint);
+ await page.getByLabel("Source name").fill(names.managedOAuth);
+ await page.getByLabel("Allow private network addresses for this source").check();
+ await page.getByRole("button", { name: "Connect source" }).click();
+ await expectLocatorVisible(page.getByRole("heading", { name: names.managedOAuth }));
+
+ const oauth = page.getByRole("region", {
+ name: `Managed OAuth for ${names.managedOAuth}`,
+ });
+ await expectLocatorVisible(oauth.getByLabel("Client ID"));
+ await oauth
+ .getByLabel("Authorization server override (optional)")
+ .fill(provider.issuer);
+ await oauth.getByLabel("Client ID").fill("pending-browser-registration");
+ await oauth.getByLabel("Requested scopes").fill("repo read:user");
+ const initialOAuthSave = page.waitForResponse((response) => {
+ const request = response.request();
+ return (
+ request.method() === "PUT" && new URL(response.url()).pathname.includes("/oauth/")
+ );
+ });
+ await oauth.getByRole("button", { name: "Save configuration" }).click();
+ const initialOAuthSaveResponse = await initialOAuthSave;
+ const initialOAuthSaveBody = await initialOAuthSaveResponse.text();
+ expect(
+ initialOAuthSaveResponse.ok(),
+ `Managed OAuth configuration failed (${initialOAuthSaveResponse.status()}): ${initialOAuthSaveBody}`,
+ ).toBe(true);
+ const callback = oauth.getByLabel("Exact callback URL");
+ await expectLocatorVisible(callback);
+ const clientId = await provider.registerClient(await callback.inputValue());
+ await oauth.getByLabel("Client ID").fill(clientId);
+ await oauth.getByRole("button", { name: "Save configuration" }).click();
+ await expectLocatorVisible(oauth.getByText("OAuth configuration saved"));
+ const reachedProvider = page.waitForURL(
+ (url) =>
+ url.origin === new URL(provider.issuer).origin && url.pathname === "/authorize",
+ { waitUntil: "commit" },
+ );
+ await oauth.getByRole("button", { name: "Connect OAuth" }).click();
+ await reachedProvider;
+ const callbackUrl = await provider.approveAuthorization(
+ page.url(),
+ LOCAL_ADMIN.username,
+ );
+ const returnedToExecutor = page.waitForURL(
+ (url) => url.pathname === "/sources" && url.searchParams.has("oauth"),
+ { waitUntil: "commit" },
+ );
+ await page.goto(callbackUrl);
+ await returnedToExecutor;
+ await expectLocatorVisible(page.getByText("OAuth authorization completed"));
+ const connectedOauth = page.getByRole("region", {
+ name: `Managed OAuth for ${names.managedOAuth}`,
+ });
+ await expectLocatorVisible(connectedOauth.getByText("Connected", { exact: true }));
+ const card = page.getByRole("article").filter({
+ has: page.getByRole("heading", { name: names.managedOAuth, exact: true }),
+ });
+ await card.getByRole("button", { name: "Reconnect and refresh tools" }).click();
+ await expectLocatorText(page.locator("#source-status"), "Source refreshed:");
+ });
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ const byName = (name: string) => {
+ const source = stored.sources.find((candidate) => candidate.displayName === name);
+ if (!source) throw new Error(`browser source was not stored: ${name}`);
+ return source;
+ };
+ const sources = {
+ openapi: byName(names.openapi),
+ graphql: byName(names.graphql),
+ mcpHttp: byName(names.mcpHttp),
+ mcpStdio: byName(names.mcpStdio),
+ managedOAuth: byName(names.managedOAuth),
+ };
+ const gatewayToken = yield* acquireToken(
+ client,
+ target.baseUrl,
+ unique("browser-source-agent"),
+ );
+ const invoke = async (
+ source: LocalSource,
+ label: RegExp,
+ arguments_: Readonly> = {},
+ ) => {
+ const catalog = await client.listTools({ sourceId: source.id });
+ const tool = catalog.items.find((candidate) =>
+ label.test(`${candidate.stableKey} ${candidate.displayName}`),
+ );
+ if (!tool) throw new Error(`${source.displayName} did not expose ${label.source}`);
+ const enabled = await client.setToolMode(tool, "enabled");
+ const response = await fetch(new URL("/api/v1/gateway/tools/invoke", target.baseUrl), {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${gatewayToken.token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ path: enabled.callablePath, arguments: arguments_ }),
+ });
+ const body = await response.text();
+ expect(response.status, `${source.displayName} invokes through the gateway`).toBe(200);
+ return body;
+ };
+
+ yield* Effect.promise(() =>
+ invoke(sources.openapi, /client.*credentials.*basic/i, {
+ body: { grant_type: "client_credentials" },
+ }),
+ );
+ const spotifyLedger = yield* Effect.promise(() => spotify.ledger.list());
+ const basicCall = spotifyLedger.find((entry) => entry.path === "/api/token");
+ expect(
+ basicCall?.request.headers.authorization,
+ "Spotify observed the HTTP Basic header",
+ ).toBe("[redacted]");
+ expect(
+ basicCall?.response.status,
+ "Spotify accepted the exact seeded Basic username and password",
+ ).toBe(200);
+ yield* Effect.promise(() => invoke(sources.openapi, /meta.*api.*key/i));
+ yield* Effect.promise(() => invoke(sources.openapi, /current.*user.*manual/i));
+ yield* Effect.promise(() => invoke(sources.graphql, /viewer/i));
+ yield* Effect.promise(() => invoke(sources.mcpHttp, /get_me/i));
+ const staticMcpLedger = yield* Effect.promise(() => staticMcp.ledger.list());
+ const staticMcpCall = staticMcpLedger.find(
+ (entry) =>
+ entry.path === "/mcp" && JSON.stringify(entry.request.body).includes("get_me"),
+ );
+ expect(
+ staticMcpCall?.request.headers.authorization,
+ "the MCP emulator observed its manual access token",
+ ).toBe("[redacted]");
+ expect(staticMcpCall?.identity.user?.login).toBe("admin");
+ const mcpToolCallsBeforeOAuth = staticMcpLedger.filter(
+ (entry) =>
+ entry.path === "/mcp" && JSON.stringify(entry.request.body).includes("get_me"),
+ ).length;
+ const stdioResult = yield* Effect.promise(() =>
+ invoke(sources.mcpStdio, /secret_status/i),
+ );
+ expect(
+ stdioResult,
+ "the stdio process receives the exact encrypted template secret",
+ ).toContain(`secret-sha256:${stdioSecretDigest}`);
+ yield* Effect.promise(() => invoke(sources.managedOAuth, /get_me/i));
+
+ const githubLedger = yield* Effect.promise(() => github.ledger.list());
+ const githubCall = (path: string) => {
+ const entry = githubLedger.find((candidate) => candidate.path === path);
+ if (!entry) throw new Error(`GitHub emulator did not observe ${path}`);
+ return entry;
+ };
+ expect(
+ githubCall("/meta").request.headers["x-executor-e2e-key"],
+ "the emulator observed the configured API key header",
+ ).toBe(apiKey);
+ expect(
+ githubCall("/user").identity.user?.login,
+ "the manual OAuth token authenticated its upstream request",
+ ).toBe("octocat");
+ const graphqlCall = githubLedger.find(
+ (candidate) =>
+ candidate.path === "/graphql" &&
+ !JSON.stringify(candidate.request.body).includes("__schema"),
+ );
+ expect(
+ graphqlCall?.identity.user?.login,
+ "the GraphQL bearer token authenticated a non-introspection query",
+ ).toBe("octocat");
+
+ const oauthLedger = yield* Effect.promise(() => provider.ledger());
+ const oauthToolCall = oauthLedger.find(
+ (entry) =>
+ entry.path === "/mcp" && JSON.stringify(entry.request.body).includes("get_me"),
+ );
+ expect(
+ oauthToolCall?.identity.user?.login,
+ "the managed OAuth token authenticated the upstream MCP call",
+ ).toBe("admin");
+ expect(
+ oauthLedger.filter(
+ (entry) =>
+ entry.path === "/mcp" && JSON.stringify(entry.request.body).includes("get_me"),
+ ).length,
+ "managed OAuth added one independently authenticated tool call",
+ ).toBe(mcpToolCallsBeforeOAuth + 1);
+
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step(
+ "Review every browser-created source in the redesigned dashboard",
+ async () => {
+ await page.goto("/sources");
+ for (const name of createdNames) {
+ await expectLocatorVisible(page.getByRole("heading", { name, exact: true }));
+ }
+ await expectLocatorText(
+ page.getByRole("region", { name: `Managed OAuth for ${names.managedOAuth}` }),
+ "Connected",
+ );
+ },
+ );
+ await step(
+ "Open the shared tool catalog from the browser-created OpenAPI source",
+ async () => {
+ const card = page.getByRole("article").filter({
+ has: page.getByRole("heading", { name: names.openapi, exact: true }),
+ });
+ await card.getByRole("link", { name: "View tools" }).click();
+ await expectLocatorText(
+ page.getByRole("table"),
+ "Exchange client credentials with Basic",
+ );
+ },
+ );
+ });
+ }),
+ Effect.gen(function* () {
+ yield* deleteSourcesNamed(client, createdNames);
+ }),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · a committed plain-text 502 reloads as exactly one source",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Committed replay");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ let postCount = 0;
+ let releaseLookup = () => {};
+ const lookupGate = new Promise((resolve) => {
+ releaseLookup = resolve;
+ });
+ let markLookupStarted = () => {};
+ const lookupStarted = new Promise((resolve) => {
+ markLookupStarted = resolve;
+ });
+ let lookupCount = 0;
+ let replayResponse:
+ | { readonly status: number; readonly headers: Record }
+ | undefined;
+
+ await page.route("**/api/v1/sources/idempotency", async (route) => {
+ if (route.request().method() !== "GET") {
+ await route.continue();
+ return;
+ }
+ lookupCount += 1;
+ if (lookupCount === 1) {
+ markLookupStarted();
+ await lookupGate;
+ await route.abort("aborted").catch(() => {});
+ return;
+ }
+ if (lookupCount === 2) {
+ await route.fulfill({
+ status: 200,
+ headers: {
+ "cache-control": "no-store",
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ status: "in_progress" }),
+ });
+ return;
+ }
+ const replay = await route.fetch();
+ replayResponse = { status: replay.status(), headers: replay.headers() };
+ await route.fulfill({ response: replay });
+ });
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ postCount += 1;
+ const committed = await route.fetch();
+ expect(committed.status(), "the real API committed before delivery failed").toBe(201);
+ await route.fulfill({
+ status: 502,
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ body: "gateway lost the committed response",
+ });
+ });
+
+ await step("Reload the ambiguous create and recover its committed result", async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await page.getByRole("button", { name: "Import source" }).click();
+ try {
+ await lookupStarted;
+ const pendingKey = await sourceCreateStorageValue(page);
+ expect(pendingKey, "one opaque key survives the ambiguous response").toMatch(
+ /^[0-9a-f]{32}$/,
+ );
+ const reloaded = page.reload();
+ releaseLookup();
+ await reloaded;
+ await expectLocatorVisible(
+ page.getByRole("status").filter({
+ hasText: "Executor is still finishing this source connection.",
+ }),
+ );
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ await expect
+ .poll(() => sourceCreateStorageValue(page), { timeout: 10_000 })
+ .toBeNull();
+ } finally {
+ releaseLookup();
+ }
+ expect(postCount, "reload recovery never issued a second create").toBe(1);
+ expect(
+ lookupCount,
+ "recovery polled after an in-progress status",
+ ).toBeGreaterThanOrEqual(3);
+ expect(replayResponse?.status, "the completed lookup replays HTTP 201").toBe(201);
+ expect(
+ replayResponse?.headers["idempotency-replayed"],
+ "the browser accepted the real replay marker",
+ ).toBe("true");
+ expect(
+ replayResponse?.headers["cache-control"],
+ "replay handling remains explicitly uncached",
+ ).toBe("no-store");
+ });
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(
+ stored.sources.filter((source) => source.displayName === sourceName),
+ "the committed key resolves to exactly one durable source",
+ ).toHaveLength(1);
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · a hard reload seals a missing request before unlocking",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Missing sealed");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ let postCount = 0;
+ let sealCount = 0;
+ let releaseLookup = () => {};
+ const lookupGate = new Promise((resolve) => {
+ releaseLookup = resolve;
+ });
+ let markLookupStarted = () => {};
+ const lookupStarted = new Promise((resolve) => {
+ markLookupStarted = resolve;
+ });
+ let firstLookup = true;
+
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ postCount += 1;
+ await route.abort("connectionreset");
+ });
+ await page.route("**/api/v1/sources/idempotency", async (route) => {
+ if (route.request().method() === "GET" && firstLookup) {
+ firstLookup = false;
+ markLookupStarted();
+ await lookupGate;
+ await route.abort("aborted").catch(() => {});
+ return;
+ }
+ await route.continue();
+ });
+ await page.route("**/api/v1/sources/idempotency/seal", async (route) => {
+ if (route.request().method() === "POST") sealCount += 1;
+ await route.continue();
+ });
+
+ await step("Reload after delivery loss and seal the server's missing key", async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await page.getByRole("button", { name: "Import source" }).click();
+ try {
+ await lookupStarted;
+ const unload = await page.evaluate(() => {
+ const event = new Event("beforeunload", { cancelable: true });
+ return {
+ dispatched: window.dispatchEvent(event),
+ defaultPrevented: event.defaultPrevented,
+ };
+ });
+ expect(
+ unload,
+ "recovery allows a real reload because no request is being dispatched",
+ ).toEqual({ dispatched: true, defaultPrevented: false });
+ const reloaded = page.reload();
+ releaseLookup();
+ await reloaded;
+ await expectLocatorVisible(
+ page.getByText(
+ "The unused source connection key was sealed. You can start a new request.",
+ ),
+ );
+ await expect.poll(() => sourceCreateStorageValue(page)).toBeNull();
+ } finally {
+ releaseLookup();
+ }
+ expect(postCount, "reload recovery did not redispatch the lost payload").toBe(1);
+ expect(sealCount, "one seal permanently fences the unused key").toBe(1);
+ });
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 0,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · failed delivery retries the same key and exact payload",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Exact retry");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ const attempts: Array<{ readonly key: string | undefined; readonly body: string }> = [];
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ attempts.push({
+ key: route.request().headers()["idempotency-key"],
+ body: route.request().postData() ?? "",
+ });
+ if (attempts.length <= 2) {
+ await route.abort("connectionreset");
+ return;
+ }
+ await route.continue();
+ });
+
+ await step("Retry one in-memory request until the real API receives it", async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await page.getByRole("button", { name: "Import source" }).click();
+ const retry = page.getByRole("button", { name: "Retry exact request" });
+ await expectLocatorVisible(retry);
+ expect(attempts, "initial delivery and its automatic retry both failed").toHaveLength(
+ 2,
+ );
+ expect(
+ attempts.map((attempt) => attempt.key),
+ "the automatic retry retained one idempotency key",
+ ).toEqual([attempts[0]?.key, attempts[0]?.key]);
+ expect(
+ attempts.map((attempt) => attempt.body),
+ "the automatic retry retained byte-identical JSON",
+ ).toEqual([attempts[0]?.body, attempts[0]?.body]);
+
+ await retry.click();
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ expect(attempts, "the deliberate retry reached the server").toHaveLength(3);
+ expect(attempts[2]?.key).toBe(attempts[0]?.key);
+ expect(attempts[2]?.body).toBe(attempts[0]?.body);
+ expect(attempts[0]?.key).toMatch(/^[0-9a-f]{32}$/);
+ });
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 1,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · a lost failed response replays its exact error and headers",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Failed replay");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ let postCount = 0;
+ let releaseFirstLookup = () => {};
+ const firstLookupGate = new Promise((resolve) => {
+ releaseFirstLookup = resolve;
+ });
+ let markFirstLookupStarted = () => {};
+ const firstLookupStarted = new Promise((resolve) => {
+ markFirstLookupStarted = resolve;
+ });
+ let firstLookup = true;
+ let originalFailure:
+ | {
+ readonly status: number;
+ readonly body: string;
+ readonly headers: Record;
+ }
+ | undefined;
+ let replayFailure:
+ | {
+ readonly status: number;
+ readonly body: string;
+ readonly headers: Record;
+ }
+ | undefined;
+
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ postCount += 1;
+ const failed = await route.fetch({
+ postData: JSON.stringify({
+ kind: "openapi",
+ displayName: sourceName,
+ preferredSlug: unique("invalid-replay"),
+ spec: { type: "inline", content: "not an OpenAPI document" },
+ }),
+ });
+ originalFailure = {
+ status: failed.status(),
+ body: await failed.text(),
+ headers: failed.headers(),
+ };
+ await route.fulfill({
+ status: 502,
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ body: "gateway lost the failed response",
+ });
+ });
+ await page.route("**/api/v1/sources/idempotency", async (route) => {
+ if (firstLookup) {
+ firstLookup = false;
+ markFirstLookupStarted();
+ await firstLookupGate;
+ await route.abort("aborted").catch(() => {});
+ return;
+ }
+ const replay = await route.fetch();
+ const body = await replay.text();
+ replayFailure = {
+ status: replay.status(),
+ body,
+ headers: replay.headers(),
+ };
+ await route.fulfill({
+ status: replay.status(),
+ headers: replay.headers(),
+ body,
+ });
+ });
+
+ await step(
+ "Recover the server's exact failed result after its response is lost",
+ async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await page.getByRole("button", { name: "Import source" }).click();
+ try {
+ await firstLookupStarted;
+ const reloaded = page.reload();
+ releaseFirstLookup();
+ await reloaded;
+ } finally {
+ releaseFirstLookup();
+ }
+ await expectLocatorVisible(page.locator("#source-create-status"));
+ await expect.poll(() => sourceCreateStorageValue(page)).toBeNull();
+ expect(postCount, "failed replay recovery never resubmits the source").toBe(1);
+ expect(replayFailure?.status).toBe(originalFailure?.status);
+ expect(replayFailure?.body, "the replay body is byte-identical").toBe(
+ originalFailure?.body,
+ );
+ expect(replayFailure?.headers["content-type"]).toBe(
+ originalFailure?.headers["content-type"],
+ );
+ const originalRequestReference = JSON.parse(originalFailure?.body ?? "{}") as {
+ readonly error?: { readonly requestId?: string };
+ };
+ const replayRequestReference = JSON.parse(replayFailure?.body ?? "{}") as {
+ readonly error?: { readonly requestId?: string };
+ };
+ expect(originalRequestReference.error?.requestId).toMatch(/\S+/);
+ expect(replayRequestReference.error?.requestId).toBe(
+ originalRequestReference.error?.requestId,
+ );
+ expect(replayFailure?.headers["idempotency-replayed"]).toBe("true");
+ expect(replayFailure?.headers["cache-control"]).toBe(
+ originalFailure?.headers["cache-control"],
+ );
+ },
+ );
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 0,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · a duplicated tab joins one pending attempt without another POST",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Duplicated tab");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ let releaseOriginal = () => {};
+ const originalGate = new Promise((resolve) => {
+ releaseOriginal = resolve;
+ });
+ let markCommitted = () => {};
+ const committed = new Promise((resolve) => {
+ markCommitted = resolve;
+ });
+ let postCount = 0;
+ page.context().on("request", (request) => {
+ const url = new URL(request.url());
+ if (request.method() === "POST" && url.pathname === "/api/v1/sources") {
+ postCount += 1;
+ }
+ });
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ const response = await route.fetch();
+ expect(response.status(), "the pending browser delivery committed once").toBe(201);
+ markCommitted();
+ await originalGate;
+ await route.fulfill({ response });
+ });
+
+ await step("Duplicate the pending tab and recover from its copied key", async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await page.getByRole("button", { name: "Import source" }).click();
+ let clone: Page | null = null;
+ try {
+ await committed;
+ expect(await sourceCreateStorageValue(page)).toMatch(/^[0-9a-f]{32}$/);
+ const popup = page.waitForEvent("popup");
+ await page.evaluate(() => {
+ window.open("/sources", "_blank");
+ });
+ clone = await popup;
+ await expectLocatorVisible(clone.getByRole("heading", { name: sourceName }));
+ await expect
+ .poll(() => (clone ? sourceCreateStorageValue(clone) : null))
+ .toBeNull();
+ expect(postCount, "the duplicated tab used lookup instead of POST").toBe(1);
+ } finally {
+ releaseOriginal();
+ }
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ expect(postCount, "both tabs converge on the original POST").toBe(1);
+ await clone?.close();
+ });
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 1,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · signing out during lookup never redispatches the request",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Signout recovery");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.privateSession(identity, async ({ page, step }) => {
+ let postCount = 0;
+ let sealCount = 0;
+ let sessionDeleteCount = 0;
+ let releaseLookup = () => {};
+ const lookupGate = new Promise((resolve) => {
+ releaseLookup = resolve;
+ });
+ let markLookupStarted = () => {};
+ const lookupStarted = new Promise((resolve) => {
+ markLookupStarted = resolve;
+ });
+ let markLookupSettled = () => {};
+ const lookupSettled = new Promise((resolve) => {
+ markLookupSettled = resolve;
+ });
+ let releaseSessionDelete = () => {};
+ const sessionDeleteGate = new Promise((resolve) => {
+ releaseSessionDelete = resolve;
+ });
+ let markSessionDeleteStarted = () => {};
+ const sessionDeleteStarted = new Promise((resolve) => {
+ markSessionDeleteStarted = resolve;
+ });
+ let holdFirstLookup = true;
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ postCount += 1;
+ const committed = await route.fetch();
+ await route.fulfill({
+ status: 502,
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ body: "gateway lost the committed response",
+ });
+ expect(committed.status()).toBe(201);
+ });
+ await page.route("**/api/v1/sources/idempotency", async (route) => {
+ if (route.request().method() === "GET" && holdFirstLookup) {
+ holdFirstLookup = false;
+ markLookupStarted();
+ await lookupGate;
+ await route.abort("aborted").catch(() => {});
+ markLookupSettled();
+ return;
+ }
+ await route.continue();
+ });
+ await page.route("**/api/v1/sources/idempotency/seal", async (route) => {
+ if (route.request().method() === "POST") sealCount += 1;
+ await route.continue();
+ });
+ await page.route("**/api/v1/session", async (route) => {
+ if (route.request().method() !== "DELETE") {
+ await route.continue();
+ return;
+ }
+ sessionDeleteCount += 1;
+ markSessionDeleteStarted();
+ await sessionDeleteGate;
+ await route.continue();
+ });
+
+ await step(
+ "Sign out during recovery, sign back in, and join the committed result",
+ async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await page.getByRole("button", { name: "Import source" }).click();
+ try {
+ await lookupStarted;
+ const unload = await page.evaluate(() => {
+ const event = new Event("beforeunload", { cancelable: true });
+ return {
+ dispatched: window.dispatchEvent(event),
+ defaultPrevented: event.defaultPrevented,
+ };
+ });
+ expect(unload, "recovery permits session navigation").toEqual({
+ dispatched: true,
+ defaultPrevented: false,
+ });
+ const signOut = page.getByRole("button", { name: "Sign out" }).click();
+ await sessionDeleteStarted;
+ expect(
+ new URL(page.url()).pathname,
+ "the recovery page remains mounted while session deletion is in flight",
+ ).toBe("/sources");
+ expect(await sourceCreateStorageValue(page)).toMatch(/^[0-9a-f]{32}$/);
+ releaseLookup();
+ await lookupSettled;
+ await page.evaluate(
+ () =>
+ new Promise((resolve) => {
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
+ }),
+ );
+ expect(
+ postCount,
+ "settled lookup did not redispatch while DELETE was gated",
+ ).toBe(1);
+ expect(sealCount, "sign-out pause never seals the recoverable key").toBe(0);
+ expect(
+ await sourceCreateStorageValue(page),
+ "the pending key survives the lookup and session-delete race",
+ ).toMatch(/^[0-9a-f]{32}$/);
+ releaseSessionDelete();
+ await signOut;
+ await page.waitForURL((url) => url.pathname === "/login");
+ expect(sessionDeleteCount, "recovery permits one deliberate sign-out").toBe(1);
+ expect(
+ await sourceCreateStorageValue(page),
+ "the key remains available to the next authenticated Sources page",
+ ).toMatch(/^[0-9a-f]{32}$/);
+
+ await page.getByLabel("Username").fill(LOCAL_ADMIN.username);
+ await page.getByLabel("Password").fill(LOCAL_ADMIN.password);
+ await page.getByRole("button", { name: "Sign in" }).click();
+ await page.waitForURL((url) => url.pathname === "/sources");
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ await expect.poll(() => sourceCreateStorageValue(page)).toBeNull();
+ expect(postCount, "authenticated recovery still uses the original POST").toBe(1);
+ expect(sealCount, "authenticated recovery never seals a committed key").toBe(0);
+ } finally {
+ releaseLookup();
+ releaseSessionDelete();
+ }
+ expect(postCount, "session recovery joins by key instead of redispatching").toBe(1);
+ },
+ );
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 1,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation recovery · authoritative refresh B cannot be overwritten by stale list A",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Authoritative refresh");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ let postCount = 0;
+ let committedStatus: number | undefined;
+ let listCount = 0;
+ let releaseStaleList = () => {};
+ const staleListGate = new Promise((resolve) => {
+ releaseStaleList = resolve;
+ });
+ let markStaleListCaptured = () => {};
+ const staleListCaptured = new Promise((resolve) => {
+ markStaleListCaptured = resolve;
+ });
+ let markStaleListDelivered = () => {};
+ const staleListDelivered = new Promise((resolve) => {
+ markStaleListDelivered = resolve;
+ });
+ let markAuthoritativeListDelivered = () => {};
+ const authoritativeListDelivered = new Promise((resolve) => {
+ markAuthoritativeListDelivered = resolve;
+ });
+
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() === "POST") {
+ postCount += 1;
+ const committed = await route.fetch();
+ committedStatus = committed.status();
+ await route.fulfill({
+ status: 502,
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ body: "gateway lost the committed response",
+ });
+ return;
+ }
+ if (route.request().method() !== "GET") {
+ await route.continue();
+ return;
+ }
+ const currentList = ++listCount;
+ const response = await route.fetch();
+ if (currentList === 1) {
+ markStaleListCaptured();
+ await staleListGate;
+ } else if (currentList === 2) {
+ markAuthoritativeListDelivered();
+ }
+ if (currentList === 1) {
+ await route.fulfill({ response }).catch(() => {});
+ markStaleListDelivered();
+ return;
+ }
+ await route.fulfill({ response });
+ });
+
+ await step(
+ "Keep list A in flight until replay refresh B is authoritative",
+ async () => {
+ await prepareInlineOpenApiSource(
+ page,
+ minimalOpenApiDocument(github.url, sourceName),
+ sourceName,
+ );
+ await staleListCaptured;
+ await page.getByRole("button", { name: "Import source" }).click();
+ try {
+ await authoritativeListDelivered;
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ releaseStaleList();
+ await staleListDelivered;
+ await page.evaluate(
+ () =>
+ new Promise((resolve) => {
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
+ }),
+ );
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ } finally {
+ releaseStaleList();
+ }
+ expect(
+ committedStatus,
+ "the source committed before response delivery failed",
+ ).toBe(201);
+ expect(postCount, "replay recovery keeps one source create").toBe(1);
+ expect(listCount, "refresh B follows the earlier list A").toBeGreaterThanOrEqual(2);
+ },
+ );
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 1,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Source creation guard · navigation waits and session storage retains only an opaque key",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const github = yield* githubEmulator;
+ const sourceName = unique("Guarded source");
+ const credential = unique("guarded-credential");
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.privateSession(identity, async ({ page, step }) => {
+ let releaseSubmission = () => {};
+ const submissionGate = new Promise((resolve) => {
+ releaseSubmission = resolve;
+ });
+ let markSubmissionStarted = () => {};
+ const submissionStarted = new Promise((resolve) => {
+ markSubmissionStarted = resolve;
+ });
+ let submittedPayload = "";
+ const sessionDeletes: string[] = [];
+ page.on("request", (request) => {
+ const url = new URL(request.url());
+ if (request.method() === "DELETE" && url.pathname === "/api/v1/session") {
+ sessionDeletes.push(request.url());
+ }
+ });
+ await page.route("**/api/v1/sources", async (route) => {
+ if (route.request().method() !== "POST") {
+ await route.continue();
+ return;
+ }
+ submittedPayload = route.request().postData() ?? "";
+ markSubmissionStarted();
+ await submissionGate;
+ await route.continue();
+ });
+
+ await step("Block every exit while the exact source request is gated", async () => {
+ await page.goto("/tools");
+ await prepareInlineOpenApiSource(
+ page,
+ staticAuthOpenApiDocument(github.url, github.url),
+ sourceName,
+ );
+ await page.getByRole("checkbox", { name: /basicAuth/ }).check();
+ await page.getByLabel("Username").fill("guarded-admin");
+ await page.getByLabel("Basic auth", { exact: true }).fill(credential);
+ await page.getByRole("button", { name: "Import source" }).click();
+ try {
+ await submissionStarted;
+ await expectLocatorValue(page.getByLabel("Basic auth", { exact: true }), "");
+ const sourceTypes = page.getByRole("group", { name: "Source type" });
+ for (const connector of [
+ "OpenAPI service",
+ "GraphQL API",
+ "MCP over HTTP",
+ "Trusted local MCP template",
+ ]) {
+ await expectLocatorDisabled(
+ sourceTypes.getByLabel(connector),
+ `${connector} stays locked during dispatch`,
+ );
+ }
+
+ const unload = await page.evaluate(() => {
+ const event = new Event("beforeunload", { cancelable: true });
+ return {
+ dispatched: window.dispatchEvent(event),
+ defaultPrevented: event.defaultPrevented,
+ };
+ });
+ expect(
+ unload,
+ "dispatch blocks an unload that could lose the in-memory payload",
+ ).toEqual({ dispatched: false, defaultPrevented: true });
+
+ const storage = await page.evaluate(() =>
+ Object.fromEntries(
+ Array.from({ length: sessionStorage.length }, (_, index) => {
+ const key = sessionStorage.key(index) ?? "";
+ return [key, sessionStorage.getItem(key) ?? ""];
+ }),
+ ),
+ );
+ const entries = Object.entries(storage).filter(
+ ([key]) => key === sourceCreateStorageKey,
+ );
+ expect(entries, "session storage has one pending idempotency record").toHaveLength(
+ 1,
+ );
+ expect(entries[0]?.[1]).toMatch(/^[0-9a-f]{32}$/);
+ const persisted = JSON.stringify(storage);
+ expect(persisted).not.toContain(github.url);
+ expect(persisted).not.toContain(sourceName);
+ expect(persisted).not.toContain(credential);
+ expect(persisted).not.toContain(submittedPayload);
+
+ const toolsLink = page
+ .getByRole("navigation", { name: "Dashboard" })
+ .getByRole("link", { name: "Tools", exact: true });
+ await toolsLink.click();
+ expect(new URL(page.url()).pathname).toBe("/sources");
+ const warning = page.locator("#source-navigation-status");
+ await expectLocatorText(warning, "still being submitted");
+ await expectLocatorFocused(warning);
+
+ const unloadPrompt = page
+ .waitForEvent("dialog", { timeout: 2_000 })
+ .catch(() => null);
+ const historyNavigation = page
+ .goBack({
+ waitUntil: "domcontentloaded",
+ timeout: 2_000,
+ })
+ .catch(() => null);
+ const dialog = await unloadPrompt;
+ const dialogType = dialog?.type() ?? null;
+ expect(
+ [null, "beforeunload"],
+ "history navigation either cancels in-app or raises the native unload guard",
+ ).toContain(dialogType);
+ if (dialog !== null) {
+ await dialog.dismiss();
+ }
+ await historyNavigation;
+ expect(new URL(page.url()).pathname).toBe("/sources");
+ await expectLocatorFocused(warning);
+
+ await page.getByRole("button", { name: "Sign out" }).click();
+ expect(new URL(page.url()).pathname).toBe("/sources");
+ await expectLocatorFocused(warning);
+ expect(
+ sessionDeletes,
+ "blocked sign-out never destroys the administrator session",
+ ).toHaveLength(0);
+ } finally {
+ releaseSubmission();
+ }
+
+ await expectLocatorVisible(page.getByRole("heading", { name: sourceName }));
+ await expect.poll(() => sourceCreateStorageValue(page)).toBeNull();
+ await page
+ .getByRole("navigation", { name: "Dashboard" })
+ .getByRole("link", { name: "Tools", exact: true })
+ .click();
+ await page.waitForURL((url) => url.pathname === "/tools");
+ expect(
+ sessionDeletes,
+ "successful release does not sign the administrator out",
+ ).toHaveLength(0);
+ });
+ });
+
+ const stored = yield* Effect.promise(() => client.listSources());
+ expect(stored.sources.filter((source) => source.displayName === sourceName)).toHaveLength(
+ 1,
+ );
+ }),
+ deleteSourcesNamed(client, [sourceName]),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Tools · search, filters, and broad mode changes stay explicit",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const upstream = yield* serveOpenApiEchoTestServer();
+ const name = unique("Mode controls");
+ const source = yield* Effect.promise(() =>
+ client.createSource({
+ kind: "openapi",
+ displayName: name,
+ preferredSlug: unique("modes").replaceAll("-", "_"),
+ spec: { type: "inline", content: upstream.specJson },
+ allowPrivateNetwork: true,
+ }),
+ );
+ const catalog = yield* Effect.promise(() => client.listTools({ sourceId: source.id }));
+ const tool = catalog.items.find((candidate) =>
+ candidate.sandboxPath.endsWith(".echo_echo_message"),
+ );
+ if (!tool) return yield* Effect.die("the mode fixture produced no echo message tool");
+ const token = yield* acquireToken(client, target.baseUrl, unique("mode-agent"));
+ const invoke = (idempotencyKey?: string) => {
+ const headers = new Headers({
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ });
+ if (idempotencyKey !== undefined) headers.set("idempotency-key", idempotencyKey);
+ return fetch(new URL("/api/v1/gateway/tools/invoke", target.baseUrl), {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ path: tool.callablePath,
+ arguments: { path: { message: "Mode" } },
+ }),
+ });
+ };
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step(
+ "Set the source default to Enabled with deliberate confirmation",
+ async () => {
+ await page.goto("/sources");
+ const card = page.getByRole("article").filter({
+ has: page.getByRole("heading", { name, exact: true }),
+ });
+ const sourceModes = card.getByRole("group", { name: "Default tool behavior" });
+ await sourceModes.getByLabel("Enabled", { exact: true }).check();
+ await card.getByRole("button", { name: "Apply source default" }).click();
+ const confirmation = card.getByRole("group", {
+ name: `Confirm default for ${name}`,
+ });
+ await expectLocatorVisible(confirmation);
+ await confirmation.getByRole("button", { name: "Confirm broad change" }).click();
+ await expectLocatorText(
+ page.locator("#source-status"),
+ "Source default changed to Enabled",
+ );
+ },
+ );
+
+ await step("Search the global catalog and filter to one source", async () => {
+ await page.goto("/tools");
+ await page.getByLabel("Search tools").fill("echo");
+ await page.getByRole("button", { name: "Search" }).click();
+ await page.getByLabel("Source").selectOption({ label: name });
+ await expectLocatorText(page.getByRole("table"), "echo");
+ });
+
+ await step(
+ "Disable one tool from its row and enforce the change at the gateway",
+ async () => {
+ const toolModes = page.getByRole("group", {
+ name: `Behavior for ${tool.displayName}`,
+ });
+ const changed = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ response.request().method() === "PATCH" &&
+ url.pathname === `/api/v1/tools/${tool.id}/mode`
+ );
+ });
+ await toolModes.getByLabel("Disabled", { exact: true }).check();
+ expect((await changed).status(), "the row mutation reached the real API").toBe(200);
+ await expectLocatorChecked(toolModes.getByLabel("Disabled", { exact: true }));
+ const denied = await invoke();
+ expect(
+ (await denied.json()) as unknown,
+ "the row-level Disabled setting is enforced at the gateway",
+ ).toMatchObject({ error: { code: "tool_disabled" } });
+ },
+ );
+
+ await step(
+ "Inherit warns before the Disabled tool becomes source Enabled",
+ async () => {
+ await page.getByLabel(`Select ${tool.displayName}`).check();
+ await page.getByRole("button", { name: "Apply Inherit to 1 selected" }).click();
+ const confirmation = page.getByRole("group", {
+ name: "Confirm bulk tool behavior",
+ });
+ await expectLocatorText(
+ confirmation,
+ "1 tool will become Enabled under its source default",
+ );
+ await confirmation.getByRole("button", { name: "Confirm Inherit" }).click();
+ await expectLocatorText(
+ page.locator("#bulk-status"),
+ "Inherit applied to 1 selected tool",
+ );
+ expect(
+ (await invoke()).status,
+ "the confirmed inherited Enabled mode is callable",
+ ).toBe(200);
+ },
+ );
+
+ await step("Set Ask from the tool row and approve its real gateway call", async () => {
+ const toolModes = page.getByRole("group", {
+ name: `Behavior for ${tool.displayName}`,
+ });
+ const changed = page.waitForResponse((response) => {
+ const url = new URL(response.url());
+ return (
+ response.request().method() === "PATCH" &&
+ url.pathname === `/api/v1/tools/${tool.id}/mode`
+ );
+ });
+ await toolModes.getByLabel("Ask", { exact: true }).check();
+ expect((await changed).status(), "the Ask row mutation reached the real API").toBe(
+ 200,
+ );
+ await expectLocatorChecked(toolModes.getByLabel("Ask", { exact: true }));
+
+ const asked = await invoke(randomUUID());
+ expect(asked.status, "the row-level Ask setting pauses the gateway call").toBe(202);
+ const pending = (await asked.json()) as {
+ readonly approval: { readonly id: string; readonly statusUrl: string };
+ };
+ await page.goto(`/approvals?approval=${encodeURIComponent(pending.approval.id)}`);
+ await expectLocatorVisible(page.getByText(tool.callablePath, { exact: true }).last());
+ await page.getByRole("button", { name: "Approve once" }).click();
+ await page.getByRole("button", { name: "Yes, approve once" }).click();
+ await expectLocatorVisible(page.getByText(/was approved/i));
+ await expect
+ .poll(
+ async () => {
+ const response = await fetch(
+ new URL(pending.approval.statusUrl, target.baseUrl),
+ { headers: { authorization: `Bearer ${token.token}` } },
+ );
+ const body = (await response.json()) as { readonly status: string };
+ return body.status;
+ },
+ { timeout: 10_000 },
+ )
+ .toBe("succeeded");
+ await page.goto(
+ `/tools?source=${encodeURIComponent(source.id)}&q=${encodeURIComponent("echo")}`,
+ );
+ await expectLocatorText(page.getByRole("table"), tool.displayName);
+ });
+
+ await step("Ask applies immediately to the selected tools", async () => {
+ await page.getByLabel("Select all active tools on this page").check();
+ await page
+ .getByRole("group", { name: "Set selected tools" })
+ .getByLabel("Ask")
+ .check();
+ await page.getByRole("button", { name: /^Apply Ask to \d+ selected$/ }).click();
+ await expectLocatorText(page.locator("#bulk-status"), "Ask applied");
+ });
+
+ await step("Disabling many tools requires a second confirmation", async () => {
+ const selectAll = page.getByLabel("Select all active tools on this page");
+ const selection = page.getByLabel(`Select ${tool.displayName}`);
+ const bulkModes = page.getByRole("group", { name: "Set selected tools" });
+ const toolModes = page.getByRole("group", {
+ name: `Behavior for ${tool.displayName}`,
+ });
+ const applyDisabled = page.getByRole("button", {
+ name: /^Apply Disabled to \d+ selected$/,
+ });
+ const applyInherit = page.getByRole("button", {
+ name: /^Apply Inherit to \d+ selected$/,
+ });
+
+ await selectAll.check();
+ await bulkModes.getByLabel("Disabled").check();
+ await applyDisabled.click();
+
+ let confirmation = page.getByRole("group", {
+ name: "Confirm bulk tool behavior",
+ });
+ let cancel = confirmation.getByRole("button", { name: "Cancel" });
+ await expectLocatorVisible(confirmation);
+ await expectLocatorFocused(cancel);
+ await expectLocatorDisabled(selectAll);
+ await expectLocatorDisabled(selection);
+ await expectLocatorDisabled(bulkModes.getByLabel("Disabled"));
+ await expectLocatorDisabled(toolModes.getByLabel("Ask", { exact: true }));
+ await expectLocatorDisabled(applyDisabled);
+ await expectLocatorDisabled(applyInherit);
+
+ await cancel.press("Escape");
+ await expectLocatorCount(confirmation, 0);
+ await expectLocatorFocused(applyDisabled);
+
+ await applyDisabled.click();
+ confirmation = page.getByRole("group", { name: "Confirm bulk tool behavior" });
+ cancel = confirmation.getByRole("button", { name: "Cancel" });
+ await expectLocatorFocused(cancel);
+ await cancel.click();
+ await expectLocatorCount(confirmation, 0);
+ await expectLocatorFocused(applyDisabled);
+
+ await applyDisabled.click();
+ confirmation = page.getByRole("group", { name: "Confirm bulk tool behavior" });
+ await confirmation.getByRole("button", { name: "Confirm Disabled" }).click();
+ const status = page.locator("#bulk-status");
+ await expectLocatorText(status, "Disabled applied");
+ await expectLocatorFocused(status);
+ const denied = await invoke();
+ expect(
+ (await denied.json()) as unknown,
+ "disabled tools are denied at the gateway",
+ ).toMatchObject({
+ error: { code: "tool_disabled" },
+ });
+ });
+
+ await step("Re-enabling many tools also requires deliberate confirmation", async () => {
+ await page.getByLabel("Effective mode").selectOption("disabled");
+ await page.getByLabel("Select all active tools on this page").check();
+ await page
+ .getByRole("group", { name: "Set selected tools" })
+ .getByLabel("Enabled")
+ .check();
+ await page.getByRole("button", { name: /^Apply Enabled to \d+ selected$/ }).click();
+ await page.getByRole("button", { name: "Confirm Enabled" }).click();
+ await expectLocatorText(page.locator("#bulk-status"), "Enabled applied");
+ expect((await invoke()).status, "enabled tools are callable without approval").toBe(
+ 200,
+ );
+ });
+ });
+ }),
+ Effect.promise(() => client.deleteSource(source.id)).pipe(Effect.ignore),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "MCP and CLI · bearer clients share the global catalog and concurrent runtime",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const client = yield* adminClient(target.baseUrl);
+ const upstream = yield* serveOpenApiEchoTestServer();
+ const source = yield* Effect.promise(() =>
+ client.createSource({
+ kind: "openapi",
+ displayName: unique("MCP CLI source"),
+ spec: { type: "inline", content: upstream.specJson },
+ allowPrivateNetwork: true,
+ }),
+ );
+ const catalog = yield* Effect.promise(() => client.listTools({ sourceId: source.id }));
+ const tool = catalog.items.find((candidate) =>
+ candidate.sandboxPath.endsWith(".echo_echo_message"),
+ );
+ if (!tool) return yield* Effect.die("the MCP and CLI fixture produced no echo message tool");
+ yield* Effect.promise(() => client.setToolMode(tool, "enabled"));
+ const token = yield* acquireToken(client, target.baseUrl, unique("mcp-cli-agent"));
+ const binary = process.env.E2E_EXECUTOR_BIN;
+ if (!binary) return yield* Effect.die("the e2e harness did not publish its Executor binary");
+ const cliEnvironment = { ...process.env, EXECUTOR_API_TOKEN: token.token };
+ const cli = (arguments_: readonly string[]) =>
+ Effect.promise(() =>
+ executeFile(binary, ["--base-url", target.baseUrl, "--json", ...arguments_], {
+ env: cliEnvironment,
+ }),
+ );
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const sources = yield* cli(["tools", "sources"]);
+ expect(sources.stdout, "the CLI lists the source catalog").toContain(source.slug);
+ const search = yield* cli(["tools", "search", "echo"]);
+ expect(search.stdout, "the CLI searches the same global tools").toContain(
+ tool.sandboxPath,
+ );
+ const described = yield* cli(["tools", "describe", tool.sandboxPath]);
+ expect(described.stdout, "the CLI describes the selected tool").toContain(
+ tool.sandboxPath,
+ );
+ const called = yield* cli(["call", tool.sandboxPath, '{"path":{"message":"CLI"}}']);
+ expect(called.stdout, "the CLI calls the enabled tool").toContain("CLI");
+
+ const httpMcp = yield* Effect.acquireRelease(
+ Effect.promise(async () => {
+ const mcp = new Client({ name: "executor-e2e-http", version: "1.0.0" });
+ const transport = new StreamableHTTPClientTransport(new URL(target.mcpUrl), {
+ requestInit: { headers: { authorization: `Bearer ${token.token}` } },
+ });
+ await mcp.connect(transport);
+ return mcp;
+ }),
+ (mcp) => Effect.promise(() => mcp.close()).pipe(Effect.ignore),
+ );
+ const advertised = yield* Effect.promise(() => httpMcp.listTools());
+ expect(
+ advertised.tools.map((candidate) => candidate.name),
+ "HTTP MCP advertises the bounded gateway surface",
+ ).toEqual(expect.arrayContaining(["execute", "call", "search", "describe", "sources"]));
+ const executed = yield* Effect.promise(() =>
+ httpMcp.callTool({
+ name: "execute",
+ arguments: {
+ code: `const values = await Promise.all([${tool.callablePath}({ path: { message: "one" } }), ${tool.callablePath}({ path: { message: "two" } })]); return values;`,
+ },
+ }),
+ );
+ expect(
+ JSON.stringify(executed),
+ "the TS runtime completes two concurrent calls",
+ ).toContain("one");
+ expect(JSON.stringify(executed)).toContain("two");
+
+ const stdioMcp = yield* Effect.acquireRelease(
+ Effect.promise(async () => {
+ const mcp = new Client({ name: "executor-e2e-stdio", version: "1.0.0" });
+ const transport = new StdioClientTransport({
+ command: binary,
+ args: ["--base-url", target.baseUrl, "mcp"],
+ env: {
+ PATH: process.env.PATH ?? "",
+ HOME: process.env.HOME ?? "",
+ EXECUTOR_API_TOKEN: token.token,
+ },
+ stderr: "pipe",
+ });
+ await mcp.connect(transport);
+ return mcp;
+ }),
+ (mcp) => Effect.promise(() => mcp.close()).pipe(Effect.ignore),
+ );
+ const bridged = yield* Effect.promise(() => stdioMcp.listTools());
+ expect(
+ bridged.tools.map((candidate) => candidate.name),
+ "the CLI stdio bridge exposes the same MCP tools",
+ ).toEqual(advertised.tools.map((candidate) => candidate.name));
+
+ yield* Effect.promise(() => client.revokeToken(token.id));
+ const afterRevocation = yield* Effect.exit(Effect.tryPromise(() => httpMcp.listTools()));
+ expect(afterRevocation._tag, "revocation invalidates an existing MCP session").toBe(
+ "Failure",
+ );
+ }),
+ Effect.promise(() => client.deleteSource(source.id)).pipe(Effect.ignore),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Approvals and logs · an Ask call waits for one administrator decision",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const upstream = yield* serveOpenApiEchoTestServer();
+ const source = yield* Effect.promise(() =>
+ client.createSource({
+ kind: "openapi",
+ displayName: unique("Approval source"),
+ spec: { type: "inline", content: upstream.specJson },
+ allowPrivateNetwork: true,
+ }),
+ );
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const page = yield* Effect.promise(() => client.listTools({ sourceId: source.id }));
+ const tool = page.items.find((candidate) =>
+ candidate.sandboxPath.endsWith(".echo_echo_message"),
+ );
+ if (!tool) return yield* Effect.die("the approval fixture produced no echo message tool");
+ yield* Effect.promise(() => client.setToolMode(tool, "ask"));
+ const token = yield* acquireToken(client, target.baseUrl, unique("approval-agent"));
+ const invoke = yield* Effect.promise(() =>
+ fetch(new URL("/api/v1/gateway/tools/invoke", target.baseUrl), {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ "idempotency-key": randomUUID(),
+ },
+ body: JSON.stringify({
+ path: tool.callablePath,
+ arguments: { path: { message: "Ada" } },
+ }),
+ }),
+ );
+ const invokeBody = yield* Effect.promise(() => invoke.clone().text());
+ expect(invoke.status, `Ask returns an accepted approval handle: ${invokeBody}`).toBe(202);
+ const pending = (yield* Effect.promise(() => invoke.json())) as {
+ readonly approval: { readonly id: string; readonly statusUrl: string };
+ };
+ const approvalStatusUrl = new URL(pending.approval.statusUrl, target.baseUrl);
+ expect(approvalStatusUrl.origin, "the bearer stays on this Executor origin").toBe(
+ new URL(target.baseUrl).origin,
+ );
+ expect(approvalStatusUrl.pathname, "the handle names this exact approval").toBe(
+ `/api/v1/gateway/approvals/${pending.approval.id}`,
+ );
+
+ const identity = yield* target.newIdentity();
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Review the exact pending request", async () => {
+ await page.goto(`/approvals?approval=${encodeURIComponent(pending.approval.id)}`);
+ await expectLocatorVisible(page.getByText(tool.callablePath, { exact: true }).last());
+ await expectLocatorVisible(page.getByText("Structural, redacted argument preview"));
+ });
+ await step("Approve this invocation once", async () => {
+ await page.getByRole("button", { name: "Approve once" }).click();
+ await page.getByRole("button", { name: "Yes, approve once" }).click();
+ await expectLocatorVisible(page.getByText(/was approved/i));
+ });
+ await step("Open Logs and find the completed gateway request", async () => {
+ await page.goto("/logs");
+ await expectLocatorVisible(
+ page.getByText(tool.callablePath, { exact: true }).first(),
+ );
+ });
+ });
+
+ const completed = yield* Effect.promise(async () => {
+ for (let attempt = 0; attempt < 80; attempt += 1) {
+ const response = await fetch(approvalStatusUrl, {
+ headers: { authorization: `Bearer ${token.token}` },
+ });
+ const body = (await response.json()) as { readonly status: string };
+ if (!["pending", "approved", "executing"].includes(body.status)) return body;
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
+ }
+ throw new Error("approved call did not finish");
+ });
+ expect(completed.status, "the approved invocation runs exactly once").toBe("succeeded");
+ const requests = yield* upstream.requests;
+ expect(
+ requests.filter((request) => request.path.startsWith("/echo/")).length,
+ "one approval produces one upstream side effect",
+ ).toBe(1);
+ }),
+ Effect.promise(() => client.deleteSource(source.id)).pipe(Effect.ignore),
+ );
+ }),
+ ),
+);
+
+scenario(
+ "Managed OAuth · the provider callback connects a source without exposing tokens",
+ { timeout: 180_000 },
+ Effect.scoped(
+ Effect.gen(function* () {
+ const target = yield* Target;
+ const browser = yield* Browser;
+ const client = yield* adminClient(target.baseUrl);
+ const provider = yield* serveOAuthTestProvider(emulatorPortC);
+ const source = yield* Effect.promise(() =>
+ client.createSource({
+ kind: "mcp_http",
+ displayName: unique("Managed OAuth"),
+ allowPrivateNetwork: true,
+ endpoint: provider.endpoint,
+ }),
+ );
+
+ yield* Effect.ensuring(
+ Effect.gen(function* () {
+ const available = yield* Effect.promise(() => client.getOAuthConnections(source.id));
+ const credentialKey = available.availableCredentials[0]?.credentialKey;
+ if (!credentialKey) return yield* Effect.die("the OAuth source exposed no credential");
+ const placeholder = yield* Effect.promise(() =>
+ client.putOAuthConnection(source.id, credentialKey, {
+ expectedRevision: 0,
+ discovery: { type: "issuer", issuer: provider.issuer },
+ client: { clientId: "pending-dynamic-registration", authentication: "none" },
+ scopes: ["repo", "read:user"],
+ }),
+ );
+ const clientId = yield* Effect.promise(() =>
+ provider.registerClient(placeholder.callbackUrl),
+ );
+ const connection = yield* Effect.promise(() =>
+ client.putOAuthConnection(source.id, credentialKey, {
+ expectedRevision: placeholder.revision,
+ discovery: { type: "issuer", issuer: provider.issuer },
+ client: { clientId, authentication: "none" },
+ scopes: ["repo", "read:user"],
+ }),
+ );
+ expect(connection.callbackUrl, "the callback is connection-specific").toContain(
+ "/api/v1/oauth/callback/",
+ );
+ const authorization = yield* Effect.promise(() =>
+ client.authorizeOAuthConnection(source.id, credentialKey, connection.revision),
+ );
+ const identity = client.asIdentity(LOCAL_ADMIN.username);
+ yield* browser.privateSession(identity, async ({ page, step }) => {
+ await step(
+ "Follow the provider authorization back to this exact connection",
+ async () => {
+ const callbackUrl = await provider.approveAuthorization(
+ authorization.authorizationUrl,
+ LOCAL_ADMIN.username,
+ );
+ const returnedToExecutor = page.waitForURL(
+ (url) => url.pathname === "/sources" && url.searchParams.has("oauth"),
+ { waitUntil: "commit" },
+ );
+ await page.goto(callbackUrl);
+ await returnedToExecutor;
+ await expectLocatorVisible(page.getByText("OAuth authorization completed"));
+ await expectLocatorVisible(page.getByText("Connected", { exact: true }).last());
+ },
+ );
+ });
+ const after = yield* Effect.promise(() => client.getOAuthConnections(source.id));
+ expect(after.connections[0]?.status, "the callback exchanged the code").toBe("connected");
+ yield* Effect.promise(() => client.refreshSource(source.id));
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Return to Sources and see the managed connection online", async () => {
+ await page.goto("/sources");
+ await expectLocatorVisible(page.getByText(source.displayName, { exact: true }));
+ const oauth = page.getByRole("region", {
+ name: `Managed OAuth for ${source.displayName}`,
+ });
+ await expectLocatorVisible(oauth.getByText("Connected", { exact: true }));
+ });
+ await step(
+ "Cancel OAuth deletion with the keyboard and return to its opener",
+ async () => {
+ const oauth = page.getByRole("region", {
+ name: `Managed OAuth for ${source.displayName}`,
+ });
+ const deleteOpener = oauth.getByRole("button", { name: "Delete configuration" });
+ await deleteOpener.click();
+ const confirmation = oauth.getByRole("dialog", {
+ name: "Delete this OAuth configuration?",
+ });
+ const cancel = confirmation.getByRole("button", { name: "Cancel" });
+ await expectLocatorVisible(confirmation);
+ await expectLocatorFocused(cancel);
+ await cancel.press("Escape");
+ await expectLocatorCount(confirmation, 0);
+ await expectLocatorFocused(deleteOpener);
+ },
+ );
+ });
+ const tools = yield* Effect.promise(() => client.listTools({ sourceId: source.id }));
+ const tool = tools.items.find((candidate) => candidate.stableKey.endsWith("get_me"));
+ if (!tool) return yield* Effect.die("the managed OAuth source produced no tool");
+ const enabled = yield* Effect.promise(() => client.setToolMode(tool, "enabled"));
+ const token = yield* acquireToken(client, target.baseUrl, unique("oauth-agent"));
+ const callsBefore = (yield* Effect.promise(() => provider.ledger())).filter(
+ isSuccessfulMcpToolCall,
+ ).length;
+ const invoked = yield* Effect.promise(() =>
+ fetch(new URL("/api/v1/gateway/tools/invoke", target.baseUrl), {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token.token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ path: enabled.callablePath, arguments: {} }),
+ }),
+ );
+ expect(invoked.status, "the connected OAuth tool is callable").toBe(200);
+ expect(
+ (yield* Effect.promise(() => provider.ledger())).filter(isSuccessfulMcpToolCall).length,
+ "the emulator ledger records an authenticated MCP tool call",
+ ).toBeGreaterThan(callsBefore);
+ }),
+ Effect.promise(() => client.deleteSource(source.id)).pipe(Effect.ignore),
+ );
+ }),
+ ),
+);
diff --git a/e2e/local/cli-open-stale-manifest.test.ts b/e2e/local/cli-open-stale-manifest.test.ts
index c33aab461..b75a10778 100644
--- a/e2e/local/cli-open-stale-manifest.test.ts
+++ b/e2e/local/cli-open-stale-manifest.test.ts
@@ -67,7 +67,7 @@ scenario(
writeFileSync(openerPath, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
chmodSync(openerPath, 0o755);
- const { stdout, stderr } = await execFileAsync("bun", ["run", "dev:cli", "open"], {
+ const { stdout, stderr } = await execFileAsync("bun", ["run", "legacy:dev:cli", "open"], {
cwd: repoRoot,
env: {
...process.env,
diff --git a/e2e/local/cli-systemd-unit-escaping.test.ts b/e2e/local/cli-systemd-unit-escaping.test.ts
index ead36d92d..dae36ab77 100644
--- a/e2e/local/cli-systemd-unit-escaping.test.ts
+++ b/e2e/local/cli-systemd-unit-escaping.test.ts
@@ -4,7 +4,7 @@
import { expect } from "@effect/vitest";
import { Effect } from "effect";
-import { generateSystemdUnit } from "../../apps/cli/src/service";
+import { generateSystemdUnit } from "../../legacy/cli/src/service";
import { scenario } from "../src/scenario";
scenario(
diff --git a/e2e/local/cli-web-transition.test.ts b/e2e/local/cli-web-transition.test.ts
index c6865c416..a549b5854 100644
--- a/e2e/local/cli-web-transition.test.ts
+++ b/e2e/local/cli-web-transition.test.ts
@@ -25,7 +25,7 @@ scenario(
const root = mkdtempSync(join(tmpdir(), "executor-web-transition-"));
const dataDir = join(root, "data");
try {
- const { stdout, stderr } = await execFileAsync("bun", ["run", "dev:cli", "web"], {
+ const { stdout, stderr } = await execFileAsync("bun", ["run", "legacy:dev:cli", "web"], {
cwd: repoRoot,
env: { ...process.env, EXECUTOR_DATA_DIR: dataDir },
});
diff --git a/e2e/local/local-server.ts b/e2e/local/local-server.ts
index 818b272fb..b73f8a0cd 100644
--- a/e2e/local/local-server.ts
+++ b/e2e/local/local-server.ts
@@ -55,7 +55,7 @@ export const withLocalServer = (
yield* Effect.all(
[
cli.session(
- ["bun", "run", "dev:cli", "web", "--foreground", "--port", "0"],
+ ["bun", "run", "legacy:dev:cli", "web", "--foreground", "--port", "0"],
async (term) => {
markRecordingStart(runDir, "terminal");
markFocus(runDir, "terminal");
diff --git a/e2e/local/vite-dev-routing.test.ts b/e2e/local/vite-dev-routing.test.ts
index 287eb3f8c..1d6de523d 100644
--- a/e2e/local/vite-dev-routing.test.ts
+++ b/e2e/local/vite-dev-routing.test.ts
@@ -1,4 +1,4 @@
-// Local-only: plain `apps/local` Vite dev must route the same local-only HTTP
+// Local-only: plain `legacy/local` Vite dev must route the same local-only HTTP
// surfaces as production `executor web`. These routes live outside the typed
// `/api` HttpApi: `/api/health`, `/api/oauth/await/*`, and browser approval's
// `/api/mcp-sessions/*`.
@@ -19,7 +19,7 @@ import { scenario } from "../src/scenario";
import { waitForHttp } from "../setup/boot";
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
-const localAppDir = join(repoRoot, "apps/local");
+const localAppDir = join(repoRoot, "legacy/local");
const APPROVAL_TARGET_TOOL = "executor.coreTools.policies.list";
const EXECUTE_CODE = `
@@ -221,7 +221,7 @@ scenario(
expect(
failures,
- "plain apps/local Vite dev should special-case the same local-only routes as production",
+ "plain legacy/local Vite dev should special-case the same local-only routes as production",
).toEqual([]);
});
}),
diff --git a/e2e/package.json b/e2e/package.json
index 06370f939..a702c27c4 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -5,18 +5,20 @@
"type": "module",
"scripts": {
"cli": "bun scripts/cli.ts",
- "test": "vitest run --project cloud && vitest run --project selfhost",
- "test:cloud": "vitest run --project cloud",
- "test:selfhost": "vitest run --project selfhost",
- "test:selfhost-docker": "vitest run --project selfhost-docker",
- "test:cloudflare": "vitest run --project cloudflare",
- "test:watch": "vitest",
+ "test": "vitest run --project local-selfhost",
+ "test:local": "vitest run --project local-selfhost",
+ "test:unit": "vitest run --config vitest.unit.config.ts",
+ "test:legacy:cloud": "vitest run --config vitest.legacy.config.ts --project cloud",
+ "test:legacy:selfhost": "vitest run --config vitest.legacy.config.ts --project selfhost",
+ "test:legacy:selfhost-docker": "vitest run --config vitest.legacy.config.ts --project selfhost-docker",
+ "test:legacy:cloudflare": "vitest run --config vitest.legacy.config.ts --project cloudflare",
+ "test:watch": "vitest --project local-selfhost",
"ports": "bun scripts/ports.ts",
"summary": "bun scripts/summary.ts",
"viewer:build": "bun scripts/rebuild-viewer.ts",
"serve": "bun scripts/rebuild-viewer.ts && bun scripts/serve.ts",
"typecheck": "tsc --noEmit",
- "test:desktop": "vitest run --project desktop",
+ "test:legacy:desktop": "vitest run --config vitest.legacy.config.ts --project desktop",
"motel": "MOTEL_OTEL_BASE_URL=http://127.0.0.1:4796 MOTEL_OTEL_DB_PATH=runs/.motel/telemetry.sqlite motel server"
},
"dependencies": {
diff --git a/e2e/scripts/ports.ts b/e2e/scripts/ports.ts
index 1724a41d9..5a3d61428 100644
--- a/e2e/scripts/ports.ts
+++ b/e2e/scripts/ports.ts
@@ -10,6 +10,7 @@ import {
WORKOS_EMULATOR_PORT,
} from "../targets/cloud";
import { SELFHOST_PORT } from "../targets/selfhost";
+import { LOCAL_SELFHOST_PORT, LOCAL_SETUP_PORT } from "../targets/local-selfhost";
import { repoRoot } from "../src/ports";
console.log(`preferred e2e ports for ${repoRoot}`);
@@ -18,3 +19,5 @@ console.log(` cloud dev-db ${CLOUD_DB_PORT}`);
console.log(` workos emulator ${WORKOS_EMULATOR_PORT}`);
console.log(` autumn emulator ${AUTUMN_EMULATOR_PORT}`);
console.log(` selfhost http://localhost:${SELFHOST_PORT}`);
+console.log(` local executor http://127.0.0.1:${LOCAL_SELFHOST_PORT}`);
+console.log(` first boot http://127.0.0.1:${LOCAL_SETUP_PORT}`);
diff --git a/e2e/setup/cloud.boot.ts b/e2e/setup/cloud.boot.ts
index 24e7cd9c9..b5c072c6f 100644
--- a/e2e/setup/cloud.boot.ts
+++ b/e2e/setup/cloud.boot.ts
@@ -12,7 +12,7 @@ import { createEmulator } from "@executor-js/emulate";
import { bootProcesses, waitForHttp } from "./boot";
-export const cloudDir = fileURLToPath(new URL("../../apps/cloud/", import.meta.url));
+export const cloudDir = fileURLToPath(new URL("../../legacy/cloud/", import.meta.url));
export interface CloudBootOptions {
readonly cloudPort: number;
diff --git a/e2e/setup/cloudflare.boot.ts b/e2e/setup/cloudflare.boot.ts
index df58a54b7..8020b2f1d 100644
--- a/e2e/setup/cloudflare.boot.ts
+++ b/e2e/setup/cloudflare.boot.ts
@@ -11,9 +11,11 @@ import { promisify } from "node:util";
import { bootProcesses, waitForHttp, type BootedProcesses } from "./boot";
-export const cloudflareDir = fileURLToPath(new URL("../../apps/host-cloudflare/", import.meta.url));
+export const cloudflareDir = fileURLToPath(
+ new URL("../../legacy/host-cloudflare/", import.meta.url),
+);
const wranglerBin = fileURLToPath(
- new URL("../../apps/host-cloudflare/node_modules/.bin/wrangler", import.meta.url),
+ new URL("../../legacy/host-cloudflare/node_modules/.bin/wrangler", import.meta.url),
);
export interface CloudflareBootOptions {
diff --git a/e2e/setup/desktop-packaged.globalsetup.ts b/e2e/setup/desktop-packaged.globalsetup.ts
index 14a0f3b7a..acef3452c 100644
--- a/e2e/setup/desktop-packaged.globalsetup.ts
+++ b/e2e/setup/desktop-packaged.globalsetup.ts
@@ -16,7 +16,7 @@ import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
-const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
+const appDir = fileURLToPath(new URL("../../legacy/desktop/", import.meta.url));
// (launch exe, bundled executor binary) inside the packaged bundle, per platform.
const bundlePaths = (): { exe: string; executor: string } => {
diff --git a/e2e/setup/desktop.globalsetup.ts b/e2e/setup/desktop.globalsetup.ts
index ab2d74a2d..4108e1c1d 100644
--- a/e2e/setup/desktop.globalsetup.ts
+++ b/e2e/setup/desktop.globalsetup.ts
@@ -7,7 +7,7 @@ import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
-const appsDesktop = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));
+const appsDesktop = fileURLToPath(new URL("../../legacy/desktop/", import.meta.url));
export default function setup() {
execFileSync("bun", ["run", "--filter", "@executor-js/local", "build"], {
diff --git a/e2e/setup/local-selfhost.globalsetup.ts b/e2e/setup/local-selfhost.globalsetup.ts
new file mode 100644
index 000000000..d104a2338
--- /dev/null
+++ b/e2e/setup/local-selfhost.globalsetup.ts
@@ -0,0 +1,161 @@
+import { spawn, type ChildProcess } from "node:child_process";
+import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { resolve } from "node:path";
+
+import { claimPorts, repoRoot } from "../src/ports";
+import { LOCAL_ADMIN } from "../targets/local-selfhost";
+import { waitForHttp } from "./boot";
+
+interface BootedExecutor {
+ readonly child: ChildProcess;
+ readonly dataDir: string;
+ readonly setupToken: string;
+}
+
+const stop = async (process: ChildProcess) => {
+ if (process.pid === undefined || process.exitCode !== null) return;
+ const exited = new Promise((resolveExit) => process.once("exit", () => resolveExit()));
+ try {
+ globalThis.process.kill(-process.pid, "SIGTERM");
+ } catch {
+ process.kill("SIGTERM");
+ }
+ const graceful = await Promise.race([
+ exited.then(() => true),
+ new Promise((resolveGrace) => setTimeout(() => resolveGrace(false), 5_000)),
+ ]);
+ if (!graceful) {
+ try {
+ globalThis.process.kill(-process.pid, "SIGKILL");
+ } catch {
+ process.kill("SIGKILL");
+ }
+ await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, 2_000))]);
+ }
+};
+
+const bootExecutor = async (
+ binary: string,
+ port: number,
+ label: string,
+): Promise => {
+ const dataDir = mkdtempSync(`${tmpdir()}/executor-e2e-${label}-`);
+ const stdioTemplatesFile = resolve(dataDir, "mcp-stdio-templates.json");
+ writeFileSync(
+ stdioTemplatesFile,
+ JSON.stringify({
+ templates: [
+ {
+ name: "executor-e2e-stdio",
+ executable: process.execPath,
+ cwd: repoRoot,
+ arguments: [resolve(repoRoot, "e2e/fixtures/stdio-mcp-server.mjs")],
+ environment: {},
+ secretEnvironment: ["EXECUTOR_E2E_STDIO_SECRET"],
+ },
+ ],
+ }),
+ );
+ const child = spawn(
+ binary,
+ [
+ "server",
+ "--bind",
+ `127.0.0.1:${port}`,
+ "--data-dir",
+ dataDir,
+ "--public-origin",
+ `http://127.0.0.1:${port}`,
+ "--mcp-stdio-templates",
+ stdioTemplatesFile,
+ ],
+ { cwd: repoRoot, detached: true, stdio: ["ignore", "pipe", "pipe"] },
+ );
+ let output = "";
+ let collectingSetupLink = true;
+ const capture = (chunk: Buffer) => {
+ if (!collectingSetupLink) return;
+ output = `${output}${chunk.toString("utf8")}`.slice(-64 * 1024);
+ };
+ child.stdout?.on("data", capture);
+ child.stderr?.on("data", capture);
+
+ try {
+ await waitForHttp(`http://127.0.0.1:${port}/api/v1/bootstrap`, { timeoutMs: 30_000 });
+ const deadline = Date.now() + 10_000;
+ while (!output.match(/\/setup#token=([^\s]+)/) && Date.now() < deadline) {
+ await new Promise((resolveWait) => setTimeout(resolveWait, 50));
+ }
+ const setupToken = output.match(/\/setup#token=([^\s]+)/)?.[1];
+ if (!setupToken) throw new Error(`could not read setup token from ${label} server output`);
+ collectingSetupLink = false;
+ output = "";
+ return { child, dataDir, setupToken };
+ } catch (error) {
+ await stop(child);
+ rmSync(dataDir, { recursive: true, force: true });
+ throw error;
+ }
+};
+
+const completeSetup = async (baseUrl: string, setupToken: string) => {
+ const response = await fetch(new URL("/api/v1/setup", baseUrl), {
+ method: "POST",
+ headers: { "content-type": "application/json", origin: new URL(baseUrl).origin },
+ body: JSON.stringify({ setupToken, ...LOCAL_ADMIN }),
+ });
+ if (!response.ok) {
+ throw new Error(`could not prepare local selfhost administrator (${response.status})`);
+ }
+};
+
+export default async function setup(): Promise<() => Promise> {
+ if (process.env.E2E_LOCAL_SELFHOST_URL || process.env.E2E_LOCAL_SETUP_URL) {
+ throw new Error(
+ "The complete local e2e suite needs two fresh instances. Run without E2E_LOCAL_SELFHOST_URL and E2E_LOCAL_SETUP_URL.",
+ );
+ }
+ const binary = resolve(
+ process.env.E2E_EXECUTOR_BIN ?? resolve(repoRoot, "target/debug/executor"),
+ );
+ if (!existsSync(binary)) {
+ throw new Error(
+ `Executor binary not found at ${binary}. Run the root test:e2e command so the Svelte assets and Rust binary are prepared first.`,
+ );
+ }
+ process.env.E2E_EXECUTOR_BIN = binary;
+
+ const { ports, release } = await claimPorts([
+ { envVar: "E2E_LOCAL_SELFHOST_PORT", offset: 4, label: "Rust local selfhost" },
+ { envVar: "E2E_LOCAL_SETUP_PORT", offset: 5, label: "Rust first-boot setup" },
+ { envVar: "E2E_LOCAL_EMULATOR_A_PORT", offset: 6, label: "local emulator A" },
+ { envVar: "E2E_LOCAL_EMULATOR_B_PORT", offset: 7, label: "local emulator B" },
+ { envVar: "E2E_LOCAL_EMULATOR_C_PORT", offset: 8, label: "local emulator C" },
+ ]);
+ const mainPort = ports.E2E_LOCAL_SELFHOST_PORT!;
+ const setupPort = ports.E2E_LOCAL_SETUP_PORT!;
+ const main = await bootExecutor(binary, mainPort, "main");
+ let firstBoot: BootedExecutor | undefined;
+ try {
+ await completeSetup(`http://127.0.0.1:${mainPort}`, main.setupToken);
+ firstBoot = await bootExecutor(binary, setupPort, "setup");
+ process.env.E2E_LOCAL_SELFHOST_URL = `http://127.0.0.1:${mainPort}`;
+ process.env.E2E_LOCAL_SETUP_URL = `http://127.0.0.1:${setupPort}`;
+ process.env.E2E_LOCAL_SETUP_TOKEN = firstBoot.setupToken;
+ } catch (error) {
+ await stop(main.child);
+ if (firstBoot) await stop(firstBoot.child);
+ rmSync(main.dataDir, { recursive: true, force: true });
+ if (firstBoot) rmSync(firstBoot.dataDir, { recursive: true, force: true });
+ await release();
+ throw error;
+ }
+
+ return async () => {
+ await Promise.all([stop(main.child), stop(firstBoot.child)]);
+ rmSync(main.dataDir, { recursive: true, force: true });
+ rmSync(firstBoot.dataDir, { recursive: true, force: true });
+ await release();
+ };
+}
diff --git a/e2e/setup/selfhost-docker.boot.ts b/e2e/setup/selfhost-docker.boot.ts
index 67f49d0fb..0390b66a2 100644
--- a/e2e/setup/selfhost-docker.boot.ts
+++ b/e2e/setup/selfhost-docker.boot.ts
@@ -1,5 +1,5 @@
// Boot recipe for the selfhost PRODUCTION Docker artifact: build the image
-// from this checkout's apps/host-selfhost/Dockerfile (or use
+// from this checkout's legacy/host-selfhost/Dockerfile (or use
// E2E_SELFHOST_DOCKER_IMAGE, e.g. a published ghcr tag), then run it.
//
// The container runs with HOST networking, for the same reason the dev-server
@@ -56,8 +56,8 @@ const resolveImage = async (logFile?: string): Promise => {
const pinned = process.env.E2E_SELFHOST_DOCKER_IMAGE;
if (pinned) return pinned;
const image = "executor-selfhost:e2e";
- log(logFile, `building ${image} from ${repoRoot}apps/host-selfhost/Dockerfile`);
- await exec("docker", ["build", "-f", "apps/host-selfhost/Dockerfile", "-t", image, "."], {
+ log(logFile, `building ${image} from ${repoRoot}legacy/host-selfhost/Dockerfile`);
+ await exec("docker", ["build", "-f", "legacy/host-selfhost/Dockerfile", "-t", image, "."], {
cwd: repoRoot,
maxBuffer: 64 * 1024 * 1024,
}).catch((error: { stdout?: string; stderr?: string }) => {
diff --git a/e2e/setup/selfhost.boot.ts b/e2e/setup/selfhost.boot.ts
index 6f0d743d7..b1be07e00 100644
--- a/e2e/setup/selfhost.boot.ts
+++ b/e2e/setup/selfhost.boot.ts
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
import { bootProcesses, waitForHttp, type BootedProcesses } from "./boot";
-export const selfhostDir = fileURLToPath(new URL("../../apps/host-selfhost/", import.meta.url));
+export const selfhostDir = fileURLToPath(new URL("../../legacy/host-selfhost/", import.meta.url));
export interface SelfhostBootOptions {
readonly port: number;
diff --git a/e2e/src/local-admin.test.ts b/e2e/src/local-admin.test.ts
new file mode 100644
index 000000000..de2f24b60
--- /dev/null
+++ b/e2e/src/local-admin.test.ts
@@ -0,0 +1,215 @@
+import { createHash, randomBytes } from "node:crypto";
+import { createServer } from "node:http";
+
+import { expect, it } from "@effect/vitest";
+import { Effect } from "effect";
+
+import { LocalAdminClient } from "./local-admin";
+
+const digest = (value: string) => createHash("sha256").update(value).digest("hex");
+
+it.effect("reuses one administrator session across API and browser identities", () =>
+ Effect.acquireUseRelease(
+ Effect.promise(async () => {
+ const session = randomBytes(32).toString("hex");
+ const csrf = randomBytes(32).toString("hex");
+ let observedCookie: string | undefined;
+ const server = createServer((request, response) => {
+ request.resume();
+ if (request.method === "POST" && request.url === "/api/v1/session") {
+ response.statusCode = 200;
+ response.setHeader("content-type", "application/json");
+ response.setHeader("set-cookie", [
+ `executor_session=${session}; Path=/; HttpOnly; SameSite=Lax`,
+ `executor_csrf=${csrf}; Path=/; SameSite=Lax`,
+ ]);
+ response.end("{}");
+ return;
+ }
+ if (request.method === "GET" && request.url === "/api/v1/sources") {
+ observedCookie = request.headers.cookie;
+ response.statusCode = 200;
+ response.setHeader("content-type", "application/json");
+ response.end('{"sources":[]}');
+ return;
+ }
+ response.statusCode = 404;
+ response.end();
+ });
+ await new Promise((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", resolve);
+ });
+ const address = server.address();
+ if (address === null || typeof address === "string") {
+ throw new Error("administrator test server has no TCP address");
+ }
+ return {
+ origin: `http://127.0.0.1:${address.port}`,
+ server,
+ observedCookie: () => observedCookie,
+ };
+ }),
+ ({ origin, observedCookie }) =>
+ Effect.gen(function* () {
+ const client = yield* Effect.promise(() =>
+ LocalAdminClient.signIn(origin, "admin", "test-password"),
+ );
+ const identity = client.asIdentity("admin");
+ yield* Effect.promise(() => client.listSources());
+
+ expect(identity.label).toBe("admin");
+ expect(identity.credentials).toBeUndefined();
+ expect(identity.cookies?.map((cookie) => cookie.name).sort()).toEqual([
+ "executor_csrf",
+ "executor_session",
+ ]);
+ expect(
+ identity.cookies?.map((cookie) => ({
+ name: cookie.name,
+ valueLength: cookie.value.length,
+ })),
+ ).toEqual([
+ { name: "executor_session", valueLength: 64 },
+ { name: "executor_csrf", valueLength: 64 },
+ ]);
+
+ const apiCookie = observedCookie();
+ const identityCookie = identity.headers?.cookie;
+ const browserCookie = identity.cookies
+ ?.map((cookie) => `${cookie.name}=${cookie.value}`)
+ .join("; ");
+ if (!apiCookie || !identityCookie || !browserCookie) {
+ return yield* Effect.die("administrator identity omitted its session cookies");
+ }
+ expect(digest(identityCookie), "the browser header keeps API session A").toBe(
+ digest(apiCookie),
+ );
+ expect(digest(browserCookie), "Playwright receives the same session A cookie pairs").toBe(
+ digest(apiCookie),
+ );
+ }),
+ ({ server }) =>
+ Effect.callback((resume) => {
+ server.close((error) => resume(error ? Effect.fail(error) : Effect.succeed(undefined)));
+ }),
+ ),
+);
+
+it.effect("refreshes a tool revision once after an optimistic update conflict", () =>
+ Effect.acquireUseRelease(
+ Effect.promise(async () => {
+ const session = randomBytes(32).toString("hex");
+ const csrf = randomBytes(32).toString("hex");
+ const updateBodies: string[] = [];
+ const server = createServer((request, response) => {
+ if (request.method === "POST" && request.url === "/api/v1/session") {
+ request.resume();
+ response.statusCode = 200;
+ response.setHeader("content-type", "application/json");
+ response.setHeader("set-cookie", [
+ `executor_session=${session}; Path=/; HttpOnly; SameSite=Lax`,
+ `executor_csrf=${csrf}; Path=/; SameSite=Lax`,
+ ]);
+ response.end("{}");
+ return;
+ }
+ if (request.method === "GET" && request.url === "/api/v1/tools/tool-1") {
+ request.resume();
+ response.statusCode = 200;
+ response.setHeader("content-type", "application/json");
+ response.end(
+ JSON.stringify({
+ id: "tool-1",
+ sourceId: "source-1",
+ stableKey: "hello",
+ displayName: "Hello",
+ callablePath: "tools.source_1.hello",
+ sandboxPath: "executor.tools.source_1.hello",
+ revision: 2,
+ effectiveMode: { mode: "disabled" },
+ }),
+ );
+ return;
+ }
+ if (request.method === "PATCH" && request.url === "/api/v1/tools/tool-1/mode") {
+ request.setEncoding("utf8");
+ let body = "";
+ request.on("data", (chunk) => {
+ body += chunk;
+ });
+ request.on("end", () => {
+ updateBodies.push(body);
+ response.setHeader("content-type", "application/json");
+ if (updateBodies.length === 1) {
+ response.statusCode = 409;
+ response.end('{"error":{"code":"revision_conflict","message":"Refresh and retry."}}');
+ return;
+ }
+ response.statusCode = 200;
+ response.end(
+ JSON.stringify({
+ id: "tool-1",
+ sourceId: "source-1",
+ stableKey: "hello",
+ displayName: "Hello",
+ callablePath: "tools.source_1.hello",
+ sandboxPath: "executor.tools.source_1.hello",
+ revision: 3,
+ effectiveMode: { mode: "enabled" },
+ }),
+ );
+ });
+ return;
+ }
+ request.resume();
+ response.statusCode = 404;
+ response.end();
+ });
+ await new Promise((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", resolve);
+ });
+ const address = server.address();
+ if (address === null || typeof address === "string") {
+ throw new Error("administrator test server has no TCP address");
+ }
+ return {
+ origin: `http://127.0.0.1:${address.port}`,
+ server,
+ updateBodies,
+ };
+ }),
+ ({ origin, updateBodies }) =>
+ Effect.gen(function* () {
+ const client = yield* Effect.promise(() =>
+ LocalAdminClient.signIn(origin, "admin", "test-password"),
+ );
+ const updated = yield* Effect.promise(() =>
+ client.setToolMode(
+ {
+ id: "tool-1",
+ sourceId: "source-1",
+ stableKey: "hello",
+ displayName: "Hello",
+ callablePath: "tools.source_1.hello",
+ sandboxPath: "executor.tools.source_1.hello",
+ revision: 1,
+ effectiveMode: { mode: "disabled" },
+ },
+ "enabled",
+ ),
+ );
+
+ expect(updated.revision).toBe(3);
+ expect(updateBodies.map((body) => JSON.parse(body))).toEqual([
+ { mode: "enabled", expectedRevision: 1 },
+ { mode: "enabled", expectedRevision: 2 },
+ ]);
+ }),
+ ({ server }) =>
+ Effect.callback((resume) => {
+ server.close((error) => resume(error ? Effect.fail(error) : Effect.succeed(undefined)));
+ }),
+ ),
+);
diff --git a/e2e/src/local-admin.ts b/e2e/src/local-admin.ts
new file mode 100644
index 000000000..44f692a23
--- /dev/null
+++ b/e2e/src/local-admin.ts
@@ -0,0 +1,222 @@
+import type { Identity } from "./target";
+
+export interface LocalTool {
+ readonly id: string;
+ readonly sourceId: string;
+ readonly stableKey: string;
+ readonly displayName: string;
+ readonly callablePath: string;
+ readonly sandboxPath: string;
+ readonly revision: number;
+ readonly effectiveMode: { readonly mode: "enabled" | "ask" | "disabled" };
+}
+
+export interface LocalSource {
+ readonly id: string;
+ readonly slug: string;
+ readonly displayName: string;
+ readonly revision: number;
+ readonly toolCount: number;
+}
+
+export interface LocalToken {
+ readonly id: string;
+ readonly token: string;
+}
+
+export class LocalAdminClient {
+ readonly #origin: string;
+ readonly #cookie: string;
+ readonly #cookiePairs: readonly string[];
+ readonly #csrf: string;
+
+ private constructor(origin: string, cookiePairs: readonly string[], csrf: string) {
+ this.#origin = origin;
+ this.#cookiePairs = [...cookiePairs];
+ this.#cookie = cookiePairs.join("; ");
+ this.#csrf = csrf;
+ }
+
+ static async signIn(origin: string, username: string, password: string) {
+ const response = await fetch(new URL("/api/v1/session", origin), {
+ method: "POST",
+ headers: { "content-type": "application/json", origin: new URL(origin).origin },
+ body: JSON.stringify({ username, password }),
+ });
+ if (!response.ok) throw new Error(`administrator sign-in failed (${response.status})`);
+ const pairs = response.headers
+ .getSetCookie()
+ .map((cookie) => cookie.split(";", 1)[0]?.trim())
+ .filter((cookie): cookie is string => Boolean(cookie));
+ const csrf = pairs
+ .find((cookie) => cookie.startsWith("executor_csrf="))
+ ?.slice("executor_csrf=".length);
+ if (pairs.length === 0 || !csrf)
+ throw new Error("administrator sign-in returned no CSRF cookie");
+ return new LocalAdminClient(origin, pairs, csrf);
+ }
+
+ asIdentity(label: string): Identity {
+ return {
+ label,
+ headers: { cookie: this.#cookie },
+ cookies: this.#cookiePairs.map((pair) => {
+ const separator = pair.indexOf("=");
+ if (separator <= 0) throw new Error("administrator sign-in returned an invalid cookie");
+ return { name: pair.slice(0, separator), value: pair.slice(separator + 1) };
+ }),
+ };
+ }
+
+ async createToken(name: string) {
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
+ const idempotencyKey = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
+ return this.#json("/api/v1/tokens", {
+ method: "POST",
+ body: { name },
+ headers: { "Idempotency-Key": idempotencyKey },
+ });
+ }
+
+ async revokeToken(tokenId: string) {
+ await this.#request(`/api/v1/tokens/${encodeURIComponent(tokenId)}`, { method: "DELETE" });
+ }
+
+ async createSource(input: unknown) {
+ return this.#json("/api/v1/sources", { method: "POST", body: input });
+ }
+
+ async listSources() {
+ return this.#json<{ readonly sources: readonly LocalSource[] }>("/api/v1/sources");
+ }
+
+ async deleteSource(sourceId: string) {
+ await this.#request(`/api/v1/sources/${encodeURIComponent(sourceId)}`, { method: "DELETE" });
+ }
+
+ async refreshSource(sourceId: string) {
+ await this.#request(`/api/v1/sources/${encodeURIComponent(sourceId)}/refresh`, {
+ method: "POST",
+ body: {},
+ });
+ }
+
+ async listTools(query: { readonly sourceId?: string; readonly q?: string } = {}) {
+ const parameters = new URLSearchParams({ limit: "100" });
+ if (query.sourceId) parameters.set("sourceId", query.sourceId);
+ if (query.q) parameters.set("query", query.q);
+ return this.#json<{ readonly items: readonly LocalTool[]; readonly catalogRevision: number }>(
+ `/api/v1/tools?${parameters}`,
+ );
+ }
+
+ async setToolMode(tool: LocalTool, mode: "enabled" | "ask" | "disabled" | null) {
+ const toolPath = `/api/v1/tools/${encodeURIComponent(tool.id)}`;
+ const modePath = `${toolPath}/mode`;
+ const response = await this.#send(modePath, {
+ method: "PATCH",
+ body: { mode, expectedRevision: tool.revision },
+ });
+ if (response.status !== 409) {
+ return this.#readJson(modePath, "PATCH", response);
+ }
+
+ const current = await this.#json(toolPath);
+ return this.#json(modePath, {
+ method: "PATCH",
+ body: { mode, expectedRevision: current.revision },
+ });
+ }
+
+ async putOAuthConnection(sourceId: string, credentialKey: string, input: unknown) {
+ return this.#json<{ readonly revision: number; readonly callbackUrl: string }>(
+ `/api/v1/sources/${encodeURIComponent(sourceId)}/oauth/${encodeURIComponent(credentialKey)}`,
+ { method: "PUT", body: input },
+ );
+ }
+
+ async authorizeOAuthConnection(
+ sourceId: string,
+ credentialKey: string,
+ expectedRevision: number,
+ ) {
+ return this.#json<{ readonly authorizationUrl: string }>(
+ `/api/v1/sources/${encodeURIComponent(sourceId)}/oauth/${encodeURIComponent(credentialKey)}/authorize`,
+ { method: "POST", body: { expectedRevision } },
+ );
+ }
+
+ async getOAuthConnections(sourceId: string) {
+ return this.#json<{
+ readonly connections: ReadonlyArray<{
+ readonly credentialKey: string;
+ readonly status: string;
+ readonly callbackUrl: string;
+ }>;
+ readonly availableCredentials: ReadonlyArray<{ readonly credentialKey: string }>;
+ }>(`/api/v1/sources/${encodeURIComponent(sourceId)}/oauth`);
+ }
+
+ async #json(
+ path: string,
+ options: {
+ readonly method?: string;
+ readonly body?: unknown;
+ readonly headers?: Readonly>;
+ } = {},
+ ) {
+ const method = options.method ?? "GET";
+ const response = await this.#send(path, options);
+ return this.#readJson(path, method, response);
+ }
+
+ async #request(
+ path: string,
+ options: {
+ readonly method?: string;
+ readonly body?: unknown;
+ readonly headers?: Readonly>;
+ } = {},
+ ) {
+ const method = options.method ?? "GET";
+ const response = await this.#send(path, options);
+ if (!response.ok) {
+ throw new Error(`${method} ${path} failed (${response.status}): ${await response.text()}`);
+ }
+ return response;
+ }
+
+ async #send(
+ path: string,
+ options: {
+ readonly method?: string;
+ readonly body?: unknown;
+ readonly headers?: Readonly>;
+ } = {},
+ ) {
+ const method = options.method ?? "GET";
+ const headers = new Headers({
+ accept: "application/json",
+ cookie: this.#cookie,
+ ...options.headers,
+ });
+ if (options.body !== undefined) headers.set("content-type", "application/json");
+ if (!["GET", "HEAD", "OPTIONS"].includes(method)) {
+ headers.set("origin", new URL(this.#origin).origin);
+ headers.set("x-executor-csrf", this.#csrf);
+ }
+ const response = await fetch(new URL(path, this.#origin), {
+ method,
+ headers,
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
+ });
+ return response;
+ }
+
+ async #readJson(path: string, method: string, response: Response) {
+ if (!response.ok) {
+ throw new Error(`${method} ${path} failed (${response.status}): ${await response.text()}`);
+ }
+ return (await response.json()) as Value;
+ }
+}
diff --git a/e2e/src/locator-assertions.test.ts b/e2e/src/locator-assertions.test.ts
new file mode 100644
index 000000000..19ba2e75c
--- /dev/null
+++ b/e2e/src/locator-assertions.test.ts
@@ -0,0 +1,27 @@
+import { it } from "@effect/vitest";
+
+import {
+ expectLocatorChecked,
+ expectLocatorCount,
+ expectLocatorDisabled,
+ expectLocatorFocused,
+ expectLocatorText,
+ expectLocatorValue,
+ expectLocatorVisible,
+} from "./locator-assertions";
+
+const sequence = (first: Value, ...rest: readonly Value[]) => {
+ const values = [first, ...rest];
+ let index = 0;
+ return async () => values[Math.min(index++, values.length - 1)]!;
+};
+
+it("polls locator state until the user-visible assertion succeeds", async () => {
+ await expectLocatorVisible({ isVisible: sequence(false, true) });
+ await expectLocatorDisabled({ isDisabled: sequence(false, true) });
+ await expectLocatorChecked({ isChecked: sequence(false, true) });
+ await expectLocatorFocused({ evaluate: sequence(false, true) });
+ await expectLocatorValue({ inputValue: sequence("pending", "ready") }, "ready");
+ await expectLocatorCount({ count: sequence(0, 2) }, 2);
+ await expectLocatorText({ textContent: sequence("Pending", "Ready\n now") }, "Ready now");
+});
diff --git a/e2e/src/locator-assertions.ts b/e2e/src/locator-assertions.ts
new file mode 100644
index 000000000..57f04e41d
--- /dev/null
+++ b/e2e/src/locator-assertions.ts
@@ -0,0 +1,76 @@
+import { expect } from "@effect/vitest";
+import type { Locator } from "playwright";
+
+const locatorAssertionTimeout = 30_000;
+
+const pollOptions = (message: string | undefined, fallback: string) => ({
+ timeout: locatorAssertionTimeout,
+ message: message ?? fallback,
+});
+
+const normalizeText = (text: string) => text.replace(/\s+/gu, " ").trim();
+
+interface FocusLocator {
+ readonly evaluate: (
+ pageFunction: (element: HTMLElement | SVGElement) => boolean,
+ ) => Promise;
+}
+
+export const expectLocatorVisible = (locator: Pick, message?: string) =>
+ expect
+ .poll(() => locator.isVisible(), pollOptions(message, "expected locator to become visible"))
+ .toBe(true);
+
+export const expectLocatorDisabled = (locator: Pick, message?: string) =>
+ expect
+ .poll(() => locator.isDisabled(), pollOptions(message, "expected locator to become disabled"))
+ .toBe(true);
+
+export const expectLocatorChecked = (locator: Pick, message?: string) =>
+ expect
+ .poll(() => locator.isChecked(), pollOptions(message, "expected locator to become checked"))
+ .toBe(true);
+
+export const expectLocatorFocused = (locator: FocusLocator, message?: string) =>
+ expect
+ .poll(
+ () => locator.evaluate((element) => element === element.ownerDocument.activeElement),
+ pollOptions(message, "expected locator to become focused"),
+ )
+ .toBe(true);
+
+export const expectLocatorValue = (
+ locator: Pick,
+ expected: string,
+ message?: string,
+) =>
+ expect
+ .poll(
+ () => locator.inputValue(),
+ pollOptions(message, `expected locator value to equal ${JSON.stringify(expected)}`),
+ )
+ .toBe(expected);
+
+export const expectLocatorCount = (
+ locator: Pick,
+ expected: number,
+ message?: string,
+) =>
+ expect
+ .poll(
+ () => locator.count(),
+ pollOptions(message, `expected locator count to equal ${expected}`),
+ )
+ .toBe(expected);
+
+export const expectLocatorText = (
+ locator: Pick,
+ expected: string,
+ message?: string,
+) =>
+ expect
+ .poll(
+ () => locator.textContent().then((text) => normalizeText(text ?? "")),
+ pollOptions(message, `expected locator text to contain ${JSON.stringify(expected)}`),
+ )
+ .toContain(normalizeText(expected));
diff --git a/e2e/src/oauth-test-provider.test.ts b/e2e/src/oauth-test-provider.test.ts
new file mode 100644
index 000000000..405ab9d0e
--- /dev/null
+++ b/e2e/src/oauth-test-provider.test.ts
@@ -0,0 +1,49 @@
+import { expect, it } from "@effect/vitest";
+import { Effect } from "effect";
+
+import { serveOAuthTestProvider } from "./oauth-test-provider";
+
+it.effect("uses the MCP emulator's RFC 8414 and dynamic client surfaces", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const provider = yield* serveOAuthTestProvider();
+ expect(new URL(provider.issuer).hostname).toBe("127.0.0.1");
+ expect(provider.endpoint).toBe(`${provider.issuer}/mcp`);
+ const metadata = yield* Effect.promise(() =>
+ fetch(`${provider.issuer}/.well-known/oauth-authorization-server`).then((response) =>
+ response.json(),
+ ),
+ );
+ expect(metadata).toMatchObject({
+ issuer: provider.issuer,
+ response_types_supported: ["code"],
+ code_challenge_methods_supported: ["S256"],
+ });
+
+ const clientId = yield* Effect.promise(() =>
+ provider.registerClient("http://127.0.0.1/callback"),
+ );
+ expect(clientId).toMatch(/^mcp-client-/);
+ const authorization = new URL(`${provider.issuer}/authorize`);
+ authorization.searchParams.set("client_id", clientId);
+ authorization.searchParams.set("redirect_uri", "http://127.0.0.1/callback");
+ authorization.searchParams.set("state", "unit-state");
+ authorization.searchParams.set("response_type", "code");
+ const callbackUrl = yield* Effect.promise(() =>
+ provider.approveAuthorization(authorization.toString(), "admin"),
+ );
+ const callback = new URL(callbackUrl);
+ expect(`${callback.origin}${callback.pathname}`).toBe("http://127.0.0.1/callback");
+ expect(callback.searchParams.get("state")).toBe("unit-state");
+ expect(/^[0-9a-f]+$/.test(callback.searchParams.get("code") ?? "")).toBe(true);
+ const ledger = yield* Effect.promise(() => provider.ledger());
+ expect(ledger.map((entry) => entry.path)).toEqual(
+ expect.arrayContaining(["/register", "/authorize", "/authorize/approve"]),
+ );
+ const approval = ledger.find((entry) => entry.path === "/authorize/approve");
+ expect(approval?.method).toBe("POST");
+ expect(approval?.request.body).toMatchObject({ login: "admin", state: "unit-state" });
+ expect(approval?.response.status).toBe(302);
+ }),
+ ),
+);
diff --git a/e2e/src/oauth-test-provider.ts b/e2e/src/oauth-test-provider.ts
new file mode 100644
index 000000000..da9b42f47
--- /dev/null
+++ b/e2e/src/oauth-test-provider.ts
@@ -0,0 +1,115 @@
+import { createEmulator, type Emulator, type LedgerEntry } from "@executor-js/emulate";
+import { Effect } from "effect";
+
+export interface OAuthTestProvider {
+ readonly issuer: string;
+ readonly endpoint: string;
+ readonly registerClient: (redirectUri: string) => Promise;
+ readonly approveAuthorization: (authorizationUrl: string, login: string) => Promise;
+ readonly ledger: () => Promise>;
+}
+
+const DEFAULT_EMULATOR_PORT = 4000;
+
+const decodeHtmlAttribute = (value: string) =>
+ value
+ .replaceAll(""", '"')
+ .replaceAll("'", "'")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll("&", "&");
+
+const authorizationFormFields = (html: string) => {
+ const fields = new URLSearchParams();
+ for (const match of html.matchAll(
+ / /giu,
+ )) {
+ const [, name, value] = match;
+ if (name !== undefined && value !== undefined) {
+ fields.append(decodeHtmlAttribute(name), decodeHtmlAttribute(value));
+ }
+ }
+ if (!fields.has("client_id") || !fields.has("redirect_uri") || !fields.has("state")) {
+ throw new Error("MCP emulator authorization page returned no usable consent form");
+ }
+ return fields;
+};
+
+export const approveOAuthTestAuthorization = async (
+ issuer: string,
+ authorizationUrl: string,
+ login: string,
+) => {
+ const authorization = new URL(authorizationUrl);
+ if (authorization.origin !== new URL(issuer).origin) {
+ throw new Error("MCP emulator authorization URL uses an unexpected origin");
+ }
+ const consent = await fetch(authorization, {
+ headers: { accept: "text/html" },
+ redirect: "manual",
+ });
+ if (!consent.ok) {
+ throw new Error(`MCP emulator authorization page failed (${consent.status})`);
+ }
+ const fields = authorizationFormFields(await consent.text());
+ fields.set("login", login);
+ const approval = await fetch(new URL("/authorize/approve", issuer), {
+ method: "POST",
+ headers: { "content-type": "application/x-www-form-urlencoded" },
+ body: fields,
+ redirect: "manual",
+ });
+ if (approval.status !== 302) {
+ throw new Error(`MCP emulator authorization approval failed (${approval.status})`);
+ }
+ const location = approval.headers.get("location");
+ if (!location) {
+ throw new Error("MCP emulator authorization approval returned no callback URL");
+ }
+ return new URL(location, issuer).toString();
+};
+
+export const serveOAuthTestProvider = (port?: number) =>
+ Effect.acquireRelease(
+ Effect.promise(async (): Promise<{ provider: OAuthTestProvider; emulator: Emulator }> => {
+ const resolvedPort = port ?? DEFAULT_EMULATOR_PORT;
+ const baseUrl = `http://127.0.0.1:${resolvedPort}`;
+ const emulator = await createEmulator({
+ service: "mcp",
+ port: resolvedPort,
+ baseUrl,
+ });
+ return {
+ emulator,
+ provider: {
+ issuer: emulator.url,
+ endpoint: `${emulator.url}/mcp`,
+ registerClient: async (redirectUri) => {
+ const response = await fetch(`${emulator.url}/register`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ client_name: "Executor local e2e",
+ redirect_uris: [redirectUri],
+ grant_types: ["authorization_code"],
+ response_types: ["code"],
+ token_endpoint_auth_method: "none",
+ }),
+ });
+ if (!response.ok) {
+ throw new Error(`MCP emulator client registration failed (${response.status})`);
+ }
+ const registration = (await response.json()) as { readonly client_id?: string };
+ if (!registration.client_id) {
+ throw new Error("MCP emulator client registration returned no client_id");
+ }
+ return registration.client_id;
+ },
+ approveAuthorization: (authorizationUrl, login) =>
+ approveOAuthTestAuthorization(emulator.url, authorizationUrl, login),
+ ledger: () => emulator.ledger.list(),
+ },
+ };
+ }),
+ ({ emulator }) => Effect.promise(() => emulator.close()).pipe(Effect.ignore),
+ ).pipe(Effect.map(({ provider }) => provider));
diff --git a/e2e/src/scenario-redaction.test.ts b/e2e/src/scenario-redaction.test.ts
new file mode 100644
index 000000000..d3ed0e608
--- /dev/null
+++ b/e2e/src/scenario-redaction.test.ts
@@ -0,0 +1,36 @@
+import { expect, it } from "@effect/vitest";
+
+import { redactFailureText } from "./scenario";
+
+it("removes setup, API, cookie, CSRF, and password credentials from persisted failures", () => {
+ const secret = [
+ "http://127.0.0.1/setup#token=setup-secret",
+ "Authorization: Bearer bearer-secret",
+ "exr_token-secret",
+ "Cookie: executor_session=session-secret; executor_csrf=csrf-secret",
+ "x-executor-csrf: csrf-secret",
+ '{"password":"password-secret","setupToken":"setup-secret"}',
+ "http://127.0.0.1/callback?code=code-secret&state=state-secret&code_verifier=verifier-secret&code_challenge=challenge-secret",
+ '{"access_token":"access-secret","refresh_token":"refresh-secret","clientSecret":"client-secret","apiToken":"api-secret"}',
+ ].join("\n");
+ const redacted = redactFailureText(secret);
+ for (const value of [
+ "setup-secret",
+ "bearer-secret",
+ "token-secret",
+ "session-secret",
+ "csrf-secret",
+ "password-secret",
+ "code-secret",
+ "state-secret",
+ "verifier-secret",
+ "challenge-secret",
+ "access-secret",
+ "refresh-secret",
+ "client-secret",
+ "api-secret",
+ ]) {
+ expect(redacted).not.toContain(value);
+ }
+ expect(redacted).toContain("[REDACTED]");
+});
diff --git a/e2e/src/scenario.ts b/e2e/src/scenario.ts
index cb233c237..cfbb3cdd6 100644
--- a/e2e/src/scenario.ts
+++ b/e2e/src/scenario.ts
@@ -208,10 +208,22 @@ const missingServices = (cause: Cause.Cause): ReadonlyArray =>
};
const failureMessage = (cause: Cause.Cause): string => {
- const rendered = String(Cause.squash(cause));
+ const rendered = redactFailureText(String(Cause.squash(cause)));
return rendered.length > 2_000 ? `${rendered.slice(0, 2_000)}…` : rendered;
};
+export const redactFailureText = (text: string): string =>
+ text
+ .replace(/(#token=)[^\s"'<>]+/giu, "$1[REDACTED]")
+ .replace(/([?&](?:code|state|code_verifier|code_challenge)=)[^\s"'<>]+/giu, "$1[REDACTED]")
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]+=*/giu, "$1[REDACTED]")
+ .replace(/\bex[er]_[A-Za-z0-9._~-]+\b/gu, "exr_[REDACTED]")
+ .replace(
+ /("(?:password|setupToken|csrfToken|token|apiToken|access_token|refresh_token|clientSecret|client_secret|code_verifier|code_challenge)"\s*:\s*")[^"]*(")/giu,
+ "$1[REDACTED]$2",
+ )
+ .replace(/\b(cookie|set-cookie|x-executor-csrf):\s*[^\r\n]+/giu, "$1: [REDACTED]");
+
/** The *.test.ts file that called scenario(), from the registration stack. */
const captureTestFile = (): string | undefined => {
const stack = new Error().stack ?? "";
diff --git a/e2e/src/source-panel.test.ts b/e2e/src/source-panel.test.ts
new file mode 100644
index 000000000..79ef49c70
--- /dev/null
+++ b/e2e/src/source-panel.test.ts
@@ -0,0 +1,77 @@
+import { expect, it } from "@effect/vitest";
+
+import { openConnectSourcePanel, type SourcePanelPage } from "./source-panel";
+
+const sourcePanelSelector = "details.import-panel";
+const sourcePickerSelector = 'role:group[name="Source type"]';
+const sourceSummarySelector = 'text:exact="Connect a source"';
+
+const fixture = (initiallyOpen: boolean, autoOpenBeforeFirstClick = false) => {
+ const calls: string[] = [];
+ let panelOpen = initiallyOpen;
+ let pendingAutoOpen = autoOpenBeforeFirstClick;
+
+ const makeLocator = (selector: string) => ({
+ async waitFor(options: { readonly state: "visible" }) {
+ calls.push(`wait:${selector}:${options.state}`);
+ },
+ async getAttribute(name: string) {
+ calls.push(`attribute:${selector}:${name}`);
+ return selector === sourcePanelSelector && name === "open" && panelOpen ? "" : null;
+ },
+ async click() {
+ calls.push(`click:${selector}`);
+ if (pendingAutoOpen) {
+ pendingAutoOpen = false;
+ panelOpen = true;
+ }
+ panelOpen = !panelOpen;
+ },
+ });
+
+ const page = {
+ getByRole: () => makeLocator(sourcePickerSelector),
+ getByText: () => makeLocator(sourceSummarySelector),
+ locator: (selector: string) => makeLocator(selector),
+ } satisfies SourcePanelPage;
+
+ return { calls, page };
+};
+
+it("does not click a source panel that is visible once its picker is attached", async () => {
+ const { calls, page } = fixture(true);
+
+ await openConnectSourcePanel(page);
+
+ expect(calls).toEqual([
+ `attribute:${sourcePanelSelector}:open`,
+ `wait:${sourcePickerSelector}:visible`,
+ ]);
+});
+
+it("opens an attached source picker when its panel is closed", async () => {
+ const { calls, page } = fixture(false);
+
+ await openConnectSourcePanel(page);
+
+ expect(calls).toEqual([
+ `attribute:${sourcePanelSelector}:open`,
+ `click:${sourceSummarySelector}`,
+ `attribute:${sourcePanelSelector}:open`,
+ `wait:${sourcePickerSelector}:visible`,
+ ]);
+});
+
+it("reopens a panel that auto-opens between the open-state check and click", async () => {
+ const { calls, page } = fixture(false, true);
+
+ await openConnectSourcePanel(page);
+
+ expect(calls).toEqual([
+ `attribute:${sourcePanelSelector}:open`,
+ `click:${sourceSummarySelector}`,
+ `attribute:${sourcePanelSelector}:open`,
+ `click:${sourceSummarySelector}`,
+ `wait:${sourcePickerSelector}:visible`,
+ ]);
+});
diff --git a/e2e/src/source-panel.ts b/e2e/src/source-panel.ts
new file mode 100644
index 000000000..c68ccde3a
--- /dev/null
+++ b/e2e/src/source-panel.ts
@@ -0,0 +1,30 @@
+interface SourcePanelLocator {
+ readonly waitFor: (options: { readonly state: "visible" }) => Promise;
+ readonly getAttribute: (name: string) => Promise;
+ readonly click: () => Promise;
+}
+
+export interface SourcePanelPage {
+ readonly getByRole: (
+ role: "group",
+ options: { readonly name: "Source type" },
+ ) => SourcePanelLocator;
+ readonly getByText: (
+ text: "Connect a source",
+ options: { readonly exact: true },
+ ) => SourcePanelLocator;
+ readonly locator: (selector: string) => SourcePanelLocator;
+}
+
+const sourcePanelSelector = "details.import-panel";
+
+export const openConnectSourcePanel = async (page: SourcePanelPage) => {
+ const panel = page.locator(sourcePanelSelector);
+ if ((await panel.getAttribute("open")) === null) {
+ await page.getByText("Connect a source", { exact: true }).click();
+ if ((await panel.getAttribute("open")) === null) {
+ await page.getByText("Connect a source", { exact: true }).click();
+ }
+ }
+ await page.getByRole("group", { name: "Source type" }).waitFor({ state: "visible" });
+};
diff --git a/e2e/src/surfaces/browser.ts b/e2e/src/surfaces/browser.ts
index 2cc24903e..43742165c 100644
--- a/e2e/src/surfaces/browser.ts
+++ b/e2e/src/surfaces/browser.ts
@@ -26,6 +26,15 @@ export interface BrowserSurface {
identity: Identity,
drive: (session: BrowserSession) => Promise,
) => Effect.Effect;
+ /**
+ * Drive credential entry without persisting network bodies, DOM snapshots,
+ * or video. Step screenshots are still written after each action, so callers
+ * must leave the page in a secret-free state before a step returns.
+ */
+ readonly privateSession: (
+ identity: Identity,
+ drive: (session: BrowserSession) => Promise,
+ ) => Effect.Effect;
}
const slug = (text: string): string =>
@@ -37,8 +46,12 @@ const slug = (text: string): string =>
// acquireUseRelease so a vitest timeout (fiber interruption) still closes the
// browser and flushes video + trace — a bare promise would leak Chromium.
-export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface => ({
- session: (identity, drive) =>
+export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface => {
+ const session = (
+ identity: Identity,
+ drive: (session: BrowserSession) => Promise,
+ record: boolean,
+ ) =>
Effect.acquireUseRelease(
Effect.promise(async () => {
const videoTmp = join(dir, ".video-tmp");
@@ -66,14 +79,16 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
const context = await browser.newContext({
colorScheme: "dark",
viewport: { width: 1280, height: 800 },
- recordVideo: { dir: videoTmp, size: { width: 1280, height: 800 } },
+ ...(record ? { recordVideo: { dir: videoTmp, size: { width: 1280, height: 800 } } } : {}),
baseURL: target.baseUrl,
});
- await context.tracing.start({
- screenshots: true,
- snapshots: true,
- sources: true,
- });
+ if (record) {
+ await context.tracing.start({
+ screenshots: true,
+ snapshots: true,
+ sources: true,
+ });
+ }
if (identity.cookies?.length) {
await context.addCookies(
identity.cookies.map((cookie) => ({
@@ -85,13 +100,15 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
const page = await context.newPage();
// The session video's clock starts with the page; anchor it for the
// run's focus timeline (scripts/film.ts cuts on these).
- markRecordingStart(dir, "browser");
+ if (record) markRecordingStart(dir, "browser");
// Main-frame navigations feed the viewer's synthetic URL bar — the
// recording itself is chromeless, so this is the only place the
// address the developer "typed" survives.
- page.on("framenavigated", (frame) => {
- if (frame === page.mainFrame()) markNavigation(dir, frame.url());
- });
+ if (record) {
+ page.on("framenavigated", (frame) => {
+ if (frame === page.mainFrame()) markNavigation(dir, frame.url());
+ });
+ }
// Harvest distributed-trace ids: every app API request carries a W3C
// traceparent (Effect's HttpClient), and each id names one
// click→server→DB trace in whatever OTLP store the run exported to
@@ -145,11 +162,11 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
// filming, enterFocus lingers a beat on whatever the developer was
// looking at before tabbing here.
await enterFocus(dir, "browser");
- await context.tracing.group(label);
+ if (record) await context.tracing.group(label);
try {
await action(page);
} finally {
- await context.tracing.groupEnd();
+ if (record) await context.tracing.groupEnd();
}
await page.screenshot({
path: join(dir, `${String(shots.count++).padStart(2, "0")}-${slug(label)}.png`),
@@ -162,14 +179,18 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
await drive({ page, step });
} catch (error) {
// Freeze the scene: the artifact dir shows the screen at failure.
- await page.screenshot({ path: join(dir, "failure.png") }).catch(() => {});
+ if (record) {
+ await page.screenshot({ path: join(dir, "failure.png") }).catch(() => {});
+ }
throw error;
}
}),
({ browser, context, page, videoTmp, traceIds }) =>
Effect.promise(async () => {
appendTraces(dir, traceIds);
- await context.tracing.stop({ path: join(dir, "trace.zip") }).catch(() => {});
+ if (record) {
+ await context.tracing.stop({ path: join(dir, "trace.zip") }).catch(() => {});
+ }
const video = page.video();
await context.close(); // flushes the recording
await browser.close();
@@ -199,5 +220,10 @@ export const makeBrowserSurface = (dir: string, target: Target): BrowserSurface
}
rmSync(videoTmp, { recursive: true, force: true });
}),
- ),
-});
+ );
+
+ return {
+ session: (identity, drive) => session(identity, drive, true),
+ privateSession: (identity, drive) => session(identity, drive, false),
+ };
+};
diff --git a/e2e/src/vm/build-binary.ts b/e2e/src/vm/build-binary.ts
index b5d32c26d..78f7779cb 100644
--- a/e2e/src/vm/build-binary.ts
+++ b/e2e/src/vm/build-binary.ts
@@ -1,6 +1,6 @@
// Build the compiled `executor` for a guest os/arch. `service install` refuses
// to run from a dev (.ts) entrypoint, so the VM targets need a real binary —
-// produced via the `--target` flag on apps/cli/src/build.ts.
+// produced via the `--target` flag on legacy/cli/src/build.ts.
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
@@ -15,7 +15,7 @@ const PLATFORM_TAG: Record = { macos: "darwin", linux: "linux", wi
// e2e/src/vm/build-binary.ts → repo root.
const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
-const CLI_DIR = path.join(REPO_ROOT, "apps", "cli");
+const CLI_DIR = path.join(REPO_ROOT, "legacy", "cli");
/**
* Build the `executor` binary for a guest and return its `bin` directory
diff --git a/e2e/targets/cloudflare.ts b/e2e/targets/cloudflare.ts
index abce7ddee..e137de6c4 100644
--- a/e2e/targets/cloudflare.ts
+++ b/e2e/targets/cloudflare.ts
@@ -1,4 +1,4 @@
-// The Cloudflare self-host app (apps/host-cloudflare) as a target: the REAL
+// The Cloudflare self-host app (legacy/host-cloudflare) as a target: the REAL
// worker on workerd via Miniflare (wrangler `unstable_dev`) with a local D1 +
// R2, booted in setup/cloudflare.globalsetup.ts. Dev-auth is on, so every
// request is the fixed dev admin — no per-identity login and no MCP OAuth (the
diff --git a/e2e/targets/local-selfhost.ts b/e2e/targets/local-selfhost.ts
new file mode 100644
index 000000000..ff4fc72e5
--- /dev/null
+++ b/e2e/targets/local-selfhost.ts
@@ -0,0 +1,53 @@
+import { Effect } from "effect";
+
+import { e2ePort } from "../src/ports";
+import type { Identity, Target } from "../src/target";
+
+export const LOCAL_SELFHOST_PORT = e2ePort("E2E_LOCAL_SELFHOST_PORT", 4);
+export const LOCAL_SETUP_PORT = e2ePort("E2E_LOCAL_SETUP_PORT", 5);
+export const LOCAL_SELFHOST_BASE_URL =
+ process.env.E2E_LOCAL_SELFHOST_URL ?? `http://127.0.0.1:${LOCAL_SELFHOST_PORT}`;
+export const LOCAL_SETUP_BASE_URL =
+ process.env.E2E_LOCAL_SETUP_URL ?? `http://127.0.0.1:${LOCAL_SETUP_PORT}`;
+
+export const LOCAL_ADMIN = {
+ username: process.env.E2E_LOCAL_ADMIN_USERNAME ?? "admin",
+ password: process.env.E2E_LOCAL_ADMIN_PASSWORD ?? "executor-e2e-admin-password",
+};
+
+const cookiePairs = (headers: Headers) =>
+ (headers.getSetCookie?.() ?? [])
+ .map((cookie) => cookie.split(";", 1)[0]?.trim())
+ .filter((cookie): cookie is string => Boolean(cookie));
+
+export const signInLocalAdmin = async (
+ baseUrl: string,
+ credentials = LOCAL_ADMIN,
+): Promise => {
+ const response = await fetch(new URL("/api/v1/session", baseUrl), {
+ method: "POST",
+ headers: { "content-type": "application/json", origin: new URL(baseUrl).origin },
+ body: JSON.stringify(credentials),
+ redirect: "manual",
+ });
+ if (!response.ok) throw new Error(`local selfhost sign-in failed (${response.status})`);
+ const pairs = cookiePairs(response.headers);
+ if (pairs.length === 0) throw new Error("local selfhost sign-in returned no cookies");
+ return {
+ label: credentials.username,
+ credentials: { email: credentials.username, password: credentials.password },
+ headers: { cookie: pairs.join("; ") },
+ cookies: pairs.map((pair) => {
+ const separator = pair.indexOf("=");
+ return { name: pair.slice(0, separator), value: pair.slice(separator + 1) };
+ }),
+ };
+};
+
+export const localSelfhostTarget = (): Target => ({
+ name: "local-selfhost",
+ baseUrl: LOCAL_SELFHOST_BASE_URL,
+ mcpUrl: `${LOCAL_SELFHOST_BASE_URL}/mcp`,
+ capabilities: new Set(["browser"]),
+ newIdentity: () => Effect.promise(() => signInLocalAdmin(LOCAL_SELFHOST_BASE_URL)),
+});
diff --git a/e2e/targets/registry.ts b/e2e/targets/registry.ts
index 94e966746..1074a4088 100644
--- a/e2e/targets/registry.ts
+++ b/e2e/targets/registry.ts
@@ -7,6 +7,7 @@ import { cloudTarget } from "./cloud";
import { cloudflareTarget } from "./cloudflare";
import { desktopTarget } from "./desktop";
import { localTarget } from "./local";
+import { localSelfhostTarget } from "./local-selfhost";
import { selfhostTarget } from "./selfhost";
import { selfhostDockerTarget } from "./selfhost-docker";
@@ -20,6 +21,7 @@ const factories: Record Target> = {
// `desktop` — no standard surfaces to carry. See desktop-packaged.globalsetup.
"desktop-packaged": desktopTarget,
local: localTarget,
+ "local-selfhost": localSelfhostTarget,
// The supervised CLI daemon inside a VM, one project per guest OS — restart()
// is a real reboot. See setup/cli.globalsetup.ts.
"cli-macos": cliTarget,
diff --git a/e2e/targets/selfhost-docker.ts b/e2e/targets/selfhost-docker.ts
index 1c3d9dc30..bf1e556a2 100644
--- a/e2e/targets/selfhost-docker.ts
+++ b/e2e/targets/selfhost-docker.ts
@@ -1,5 +1,5 @@
// The PRODUCTION self-host artifact as a target: the Docker image from
-// apps/host-selfhost/Dockerfile (production Vite build, `bun src/serve.ts`,
+// legacy/host-selfhost/Dockerfile (production Vite build, `bun src/serve.ts`,
// /data volume) instead of the dev server. Same surface as the selfhost
// target — same bootstrap admin, same Better Auth sign-in, same MCP consent —
// so the whole scenario suite runs against what users actually deploy. Boot
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
index 38d1e7965..200558998 100644
--- a/e2e/tsconfig.json
+++ b/e2e/tsconfig.json
@@ -10,5 +10,15 @@
"jsx": "react-jsx",
"types": ["node"]
},
- "include": ["src", "cloud", "scenarios", "selfhost", "setup", "targets", "scripts", "viewer/src"]
+ "include": [
+ "src",
+ "cloud",
+ "scenarios",
+ "selfhost",
+ "local-selfhost",
+ "setup",
+ "targets",
+ "scripts",
+ "viewer/src"
+ ]
}
diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts
index 6f008b459..83bb5a22e 100644
--- a/e2e/vitest.config.ts
+++ b/e2e/vitest.config.ts
@@ -1,109 +1,19 @@
import { defineConfig } from "vitest/config";
-// One project per target. Same scenario files, different running instance:
-// `vitest run --project cloud` / `--project selfhost` (or both, the default).
-// Each project's globalsetup boots that app's OWN dev server (or attaches to
-// E2E__URL). Scenarios are isolated by fresh identities, not resets.
-const project = (name: string, overrides: Record = {}) => ({
- test: {
- name,
- include: ["scenarios/**/*.test.ts", `${name}/**/*.test.ts`],
- env: { E2E_TARGET: name },
- globalSetup: [`./setup/${name}.globalsetup.ts`],
- testTimeout: 180_000,
- hookTimeout: 120_000,
- ...overrides,
- },
-});
-
export default defineConfig({
test: {
projects: [
- // PGlite's socket server is effectively single-connection; parallel test
- // files (each fanning out per-request postgres sockets) crash it. Run
- // files serially — swap PGlite for real Postgres if wall-clock matters.
- project("cloud", { fileParallelism: false }),
- // selfhost identities are the shared bootstrap admin for now — run files
- // serially until per-test invite-signup isolation lands.
- project("selfhost", { fileParallelism: false }),
- // The same app as the PRODUCTION Docker artifact (the image users
- // deploy: production build, bun serve.ts, /data volume) instead of the
- // dev server. Runs the cross-target scenarios AND the selfhost/**
- // scenarios — it is the same single-tenant app, so they all apply.
- // Needs a docker daemon with host-networking support (Engine ≥ 26 on
- // Docker Desktop); not part of the default `npm run test` chain — run
- // with `npm run test:selfhost-docker` (release gate + CI for the
- // publish workflow).
- project("selfhost-docker", {
- include: ["scenarios/**/*.test.ts", "selfhost/**/*.test.ts"],
- fileParallelism: false,
- }),
- // The Cloudflare self-host worker (workerd via wrangler dev, dev-auth).
- // Scoped to the cross-target scenarios wired for this host; the rest of
- // scenarios/** is not yet validated against the worker. The full-graph
- // scenario is included on purpose: workerd's 128MB isolate is the exact
- // limit the streaming compile + content-addressed serve path defends, so
- // it is the one host where that regression must be proven. Shares
- // self-host's single-admin model.
- project("cloudflare", {
- include: [
- "scenarios/browser-approval.test.ts",
- "scenarios/microsoft-graph-full.test.ts",
- "scenarios/toolkits-mcp.test.ts",
- "cloudflare/**/*.test.ts",
- ],
- fileParallelism: false,
- }),
- // The Electron desktop app. Only desktop/** scenarios — the desktop
- // target provides none of the standard surfaces (each scenario
- // launches its own app via Playwright's electron driver), so running
- // the cross-target suite here would just emit a page of skips. Needs
- // a display; not part of the default `npm run test` chain.
- project("desktop", {
- include: ["desktop/**/*.test.ts"],
- fileParallelism: false,
- testTimeout: 300_000,
- }),
- // The PACKAGED desktop app: the real electron-builder bundle, where
- // app.isPackaged is true — the ONLY target that exercises the supervised-
- // daemon attach path (ensureSupervisedConnection) and the bundled executor.
- // Its globalsetup builds the bundle (slow), so it's separate from
- // `desktop` to keep the fast dev-electron suite off the package build.
- // Needs a display; not part of the default `npm run test` chain — run with
- // `vitest run --project desktop-packaged`.
- project("desktop-packaged", {
- include: ["desktop-packaged/**/*.test.ts"],
- fileParallelism: false,
- testTimeout: 360_000,
- hookTimeout: 600_000,
- }),
- // The single-user local app. Each scenario launches its OWN `executor
- // web` via the CLI on a throwaway data dir + an OS-assigned port, so
- // there is no shared instance and scenarios are independent — file
- // parallelism is ON. No globalSetup (nothing shared to boot). Only
- // local/** scenarios. Not part of the default `npm run test` chain; run
- // with `vitest run --project local`.
- project("local", {
- include: ["local/**/*.test.ts"],
- globalSetup: [],
- fileParallelism: true,
- testTimeout: 180_000,
- }),
- // The supervised CLI daemon inside a guest VM, one project per OS. The
- // globalsetup provisions a VM, `executor service install`s the daemon, and
- // tunnels it; restart() reboots the guest for REAL, so restart-persistence
- // proves the boot-time auto-start path. Needs tart (macOS/Linux) or an EC2
- // credential (Windows); not part of the default `npm run test` chain — run
- // with `vitest run --project cli-macos` (etc.) on the Mini.
- ...(["macos", "linux", "windows"] as const).map((os) =>
- project(`cli-${os}`, {
- include: ["scenarios/restart-persistence.test.ts", "cli/**/*.test.ts"],
- env: { E2E_TARGET: `cli-${os}`, E2E_VM_OS: os },
+ {
+ test: {
+ name: "local-selfhost",
+ include: ["local-selfhost/**/*.test.ts"],
+ env: { E2E_TARGET: "local-selfhost" },
+ globalSetup: ["./setup/local-selfhost.globalsetup.ts"],
fileParallelism: false,
- testTimeout: 300_000,
- hookTimeout: 900_000,
- }),
- ),
+ testTimeout: 180_000,
+ hookTimeout: 120_000,
+ },
+ },
],
},
});
diff --git a/e2e/vitest.legacy.config.ts b/e2e/vitest.legacy.config.ts
new file mode 100644
index 000000000..94a5ef1f3
--- /dev/null
+++ b/e2e/vitest.legacy.config.ts
@@ -0,0 +1,60 @@
+import { defineConfig } from "vitest/config";
+
+const project = (name: string, overrides: Record = {}) => ({
+ test: {
+ name,
+ include: ["scenarios/**/*.test.ts", `${name}/**/*.test.ts`],
+ env: { E2E_TARGET: name },
+ globalSetup: [`./setup/${name}.globalsetup.ts`],
+ testTimeout: 180_000,
+ hookTimeout: 120_000,
+ ...overrides,
+ },
+});
+
+export default defineConfig({
+ test: {
+ projects: [
+ project("cloud", { fileParallelism: false }),
+ project("selfhost", { fileParallelism: false }),
+ project("selfhost-docker", {
+ include: ["scenarios/**/*.test.ts", "selfhost/**/*.test.ts"],
+ fileParallelism: false,
+ }),
+ project("cloudflare", {
+ include: [
+ "scenarios/browser-approval.test.ts",
+ "scenarios/microsoft-graph-full.test.ts",
+ "scenarios/toolkits-mcp.test.ts",
+ "cloudflare/**/*.test.ts",
+ ],
+ fileParallelism: false,
+ }),
+ project("desktop", {
+ include: ["desktop/**/*.test.ts"],
+ fileParallelism: false,
+ testTimeout: 300_000,
+ }),
+ project("desktop-packaged", {
+ include: ["desktop-packaged/**/*.test.ts"],
+ fileParallelism: false,
+ testTimeout: 360_000,
+ hookTimeout: 600_000,
+ }),
+ project("local", {
+ include: ["local/**/*.test.ts"],
+ globalSetup: [],
+ fileParallelism: true,
+ }),
+ ...(["macos", "linux"] as const).map((os) =>
+ project(`cli-${os}`, {
+ include: ["scenarios/restart-persistence.test.ts", "cli/**/*.test.ts"],
+ env: { E2E_TARGET: `cli-${os}`, E2E_VM_OS: os },
+ fileParallelism: false,
+ testTimeout: 300_000,
+ hookTimeout: 900_000,
+ }),
+ ),
+ ],
+ },
+});
diff --git a/apps/cli/vitest.config.ts b/e2e/vitest.unit.config.ts
similarity index 100%
rename from apps/cli/vitest.config.ts
rename to e2e/vitest.unit.config.ts
diff --git a/knip.config.ts b/knip.config.ts
index b823e3b4f..522f2166f 100644
--- a/knip.config.ts
+++ b/knip.config.ts
@@ -3,14 +3,16 @@ import type { KnipConfig } from "knip";
const config: KnipConfig = {
workspaces: {
".": {},
- "apps/cli": {},
- "apps/local": {
+ "legacy/cli": {},
+ "legacy/local": {
entry: ["src/server.ts", "src/routes/**/*.tsx", "src/server/*.ts"],
},
- "apps/cloud": {
+ "legacy/host-cloudflare": {},
+ "legacy/host-selfhost": {},
+ "legacy/cloud": {
entry: ["src/server.ts", "src/routes/**/*.tsx", "src/server/*.ts"],
},
- "apps/desktop": {
+ "legacy/desktop": {
entry: ["src/preload.ts"],
},
"apps/marketing": {
diff --git a/legacy/README.md b/legacy/README.md
new file mode 100644
index 000000000..2b82eab09
--- /dev/null
+++ b/legacy/README.md
@@ -0,0 +1,27 @@
+# Legacy products
+
+This directory contains all previous TypeScript application entry points. The
+active product is the local/self-hosted Rust server and Svelte dashboard in the
+repository root and `web/`.
+
+The legacy packages remain in the Bun workspace so their dependency graph and
+historical end-to-end scenarios stay reproducible. They are excluded from the
+default development, test, typecheck, lint, format, and CI paths. Use the root
+`legacy:dev`, `legacy:dev:cli`, `legacy:test`, or `legacy:typecheck` scripts when
+working on them explicitly. `legacy:dev` covers the five packages with a real
+`dev` script; the archived CLI uses `legacy:dev:cli`. They are not the
+foundation for new product work.
+
+- `cli/`: the archived npm CLI compatibility package
+- `local/`: the archived local React server and UI bundled by that CLI
+- `host-cloudflare/`: the archived self-hosted Cloudflare Worker
+- `host-selfhost/`: the archived TypeScript self-hosted server and container
+- `cloud/`: the previous managed Cloudflare deployment
+- `desktop/`: the previous Electron desktop deployment
+
+The shared TypeScript packages under `packages/` remain outside this directory.
+They are retained independently from these deployment entry points.
+
+Cloudflare previews and TypeScript package publishing remain manual or
+repository-variable-gated compatibility workflows. They do not participate in
+the native product release path.
diff --git a/apps/cli/CHANGELOG.md b/legacy/cli/CHANGELOG.md
similarity index 100%
rename from apps/cli/CHANGELOG.md
rename to legacy/cli/CHANGELOG.md
diff --git a/apps/cli/bin/executor.ts b/legacy/cli/bin/executor.ts
similarity index 100%
rename from apps/cli/bin/executor.ts
rename to legacy/cli/bin/executor.ts
diff --git a/apps/cli/package.json b/legacy/cli/package.json
similarity index 87%
rename from apps/cli/package.json
rename to legacy/cli/package.json
index 6cd4ff4b0..dfc2f7a45 100644
--- a/apps/cli/package.json
+++ b/legacy/cli/package.json
@@ -15,9 +15,6 @@
"build:preview": "bun run src/build.ts preview",
"build:preview:tarball": "bun run src/build.ts preview-tarball",
"build:preview:wrapper": "bun run src/build.ts preview-wrapper",
- "build:publish": "bun run src/build.ts publish",
- "release:publish:dry-run": "bun run src/release.ts --dry-run",
- "release:publish": "bun run src/release.ts",
"test": "bun --bun vitest run",
"typecheck": "tsgo --noEmit",
"typecheck:slow": "tsc --noEmit"
diff --git a/apps/cli/src/build.ts b/legacy/cli/src/build.ts
similarity index 84%
rename from apps/cli/src/build.ts
rename to legacy/cli/src/build.ts
index 162951fbd..309acc6e6 100644
--- a/apps/cli/src/build.ts
+++ b/legacy/cli/src/build.ts
@@ -7,8 +7,8 @@ import { parseArgs } from "node:util";
import { $ } from "bun";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
-const cliRoot = resolve(repoRoot, "apps/cli");
-const webRoot = resolve(repoRoot, "apps/local");
+const cliRoot = resolve(repoRoot, "legacy/cli");
+const webRoot = resolve(repoRoot, "legacy/local");
const distDir = resolve(cliRoot, "dist");
const resolveQuickJsWasmPath = (): string => {
@@ -188,7 +188,7 @@ const resolveLibsqlNative = (t: Target): string | null => {
if (!target) return null;
const pkg = `@libsql/${target}`;
try {
- const req = createRequire(join(repoRoot, "apps/local", "package.json"));
+ const req = createRequire(join(repoRoot, "legacy/local", "package.json"));
const pkgJson = req.resolve(`${pkg}/package.json`);
return join(dirname(pkgJson), "index.node");
} catch {
@@ -225,7 +225,7 @@ const buildWeb = async (mode: BuildMode) => {
};
// ---------------------------------------------------------------------------
-// Embedded web UI — generates a virtual module that imports all web assets
+// Embedded web UI: generates a virtual module that imports all web assets
// using `with { type: "file" }` so Bun bakes them into the compiled binary.
// ---------------------------------------------------------------------------
@@ -243,7 +243,7 @@ const createEmbeddedWebUISource = async (mode: BuildMode) => {
const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`);
return [
- "// Auto-generated — maps web UI paths to embedded file references",
+ "// Auto-generated: maps web UI paths to embedded file references",
...imports,
"export default {",
...entries,
@@ -252,7 +252,7 @@ const createEmbeddedWebUISource = async (mode: BuildMode) => {
};
// ---------------------------------------------------------------------------
-// Embedded legacy v1 drizzle migrations — inlined as text imports so drizzle's
+// Embedded legacy v1 drizzle migrations: inlined as text imports so drizzle's
// `migrate()` (which reads a folder from disk) can be given a tmpdir
// populated from the inlined contents at runtime. The v1→v2 data migration
// replays this chain to bring an older v1 database up to v1-final before
@@ -273,7 +273,7 @@ const createEmbeddedMigrationsSource = async () => {
const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`);
return [
- "// Auto-generated — maps migration paths to inlined file contents",
+ "// Auto-generated: maps migration paths to inlined file contents",
...imports,
"export default {",
...entries,
@@ -340,10 +340,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
},
});
- // Copy QuickJS WASM next to binary — loaded at runtime by the server
+ // Copy QuickJS WASM next to binary, loaded at runtime by the server
await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm"));
- // Copy @napi-rs/keyring native binding next to executor — bun --compile
+ // Copy @napi-rs/keyring native binding next to executor. bun --compile
// doesn't bundle .node files, so the loader needs to find it on disk
// via NAPI_RS_NATIVE_LIBRARY_PATH (set in main.ts).
const keyringNative = resolveKeyringNative(target);
@@ -351,7 +351,7 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
await cp(keyringNative, join(binDir, "keyring.node"));
}
- // Copy the libSQL native binding next to executor — same bunfs limitation
+ // Copy the libSQL native binding next to executor, with the same bunfs limitation
// as keyring; main.ts redirects `require('@libsql/')` to it.
const libsqlNative = resolveLibsqlNative(target);
if (libsqlNative && existsSync(libsqlNative)) {
@@ -371,10 +371,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
// `1.4.14-linux-x64`). The wrapper references each variant via an
// `npm:` alias in its optionalDependencies. This mirrors codex's
// pattern and avoids ever having to claim a new npm package name when
- // a new platform is added — a single trusted-publishing config on the
+ // a new platform is added. A single trusted-publishing config on the
// `executor` package covers everything.
//
- // No `bin` field on purpose — the wrapper's launcher resolves the
+ // No `bin` field on purpose. The wrapper's launcher resolves the
// platform binary via require.resolve on the alias name and execs it.
const tag = platformTag(target);
const variantVersion = `${meta.version}-${tag}`;
@@ -423,7 +423,7 @@ const buildWrapperPackage = async (binaries: Record) => {
await mkdir(binDir, { recursive: true });
- // Node.js launcher — resolves the platform binary via require.resolve
+ // Node.js launcher resolves the platform binary via require.resolve
// against optionalDependencies and execs it. No postinstall: the binary
// ships as an os/cpu-filtered optional dep, the launcher resolves it at
// runtime. This works whether or not the package manager runs postinstalls
@@ -450,7 +450,7 @@ const buildWrapperPackage = async (binaries: Record) => {
// npm/bun fetch only the variant matching the current os/cpu (set
// on each variant's package.json); everything else is a no-op.
// The local alias name on the left is what the launcher passes to
- // require.resolve at runtime — npm puts the variant at
+ // require.resolve at runtime. npm puts the variant at
// `node_modules/executor--/` so resolution just works.
optionalDependencies: binaries,
engines: {
@@ -473,7 +473,7 @@ const buildWrapperPackage = async (binaries: Record) => {
};
// ---------------------------------------------------------------------------
-// Preview wrapper — a slim npm package for pkg.pr.new previews that fetches
+// Preview wrapper: a slim npm package for pkg.pr.new previews that fetches
// the platform binary from our R2 bucket at install time.
//
// The release wrapper points postinstall at GitHub Releases. For previews
@@ -543,7 +543,7 @@ const buildPreviewWrapperPackage = async (targets: Target[]) => {
await writeFile(join(wrapperDir, "postinstall.cjs"), postinstall);
// Restrict os/cpu to platforms we actually built this run so npm refuses
- // the install on anything else — a clear error beats a cryptic 404 from
+ // the install on anything else. A clear error beats a cryptic 404 from
// postinstall on an unbuilt platform.
const osList = Array.from(new Set(targets.map((t) => t.os)));
const cpuList = Array.from(new Set(targets.map((t) => t.arch)));
@@ -578,117 +578,10 @@ const buildPreviewWrapperPackage = async (targets: Target[]) => {
}
console.log(`\nPreview wrapper: ${wrapperDir}`);
- console.log(` ${meta.name}@${meta.version} — CDN ${cdnUrl}/${sha}`);
+ console.log(` ${meta.name}@${meta.version}, CDN ${cdnUrl}/${sha}`);
console.log(` targets: ${targets.map(targetPackageName).join(", ")}`);
};
-// ---------------------------------------------------------------------------
-// Publish
-// ---------------------------------------------------------------------------
-
-const packageAlreadyPublished = async (pkgDir: string) => {
- const pkg = (await Bun.file(join(pkgDir, "package.json")).json()) as {
- name?: string;
- version?: string;
- };
-
- if (!pkg.name || !pkg.version) {
- throw new Error(`Missing name/version in ${join(pkgDir, "package.json")}`);
- }
-
- const proc = Bun.spawn(["npm", "view", `${pkg.name}@${pkg.version}`, "version"], {
- cwd: pkgDir,
- stdio: ["ignore", "ignore", "ignore"],
- });
-
- return (await proc.exited) === 0;
-};
-
-/** Recognize npm error output for "this version already exists." Used to
- * make publish retries idempotent — if a previous workflow run got partway
- * through and crashed, we should skip what's already on the registry and
- * push the rest. Mirrors codex's `rust-release.yml` skip pattern. */
-const ALREADY_PUBLISHED_RE = /previously published|cannot publish over|version already exists/i;
-
-const publishPackedPackage = async (pkgDir: string, channel: string) => {
- if (await packageAlreadyPublished(pkgDir)) {
- console.log(`Skipping ${pkgDir}; package version already exists on npm.`);
- return;
- }
-
- await $`bun pm pack`.cwd(pkgDir);
-
- const provenance = process.env.GITHUB_ACTIONS === "true" ? ["--provenance"] : [];
- const result = await $`npm publish *.tgz --access public --tag ${channel} ${provenance}`
- .cwd(pkgDir)
- .nothrow()
- .quiet();
-
- const stdout = result.stdout.toString();
- const stderr = result.stderr.toString();
- process.stdout.write(stdout);
- process.stderr.write(stderr);
-
- if (result.exitCode === 0) return;
-
- // The pre-check via `npm view` has a propagation race — even when an
- // earlier publish in this same run committed, `npm view` may 404 for a few
- // seconds. Treat "already published" errors as success on retry.
- if (ALREADY_PUBLISHED_RE.test(stderr) || ALREADY_PUBLISHED_RE.test(stdout)) {
- console.log(`Skipping ${pkgDir}; npm reported version already published.`);
- return;
- }
-
- throw new Error(`npm publish failed for ${pkgDir} (exit ${result.exitCode})`);
-};
-
-/** Extract the platform-tag suffix from a variant version string.
- * e.g. "1.4.14-linux-x64" -> "linux-x64".
- * Variants get a per-platform npm dist-tag (not `latest` or `beta`) so
- * publishing them doesn't move the channel pointer. The wrapper alone
- * drives `latest`/`beta`. */
-const variantTagFromVersion = (version: string): string => {
- const idx = version.indexOf("-");
- if (idx === -1) {
- throw new Error(
- `Variant version missing platform-tag suffix (expected ' -'): ${version}`,
- );
- }
- return version.slice(idx + 1);
-};
-
-const publish = async (channel: string) => {
- const meta = await readMetadata();
-
- // Variants publish first so the wrapper's optionalDependencies resolve.
- // All variants and the wrapper publish to the same npm package
- // (`executor`) — variants under platform-tagged versions, wrapper under
- // the channel tag. Single trusted-publishing config covers everything.
- const platformDirs: string[] = [];
- for (const entry of new Bun.Glob("executor-*/package.json").scanSync({ cwd: distDir })) {
- platformDirs.push(join(distDir, dirname(entry)));
- }
- platformDirs.sort();
-
- // Publish variants sequentially. They all PUT to the same npm package
- // (`executor`), and the registry rejects concurrent packument updates with
- // 409 Conflict ("Failed to save packument. A common cause is if you try to
- // publish a new package before the previous package has been fully
- // processed.") — that's exactly what happened on v1.4.14's first attempt,
- // where 3 of 8 raced through and the rest 409'd. Serial is plenty fast
- // (~5s per variant × 8 ≈ 40s).
- console.log(`Publishing ${platformDirs.length} platform variant(s)...`);
- for (const dir of platformDirs) {
- const pkg = (await Bun.file(join(dir, "package.json")).json()) as { version: string };
- const tag = variantTagFromVersion(pkg.version);
- await publishPackedPackage(dir, tag);
- }
-
- const wrapperDir = join(distDir, meta.name);
- console.log(`Publishing wrapper ${wrapperDir} under @${channel}...`);
- await publishPackedPackage(wrapperDir, channel);
-};
-
// ---------------------------------------------------------------------------
// GitHub release assets
// ---------------------------------------------------------------------------
@@ -704,7 +597,7 @@ const ZIP_ASSET_SCRIPT = [
const createReleaseAssets = async () => {
// The dir name (e.g. "executor-linux-x64") still encodes the platform tag.
- // Don't read `pkg.name` here — under the npm:alias pattern every variant's
+ // Don't read `pkg.name` here. Under the npm:alias pattern every variant's
// package.json has the same `name: "executor"`, so all assets would collide
// on the same filename.
for (const entry of new Bun.Glob("executor-*/package.json").scanSync({ cwd: distDir })) {
@@ -724,11 +617,11 @@ const createReleaseAssets = async () => {
};
// ---------------------------------------------------------------------------
-// Node.js launcher — resolves the platform binary shipped as an
+// Node.js launcher resolves the platform binary shipped as an
// optionalDependency and execs it. Resolution order:
// 1. EXECUTOR_BIN_PATH override
// 2. require.resolve("executor--/package.json")
-// 3. bin/runtime/ (preview wrapper compat — pkg.pr.new previews
+// 3. bin/runtime/ (preview wrapper compat, pkg.pr.new previews
// download the binary here at install time instead of via
// optionalDependencies)
//
@@ -835,7 +728,7 @@ process.exit(1);
`;
// ---------------------------------------------------------------------------
-// Preview postinstall — download per-PR tarballs from our R2 CDN instead of
+// Preview postinstall downloads per-PR tarballs from our R2 CDN instead of
// GitHub Releases. The __CDN_BASE_URL__ placeholder is replaced at build time
// with `${cdnBase}/${sha}` so each preview fetches its own commit's binary.
// ---------------------------------------------------------------------------
@@ -970,16 +863,12 @@ if (command === "binary") {
await buildPreviewWrapperPackage(targets);
} else if (command === "release-assets") {
await createReleaseAssets();
-} else if (command === "publish") {
- const channel = positionals[1] ?? "latest";
- await publish(channel);
} else {
console.log(`Usage:
bun run build.ts binary [--single] [--mode production|development]
bun run build.ts preview [--mode production|development]
bun run build.ts preview-tarball [--mode production|development]
bun run build.ts preview-wrapper
- bun run build.ts release-assets
- bun run build.ts publish [channel]`);
+ bun run build.ts release-assets`);
process.exit(1);
}
diff --git a/apps/cli/src/daemon-state.test.ts b/legacy/cli/src/daemon-state.test.ts
similarity index 100%
rename from apps/cli/src/daemon-state.test.ts
rename to legacy/cli/src/daemon-state.test.ts
diff --git a/apps/cli/src/daemon-state.ts b/legacy/cli/src/daemon-state.ts
similarity index 100%
rename from apps/cli/src/daemon-state.ts
rename to legacy/cli/src/daemon-state.ts
diff --git a/apps/cli/src/daemon.test.ts b/legacy/cli/src/daemon.test.ts
similarity index 98%
rename from apps/cli/src/daemon.test.ts
rename to legacy/cli/src/daemon.test.ts
index 13ffdc80c..8758a9f14 100644
--- a/apps/cli/src/daemon.test.ts
+++ b/legacy/cli/src/daemon.test.ts
@@ -12,7 +12,7 @@ import {
describe("isDevCliEntrypoint", () => {
it("treats source entrypoints as dev", () => {
- expect(isDevCliEntrypoint("/Users/x/src/executor/apps/cli/src/main.ts")).toBe(true);
+ expect(isDevCliEntrypoint("/Users/x/src/executor/legacy/cli/src/main.ts")).toBe(true);
expect(isDevCliEntrypoint("/Users/x/dist/main.js")).toBe(true);
});
diff --git a/apps/cli/src/daemon.ts b/legacy/cli/src/daemon.ts
similarity index 100%
rename from apps/cli/src/daemon.ts
rename to legacy/cli/src/daemon.ts
diff --git a/apps/cli/src/device-login.test.ts b/legacy/cli/src/device-login.test.ts
similarity index 100%
rename from apps/cli/src/device-login.test.ts
rename to legacy/cli/src/device-login.test.ts
diff --git a/apps/cli/src/device-login.ts b/legacy/cli/src/device-login.ts
similarity index 100%
rename from apps/cli/src/device-login.ts
rename to legacy/cli/src/device-login.ts
diff --git a/apps/cli/src/embedded-web-ui.gen.d.ts b/legacy/cli/src/embedded-web-ui.gen.d.ts
similarity index 100%
rename from apps/cli/src/embedded-web-ui.gen.d.ts
rename to legacy/cli/src/embedded-web-ui.gen.d.ts
diff --git a/apps/cli/src/embedded-web-ui.gen.ts b/legacy/cli/src/embedded-web-ui.gen.ts
similarity index 100%
rename from apps/cli/src/embedded-web-ui.gen.ts
rename to legacy/cli/src/embedded-web-ui.gen.ts
diff --git a/apps/cli/src/installation.ts b/legacy/cli/src/installation.ts
similarity index 100%
rename from apps/cli/src/installation.ts
rename to legacy/cli/src/installation.ts
diff --git a/apps/cli/src/integrations.ts b/legacy/cli/src/integrations.ts
similarity index 94%
rename from apps/cli/src/integrations.ts
rename to legacy/cli/src/integrations.ts
index 3d3dd7e23..687610640 100644
--- a/apps/cli/src/integrations.ts
+++ b/legacy/cli/src/integrations.ts
@@ -18,7 +18,7 @@ const refreshRegistry = IntegrationsRegistry.asEffect().pipe(
// returns immediately, never throws, never blocks the caller. Bun's event
// loop stays alive while the request is in flight, so sub-second CLI commands
// still complete the fetch before process exit; long-lived commands (e.g.
-// `executor mcp`) get their recurring refresh from apps/local instead.
+// `executor mcp`) get their recurring refresh from legacy/local instead.
// Honors DO_NOT_TRACK and EXECUTOR_DISABLE_INTEGRATIONS_FETCH inside the
// layer.
export const fetchIntegrations = (): void => {
diff --git a/apps/cli/src/local-server-manifest.test.ts b/legacy/cli/src/local-server-manifest.test.ts
similarity index 100%
rename from apps/cli/src/local-server-manifest.test.ts
rename to legacy/cli/src/local-server-manifest.test.ts
diff --git a/apps/cli/src/local-server-manifest.ts b/legacy/cli/src/local-server-manifest.ts
similarity index 100%
rename from apps/cli/src/local-server-manifest.ts
rename to legacy/cli/src/local-server-manifest.ts
diff --git a/apps/cli/src/main.ts b/legacy/cli/src/main.ts
similarity index 100%
rename from apps/cli/src/main.ts
rename to legacy/cli/src/main.ts
diff --git a/apps/cli/src/native-bindings.ts b/legacy/cli/src/native-bindings.ts
similarity index 100%
rename from apps/cli/src/native-bindings.ts
rename to legacy/cli/src/native-bindings.ts
diff --git a/apps/cli/src/server-connection.test.ts b/legacy/cli/src/server-connection.test.ts
similarity index 100%
rename from apps/cli/src/server-connection.test.ts
rename to legacy/cli/src/server-connection.test.ts
diff --git a/apps/cli/src/server-connection.ts b/legacy/cli/src/server-connection.ts
similarity index 100%
rename from apps/cli/src/server-connection.ts
rename to legacy/cli/src/server-connection.ts
diff --git a/apps/cli/src/server-profile.test.ts b/legacy/cli/src/server-profile.test.ts
similarity index 100%
rename from apps/cli/src/server-profile.test.ts
rename to legacy/cli/src/server-profile.test.ts
diff --git a/apps/cli/src/server-profile.ts b/legacy/cli/src/server-profile.ts
similarity index 100%
rename from apps/cli/src/server-profile.ts
rename to legacy/cli/src/server-profile.ts
diff --git a/apps/cli/src/service.test.ts b/legacy/cli/src/service.test.ts
similarity index 100%
rename from apps/cli/src/service.test.ts
rename to legacy/cli/src/service.test.ts
diff --git a/apps/cli/src/service.ts b/legacy/cli/src/service.ts
similarity index 100%
rename from apps/cli/src/service.ts
rename to legacy/cli/src/service.ts
diff --git a/apps/cli/src/tooling.test.ts b/legacy/cli/src/tooling.test.ts
similarity index 100%
rename from apps/cli/src/tooling.test.ts
rename to legacy/cli/src/tooling.test.ts
diff --git a/apps/cli/src/tooling.ts b/legacy/cli/src/tooling.ts
similarity index 100%
rename from apps/cli/src/tooling.ts
rename to legacy/cli/src/tooling.ts
diff --git a/apps/cli/tsconfig.json b/legacy/cli/tsconfig.json
similarity index 100%
rename from apps/cli/tsconfig.json
rename to legacy/cli/tsconfig.json
diff --git a/apps/desktop/vitest.config.ts b/legacy/cli/vitest.config.ts
similarity index 100%
rename from apps/desktop/vitest.config.ts
rename to legacy/cli/vitest.config.ts
diff --git a/apps/cloud/.gitignore b/legacy/cloud/.gitignore
similarity index 100%
rename from apps/cloud/.gitignore
rename to legacy/cloud/.gitignore
diff --git a/apps/cloud/CHANGELOG.md b/legacy/cloud/CHANGELOG.md
similarity index 100%
rename from apps/cloud/CHANGELOG.md
rename to legacy/cloud/CHANGELOG.md
diff --git a/apps/cloud/drizzle.config.ts b/legacy/cloud/drizzle.config.ts
similarity index 100%
rename from apps/cloud/drizzle.config.ts
rename to legacy/cloud/drizzle.config.ts
diff --git a/apps/cloud/drizzle/0000_v2_baseline.sql b/legacy/cloud/drizzle/0000_v2_baseline.sql
similarity index 100%
rename from apps/cloud/drizzle/0000_v2_baseline.sql
rename to legacy/cloud/drizzle/0000_v2_baseline.sql
diff --git a/apps/cloud/drizzle/0001_illegal_wolverine.sql b/legacy/cloud/drizzle/0001_illegal_wolverine.sql
similarity index 100%
rename from apps/cloud/drizzle/0001_illegal_wolverine.sql
rename to legacy/cloud/drizzle/0001_illegal_wolverine.sql
diff --git a/apps/cloud/drizzle/0002_unwrap_openapi_output_envelope.sql b/legacy/cloud/drizzle/0002_unwrap_openapi_output_envelope.sql
similarity index 100%
rename from apps/cloud/drizzle/0002_unwrap_openapi_output_envelope.sql
rename to legacy/cloud/drizzle/0002_unwrap_openapi_output_envelope.sql
diff --git a/apps/cloud/drizzle/0003_friendly_monster_badoon.sql b/legacy/cloud/drizzle/0003_friendly_monster_badoon.sql
similarity index 100%
rename from apps/cloud/drizzle/0003_friendly_monster_badoon.sql
rename to legacy/cloud/drizzle/0003_friendly_monster_badoon.sql
diff --git a/apps/cloud/drizzle/0004_nervous_quasar.sql b/legacy/cloud/drizzle/0004_nervous_quasar.sql
similarity index 100%
rename from apps/cloud/drizzle/0004_nervous_quasar.sql
rename to legacy/cloud/drizzle/0004_nervous_quasar.sql
diff --git a/apps/cloud/drizzle/0005_integration_descriptions.sql b/legacy/cloud/drizzle/0005_integration_descriptions.sql
similarity index 100%
rename from apps/cloud/drizzle/0005_integration_descriptions.sql
rename to legacy/cloud/drizzle/0005_integration_descriptions.sql
diff --git a/apps/cloud/drizzle/0006_google_openapi_ownership.sql b/legacy/cloud/drizzle/0006_google_openapi_ownership.sql
similarity index 100%
rename from apps/cloud/drizzle/0006_google_openapi_ownership.sql
rename to legacy/cloud/drizzle/0006_google_openapi_ownership.sql
diff --git a/apps/cloud/drizzle/0007_red_dragon_lord.sql b/legacy/cloud/drizzle/0007_red_dragon_lord.sql
similarity index 100%
rename from apps/cloud/drizzle/0007_red_dragon_lord.sql
rename to legacy/cloud/drizzle/0007_red_dragon_lord.sql
diff --git a/apps/cloud/drizzle/meta/0000_snapshot.json b/legacy/cloud/drizzle/meta/0000_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0000_snapshot.json
rename to legacy/cloud/drizzle/meta/0000_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0001_snapshot.json b/legacy/cloud/drizzle/meta/0001_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0001_snapshot.json
rename to legacy/cloud/drizzle/meta/0001_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0002_snapshot.json b/legacy/cloud/drizzle/meta/0002_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0002_snapshot.json
rename to legacy/cloud/drizzle/meta/0002_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0003_snapshot.json b/legacy/cloud/drizzle/meta/0003_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0003_snapshot.json
rename to legacy/cloud/drizzle/meta/0003_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0004_snapshot.json b/legacy/cloud/drizzle/meta/0004_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0004_snapshot.json
rename to legacy/cloud/drizzle/meta/0004_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0005_snapshot.json b/legacy/cloud/drizzle/meta/0005_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0005_snapshot.json
rename to legacy/cloud/drizzle/meta/0005_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0006_snapshot.json b/legacy/cloud/drizzle/meta/0006_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0006_snapshot.json
rename to legacy/cloud/drizzle/meta/0006_snapshot.json
diff --git a/apps/cloud/drizzle/meta/0007_snapshot.json b/legacy/cloud/drizzle/meta/0007_snapshot.json
similarity index 100%
rename from apps/cloud/drizzle/meta/0007_snapshot.json
rename to legacy/cloud/drizzle/meta/0007_snapshot.json
diff --git a/apps/cloud/drizzle/meta/_journal.json b/legacy/cloud/drizzle/meta/_journal.json
similarity index 100%
rename from apps/cloud/drizzle/meta/_journal.json
rename to legacy/cloud/drizzle/meta/_journal.json
diff --git a/apps/cloud/executor.config.ts b/legacy/cloud/executor.config.ts
similarity index 100%
rename from apps/cloud/executor.config.ts
rename to legacy/cloud/executor.config.ts
diff --git a/apps/cloud/package.json b/legacy/cloud/package.json
similarity index 100%
rename from apps/cloud/package.json
rename to legacy/cloud/package.json
diff --git a/apps/cloud/public/apple-touch-icon.png b/legacy/cloud/public/apple-touch-icon.png
similarity index 100%
rename from apps/cloud/public/apple-touch-icon.png
rename to legacy/cloud/public/apple-touch-icon.png
diff --git a/apps/cloud/public/favicon-192.png b/legacy/cloud/public/favicon-192.png
similarity index 100%
rename from apps/cloud/public/favicon-192.png
rename to legacy/cloud/public/favicon-192.png
diff --git a/apps/cloud/public/favicon-32.png b/legacy/cloud/public/favicon-32.png
similarity index 100%
rename from apps/cloud/public/favicon-32.png
rename to legacy/cloud/public/favicon-32.png
diff --git a/apps/cloud/public/favicon.ico b/legacy/cloud/public/favicon.ico
similarity index 100%
rename from apps/cloud/public/favicon.ico
rename to legacy/cloud/public/favicon.ico
diff --git a/apps/cloud/scripts/backfill-org-slugs.ts b/legacy/cloud/scripts/backfill-org-slugs.ts
similarity index 100%
rename from apps/cloud/scripts/backfill-org-slugs.ts
rename to legacy/cloud/scripts/backfill-org-slugs.ts
diff --git a/apps/cloud/scripts/build.mjs b/legacy/cloud/scripts/build.mjs
similarity index 100%
rename from apps/cloud/scripts/build.mjs
rename to legacy/cloud/scripts/build.mjs
diff --git a/apps/cloud/scripts/code-migrations/google-openapi-r2-blobs.ts b/legacy/cloud/scripts/code-migrations/google-openapi-r2-blobs.ts
similarity index 100%
rename from apps/cloud/scripts/code-migrations/google-openapi-r2-blobs.ts
rename to legacy/cloud/scripts/code-migrations/google-openapi-r2-blobs.ts
diff --git a/apps/cloud/scripts/code-migrations/index.ts b/legacy/cloud/scripts/code-migrations/index.ts
similarity index 100%
rename from apps/cloud/scripts/code-migrations/index.ts
rename to legacy/cloud/scripts/code-migrations/index.ts
diff --git a/apps/cloud/scripts/code-migrations/runner.ts b/legacy/cloud/scripts/code-migrations/runner.ts
similarity index 100%
rename from apps/cloud/scripts/code-migrations/runner.ts
rename to legacy/cloud/scripts/code-migrations/runner.ts
diff --git a/apps/cloud/scripts/dev-db.ts b/legacy/cloud/scripts/dev-db.ts
similarity index 100%
rename from apps/cloud/scripts/dev-db.ts
rename to legacy/cloud/scripts/dev-db.ts
diff --git a/apps/cloud/scripts/gen-routes.ts b/legacy/cloud/scripts/gen-routes.ts
similarity index 95%
rename from apps/cloud/scripts/gen-routes.ts
rename to legacy/cloud/scripts/gen-routes.ts
index 60fc3ff14..34df80650 100644
--- a/apps/cloud/scripts/gen-routes.ts
+++ b/legacy/cloud/scripts/gen-routes.ts
@@ -27,4 +27,4 @@ await generateRouteTree({
"}",
],
});
-console.log("generated apps/cloud route tree");
+console.log("generated legacy/cloud route tree");
diff --git a/apps/cloud/scripts/migrate-auth-configs.ts b/legacy/cloud/scripts/migrate-auth-configs.ts
similarity index 100%
rename from apps/cloud/scripts/migrate-auth-configs.ts
rename to legacy/cloud/scripts/migrate-auth-configs.ts
diff --git a/apps/cloud/scripts/migrate-specs-to-blobs.ts b/legacy/cloud/scripts/migrate-specs-to-blobs.ts
similarity index 100%
rename from apps/cloud/scripts/migrate-specs-to-blobs.ts
rename to legacy/cloud/scripts/migrate-specs-to-blobs.ts
diff --git a/apps/cloud/scripts/migrate.ts b/legacy/cloud/scripts/migrate.ts
similarity index 100%
rename from apps/cloud/scripts/migrate.ts
rename to legacy/cloud/scripts/migrate.ts
diff --git a/apps/cloud/scripts/repartition-vault-metadata.ts b/legacy/cloud/scripts/repartition-vault-metadata.ts
similarity index 100%
rename from apps/cloud/scripts/repartition-vault-metadata.ts
rename to legacy/cloud/scripts/repartition-vault-metadata.ts
diff --git a/apps/cloud/scripts/test-globalsetup.ts b/legacy/cloud/scripts/test-globalsetup.ts
similarity index 100%
rename from apps/cloud/scripts/test-globalsetup.ts
rename to legacy/cloud/scripts/test-globalsetup.ts
diff --git a/apps/cloud/src/account/account-api.ts b/legacy/cloud/src/account/account-api.ts
similarity index 100%
rename from apps/cloud/src/account/account-api.ts
rename to legacy/cloud/src/account/account-api.ts
diff --git a/apps/cloud/src/account/member-limits.node.test.ts b/legacy/cloud/src/account/member-limits.node.test.ts
similarity index 100%
rename from apps/cloud/src/account/member-limits.node.test.ts
rename to legacy/cloud/src/account/member-limits.node.test.ts
diff --git a/apps/cloud/src/account/organization-limits.node.test.ts b/legacy/cloud/src/account/organization-limits.node.test.ts
similarity index 100%
rename from apps/cloud/src/account/organization-limits.node.test.ts
rename to legacy/cloud/src/account/organization-limits.node.test.ts
diff --git a/apps/cloud/src/account/workos-account-service.ts b/legacy/cloud/src/account/workos-account-service.ts
similarity index 100%
rename from apps/cloud/src/account/workos-account-service.ts
rename to legacy/cloud/src/account/workos-account-service.ts
diff --git a/apps/cloud/src/api.request-scope.node.test.ts b/legacy/cloud/src/api.request-scope.node.test.ts
similarity index 100%
rename from apps/cloud/src/api.request-scope.node.test.ts
rename to legacy/cloud/src/api.request-scope.node.test.ts
diff --git a/apps/cloud/src/api/error-response.ts b/legacy/cloud/src/api/error-response.ts
similarity index 100%
rename from apps/cloud/src/api/error-response.ts
rename to legacy/cloud/src/api/error-response.ts
diff --git a/apps/cloud/src/api/layers.ts b/legacy/cloud/src/api/layers.ts
similarity index 100%
rename from apps/cloud/src/api/layers.ts
rename to legacy/cloud/src/api/layers.ts
diff --git a/apps/cloud/src/api/protected-api-key-auth.node.test.ts b/legacy/cloud/src/api/protected-api-key-auth.node.test.ts
similarity index 100%
rename from apps/cloud/src/api/protected-api-key-auth.node.test.ts
rename to legacy/cloud/src/api/protected-api-key-auth.node.test.ts
diff --git a/apps/cloud/src/api/protected-jwt-auth.node.test.ts b/legacy/cloud/src/api/protected-jwt-auth.node.test.ts
similarity index 100%
rename from apps/cloud/src/api/protected-jwt-auth.node.test.ts
rename to legacy/cloud/src/api/protected-jwt-auth.node.test.ts
diff --git a/apps/cloud/src/api/protected.test.ts b/legacy/cloud/src/api/protected.test.ts
similarity index 100%
rename from apps/cloud/src/api/protected.test.ts
rename to legacy/cloud/src/api/protected.test.ts
diff --git a/apps/cloud/src/api/protected.ts b/legacy/cloud/src/api/protected.ts
similarity index 98%
rename from apps/cloud/src/api/protected.ts
rename to legacy/cloud/src/api/protected.ts
index 3d8a513bb..0a38f5815 100644
--- a/apps/cloud/src/api/protected.ts
+++ b/legacy/cloud/src/api/protected.ts
@@ -85,7 +85,7 @@ const ExecutionStackMiddleware = makeExecutionStackMiddleware<
// collapses `requires: IdentityProvider | DbService | UserStoreService` to the
// boot-only `WorkOSClient | ApiKeyService` (so `.layer` is a real Layer instead
// of the "Need to combine" sentinel). Exposed as a factory so tests can swap in a
-// counting fake — see `apps/cloud/src/api.request-scope.node.test.ts`.
+// counting fake, see `legacy/cloud/src/api.request-scope.node.test.ts`.
//
// `AutumnService` is provided HERE — the billing service is scoped to the
// executor plane that meters, not to the neutral boot core. (`/autumn`, the
@@ -109,7 +109,7 @@ export const makeProtectedApiLive = (rsLive: Layer.Layer) => {
const BillingRoutesLive = AutumnRoutesLive.pipe(
Layer.provide(requestScopedMiddleware(requestScopedLive).layer),
diff --git a/apps/cloud/src/app-paths.test.ts b/legacy/cloud/src/app-paths.test.ts
similarity index 100%
rename from apps/cloud/src/app-paths.test.ts
rename to legacy/cloud/src/app-paths.test.ts
diff --git a/apps/cloud/src/app-paths.ts b/legacy/cloud/src/app-paths.ts
similarity index 100%
rename from apps/cloud/src/app-paths.ts
rename to legacy/cloud/src/app-paths.ts
diff --git a/apps/cloud/src/app.ts b/legacy/cloud/src/app.ts
similarity index 99%
rename from apps/cloud/src/app.ts
rename to legacy/cloud/src/app.ts
index ef5cbf61a..7ea55c93c 100644
--- a/apps/cloud/src/app.ts
+++ b/legacy/cloud/src/app.ts
@@ -35,7 +35,7 @@ import { WorkerTelemetryLive } from "./observability/telemetry";
// capture — and Autumn BILLING entering ONLY as extensions: the engine
// metering decorator, the account seat-gate, the `/api/billing/*` proxy route,
// and the createOrganization free-limit gate. `diff` against
-// `apps/host-selfhost/src/app.ts` is the entire product difference.
+// `legacy/host-selfhost/src/app.ts` is the entire product difference.
//
// `ExecutorApp.make` owns the assembly (the execution-stack middleware wrapping
// the protected API, the MCP envelope, the account API on the /api-prefixed
diff --git a/apps/cloud/src/auth/api-jwt-bearer.ts b/legacy/cloud/src/auth/api-jwt-bearer.ts
similarity index 100%
rename from apps/cloud/src/auth/api-jwt-bearer.ts
rename to legacy/cloud/src/auth/api-jwt-bearer.ts
diff --git a/apps/cloud/src/auth/api-keys.node.test.ts b/legacy/cloud/src/auth/api-keys.node.test.ts
similarity index 100%
rename from apps/cloud/src/auth/api-keys.node.test.ts
rename to legacy/cloud/src/auth/api-keys.node.test.ts
diff --git a/apps/cloud/src/auth/api-keys.ts b/legacy/cloud/src/auth/api-keys.ts
similarity index 100%
rename from apps/cloud/src/auth/api-keys.ts
rename to legacy/cloud/src/auth/api-keys.ts
diff --git a/apps/cloud/src/auth/api.ts b/legacy/cloud/src/auth/api.ts
similarity index 100%
rename from apps/cloud/src/auth/api.ts
rename to legacy/cloud/src/auth/api.ts
diff --git a/apps/cloud/src/auth/bearer.ts b/legacy/cloud/src/auth/bearer.ts
similarity index 100%
rename from apps/cloud/src/auth/bearer.ts
rename to legacy/cloud/src/auth/bearer.ts
diff --git a/apps/cloud/src/auth/context.ts b/legacy/cloud/src/auth/context.ts
similarity index 100%
rename from apps/cloud/src/auth/context.ts
rename to legacy/cloud/src/auth/context.ts
diff --git a/apps/cloud/src/auth/cookies.ts b/legacy/cloud/src/auth/cookies.ts
similarity index 100%
rename from apps/cloud/src/auth/cookies.ts
rename to legacy/cloud/src/auth/cookies.ts
diff --git a/apps/cloud/src/auth/errors.ts b/legacy/cloud/src/auth/errors.ts
similarity index 100%
rename from apps/cloud/src/auth/errors.ts
rename to legacy/cloud/src/auth/errors.ts
diff --git a/apps/cloud/src/auth/handlers.ts b/legacy/cloud/src/auth/handlers.ts
similarity index 100%
rename from apps/cloud/src/auth/handlers.ts
rename to legacy/cloud/src/auth/handlers.ts
diff --git a/apps/cloud/src/auth/jwks-cache.node.test.ts b/legacy/cloud/src/auth/jwks-cache.node.test.ts
similarity index 100%
rename from apps/cloud/src/auth/jwks-cache.node.test.ts
rename to legacy/cloud/src/auth/jwks-cache.node.test.ts
diff --git a/apps/cloud/src/auth/jwks-cache.ts b/legacy/cloud/src/auth/jwks-cache.ts
similarity index 100%
rename from apps/cloud/src/auth/jwks-cache.ts
rename to legacy/cloud/src/auth/jwks-cache.ts
diff --git a/apps/cloud/src/auth/login-state.test.ts b/legacy/cloud/src/auth/login-state.test.ts
similarity index 100%
rename from apps/cloud/src/auth/login-state.test.ts
rename to legacy/cloud/src/auth/login-state.test.ts
diff --git a/apps/cloud/src/auth/login-state.ts b/legacy/cloud/src/auth/login-state.ts
similarity index 100%
rename from apps/cloud/src/auth/login-state.ts
rename to legacy/cloud/src/auth/login-state.ts
diff --git a/apps/cloud/src/auth/middleware-live.ts b/legacy/cloud/src/auth/middleware-live.ts
similarity index 100%
rename from apps/cloud/src/auth/middleware-live.ts
rename to legacy/cloud/src/auth/middleware-live.ts
diff --git a/apps/cloud/src/auth/middleware.ts b/legacy/cloud/src/auth/middleware.ts
similarity index 100%
rename from apps/cloud/src/auth/middleware.ts
rename to legacy/cloud/src/auth/middleware.ts
diff --git a/apps/cloud/src/auth/org-selector-auth.node.test.ts b/legacy/cloud/src/auth/org-selector-auth.node.test.ts
similarity index 100%
rename from apps/cloud/src/auth/org-selector-auth.node.test.ts
rename to legacy/cloud/src/auth/org-selector-auth.node.test.ts
diff --git a/apps/cloud/src/auth/organization.ts b/legacy/cloud/src/auth/organization.ts
similarity index 100%
rename from apps/cloud/src/auth/organization.ts
rename to legacy/cloud/src/auth/organization.ts
diff --git a/apps/cloud/src/auth/request-origin.test.ts b/legacy/cloud/src/auth/request-origin.test.ts
similarity index 100%
rename from apps/cloud/src/auth/request-origin.test.ts
rename to legacy/cloud/src/auth/request-origin.test.ts
diff --git a/apps/cloud/src/auth/request-origin.ts b/legacy/cloud/src/auth/request-origin.ts
similarity index 100%
rename from apps/cloud/src/auth/request-origin.ts
rename to legacy/cloud/src/auth/request-origin.ts
diff --git a/apps/cloud/src/auth/return-to.test.ts b/legacy/cloud/src/auth/return-to.test.ts
similarity index 100%
rename from apps/cloud/src/auth/return-to.test.ts
rename to legacy/cloud/src/auth/return-to.test.ts
diff --git a/apps/cloud/src/auth/return-to.ts b/legacy/cloud/src/auth/return-to.ts
similarity index 100%
rename from apps/cloud/src/auth/return-to.ts
rename to legacy/cloud/src/auth/return-to.ts
diff --git a/apps/cloud/src/auth/route-paths.ts b/legacy/cloud/src/auth/route-paths.ts
similarity index 100%
rename from apps/cloud/src/auth/route-paths.ts
rename to legacy/cloud/src/auth/route-paths.ts
diff --git a/apps/cloud/src/auth/ssr-gate.ts b/legacy/cloud/src/auth/ssr-gate.ts
similarity index 100%
rename from apps/cloud/src/auth/ssr-gate.ts
rename to legacy/cloud/src/auth/ssr-gate.ts
diff --git a/apps/cloud/src/auth/user-store.ts b/legacy/cloud/src/auth/user-store.ts
similarity index 100%
rename from apps/cloud/src/auth/user-store.ts
rename to legacy/cloud/src/auth/user-store.ts
diff --git a/apps/cloud/src/auth/workos-auth-provider.ts b/legacy/cloud/src/auth/workos-auth-provider.ts
similarity index 100%
rename from apps/cloud/src/auth/workos-auth-provider.ts
rename to legacy/cloud/src/auth/workos-auth-provider.ts
diff --git a/apps/cloud/src/auth/workos.node.test.ts b/legacy/cloud/src/auth/workos.node.test.ts
similarity index 100%
rename from apps/cloud/src/auth/workos.node.test.ts
rename to legacy/cloud/src/auth/workos.node.test.ts
diff --git a/apps/cloud/src/auth/workos.ts b/legacy/cloud/src/auth/workos.ts
similarity index 100%
rename from apps/cloud/src/auth/workos.ts
rename to legacy/cloud/src/auth/workos.ts
diff --git a/apps/cloud/src/db/code-migration-runner.test.ts b/legacy/cloud/src/db/code-migration-runner.test.ts
similarity index 100%
rename from apps/cloud/src/db/code-migration-runner.test.ts
rename to legacy/cloud/src/db/code-migration-runner.test.ts
diff --git a/apps/cloud/src/db/db.schema.test.ts b/legacy/cloud/src/db/db.schema.test.ts
similarity index 100%
rename from apps/cloud/src/db/db.schema.test.ts
rename to legacy/cloud/src/db/db.schema.test.ts
diff --git a/apps/cloud/src/db/db.test.ts b/legacy/cloud/src/db/db.test.ts
similarity index 100%
rename from apps/cloud/src/db/db.test.ts
rename to legacy/cloud/src/db/db.test.ts
diff --git a/apps/cloud/src/db/db.ts b/legacy/cloud/src/db/db.ts
similarity index 98%
rename from apps/cloud/src/db/db.ts
rename to legacy/cloud/src/db/db.ts
index 219beaf38..b45bc2d2e 100644
--- a/apps/cloud/src/db/db.ts
+++ b/legacy/cloud/src/db/db.ts
@@ -25,7 +25,7 @@ import * as executorSchema from "./executor-schema";
// Exported so every drizzle() call in the cloud app shares one schema
// object. Historically `mcp-session.ts` built its own and forgot to spread
// `executorSchema`, producing runtime "unknown model source" errors that
-// only surfaced in prod. See apps/cloud/src/db/db.schema.test.ts.
+// only surfaced in prod. See legacy/cloud/src/db/db.schema.test.ts.
export const combinedSchema = { ...cloudSchema, ...executorSchema };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/apps/cloud/src/db/executor-schema.ts b/legacy/cloud/src/db/executor-schema.ts
similarity index 100%
rename from apps/cloud/src/db/executor-schema.ts
rename to legacy/cloud/src/db/executor-schema.ts
diff --git a/apps/cloud/src/db/fuma.ts b/legacy/cloud/src/db/fuma.ts
similarity index 100%
rename from apps/cloud/src/db/fuma.ts
rename to legacy/cloud/src/db/fuma.ts
diff --git a/apps/cloud/src/db/google-openapi-r2-code-migration.test.ts b/legacy/cloud/src/db/google-openapi-r2-code-migration.test.ts
similarity index 100%
rename from apps/cloud/src/db/google-openapi-r2-code-migration.test.ts
rename to legacy/cloud/src/db/google-openapi-r2-code-migration.test.ts
diff --git a/apps/cloud/src/db/schema.ts b/legacy/cloud/src/db/schema.ts
similarity index 100%
rename from apps/cloud/src/db/schema.ts
rename to legacy/cloud/src/db/schema.ts
diff --git a/apps/cloud/src/edge/docs.test.ts b/legacy/cloud/src/edge/docs.test.ts
similarity index 100%
rename from apps/cloud/src/edge/docs.test.ts
rename to legacy/cloud/src/edge/docs.test.ts
diff --git a/apps/cloud/src/edge/docs.ts b/legacy/cloud/src/edge/docs.ts
similarity index 100%
rename from apps/cloud/src/edge/docs.ts
rename to legacy/cloud/src/edge/docs.ts
diff --git a/apps/cloud/src/edge/index.ts b/legacy/cloud/src/edge/index.ts
similarity index 100%
rename from apps/cloud/src/edge/index.ts
rename to legacy/cloud/src/edge/index.ts
diff --git a/apps/cloud/src/edge/marketing.ts b/legacy/cloud/src/edge/marketing.ts
similarity index 100%
rename from apps/cloud/src/edge/marketing.ts
rename to legacy/cloud/src/edge/marketing.ts
diff --git a/apps/cloud/src/edge/posthog.ts b/legacy/cloud/src/edge/posthog.ts
similarity index 100%
rename from apps/cloud/src/edge/posthog.ts
rename to legacy/cloud/src/edge/posthog.ts
diff --git a/apps/cloud/src/edge/sentry-tunnel.ts b/legacy/cloud/src/edge/sentry-tunnel.ts
similarity index 100%
rename from apps/cloud/src/edge/sentry-tunnel.ts
rename to legacy/cloud/src/edge/sentry-tunnel.ts
diff --git a/apps/cloud/src/engine/execution-stack-metered.ts b/legacy/cloud/src/engine/execution-stack-metered.ts
similarity index 100%
rename from apps/cloud/src/engine/execution-stack-metered.ts
rename to legacy/cloud/src/engine/execution-stack-metered.ts
diff --git a/apps/cloud/src/engine/execution-stack.ts b/legacy/cloud/src/engine/execution-stack.ts
similarity index 100%
rename from apps/cloud/src/engine/execution-stack.ts
rename to legacy/cloud/src/engine/execution-stack.ts
diff --git a/apps/cloud/src/engine/execution-usage.ts b/legacy/cloud/src/engine/execution-usage.ts
similarity index 100%
rename from apps/cloud/src/engine/execution-usage.ts
rename to legacy/cloud/src/engine/execution-usage.ts
diff --git a/apps/cloud/src/env-augment.d.ts b/legacy/cloud/src/env-augment.d.ts
similarity index 100%
rename from apps/cloud/src/env-augment.d.ts
rename to legacy/cloud/src/env-augment.d.ts
diff --git a/apps/cloud/src/extensions/billing/plans.ts b/legacy/cloud/src/extensions/billing/plans.ts
similarity index 100%
rename from apps/cloud/src/extensions/billing/plans.ts
rename to legacy/cloud/src/extensions/billing/plans.ts
diff --git a/apps/cloud/src/extensions/billing/route.node.test.ts b/legacy/cloud/src/extensions/billing/route.node.test.ts
similarity index 100%
rename from apps/cloud/src/extensions/billing/route.node.test.ts
rename to legacy/cloud/src/extensions/billing/route.node.test.ts
diff --git a/apps/cloud/src/extensions/billing/route.ts b/legacy/cloud/src/extensions/billing/route.ts
similarity index 100%
rename from apps/cloud/src/extensions/billing/route.ts
rename to legacy/cloud/src/extensions/billing/route.ts
diff --git a/apps/cloud/src/extensions/billing/service.ts b/legacy/cloud/src/extensions/billing/service.ts
similarity index 100%
rename from apps/cloud/src/extensions/billing/service.ts
rename to legacy/cloud/src/extensions/billing/service.ts
diff --git a/apps/cloud/src/extensions/docs.ts b/legacy/cloud/src/extensions/docs.ts
similarity index 100%
rename from apps/cloud/src/extensions/docs.ts
rename to legacy/cloud/src/extensions/docs.ts
diff --git a/apps/cloud/src/extensions/routes.ts b/legacy/cloud/src/extensions/routes.ts
similarity index 100%
rename from apps/cloud/src/extensions/routes.ts
rename to legacy/cloud/src/extensions/routes.ts
diff --git a/apps/cloud/src/frontend-atom-error-capture.node.test.ts b/legacy/cloud/src/frontend-atom-error-capture.node.test.ts
similarity index 100%
rename from apps/cloud/src/frontend-atom-error-capture.node.test.ts
rename to legacy/cloud/src/frontend-atom-error-capture.node.test.ts
diff --git a/apps/cloud/src/mcp-session.e2e.node.test.ts b/legacy/cloud/src/mcp-session.e2e.node.test.ts
similarity index 100%
rename from apps/cloud/src/mcp-session.e2e.node.test.ts
rename to legacy/cloud/src/mcp-session.e2e.node.test.ts
diff --git a/apps/cloud/src/mcp/auth-provider.ts b/legacy/cloud/src/mcp/auth-provider.ts
similarity index 100%
rename from apps/cloud/src/mcp/auth-provider.ts
rename to legacy/cloud/src/mcp/auth-provider.ts
diff --git a/apps/cloud/src/mcp/auth.ts b/legacy/cloud/src/mcp/auth.ts
similarity index 100%
rename from apps/cloud/src/mcp/auth.ts
rename to legacy/cloud/src/mcp/auth.ts
diff --git a/apps/cloud/src/mcp/index.ts b/legacy/cloud/src/mcp/index.ts
similarity index 100%
rename from apps/cloud/src/mcp/index.ts
rename to legacy/cloud/src/mcp/index.ts
diff --git a/apps/cloud/src/mcp/jwt.ts b/legacy/cloud/src/mcp/jwt.ts
similarity index 100%
rename from apps/cloud/src/mcp/jwt.ts
rename to legacy/cloud/src/mcp/jwt.ts
diff --git a/apps/cloud/src/mcp/mcp-auth.node.test.ts b/legacy/cloud/src/mcp/mcp-auth.node.test.ts
similarity index 100%
rename from apps/cloud/src/mcp/mcp-auth.node.test.ts
rename to legacy/cloud/src/mcp/mcp-auth.node.test.ts
diff --git a/apps/cloud/src/mcp/mount.test.ts b/legacy/cloud/src/mcp/mount.test.ts
similarity index 100%
rename from apps/cloud/src/mcp/mount.test.ts
rename to legacy/cloud/src/mcp/mount.test.ts
diff --git a/apps/cloud/src/mcp/mount.ts b/legacy/cloud/src/mcp/mount.ts
similarity index 99%
rename from apps/cloud/src/mcp/mount.ts
rename to legacy/cloud/src/mcp/mount.ts
index cbb302717..a46f0c49d 100644
--- a/apps/cloud/src/mcp/mount.ts
+++ b/legacy/cloud/src/mcp/mount.ts
@@ -154,7 +154,7 @@ export const prepareMcpOrgScope = (request: Request): Request => {
/**
* Build the envelope web handler from the shared `McpServingRoutes` Layer,
- * provided cloud's two seams. Mirrors the self-host mount (apps/host-selfhost
+ * provided cloud's two seams. Mirrors the self-host mount (legacy/host-selfhost
* api.ts): `HttpRouter.provideRequest` clears the route handlers' per-request
* seam requirements, the build-time `Layer.provide(McpAuthProviderLive)`
* satisfies the `HttpRouter.use` callback's read of `discoveryRoutes`, and
diff --git a/apps/cloud/src/mcp/oauth-metadata.ts b/legacy/cloud/src/mcp/oauth-metadata.ts
similarity index 100%
rename from apps/cloud/src/mcp/oauth-metadata.ts
rename to legacy/cloud/src/mcp/oauth-metadata.ts
diff --git a/apps/cloud/src/mcp/reporter.ts b/legacy/cloud/src/mcp/reporter.ts
similarity index 100%
rename from apps/cloud/src/mcp/reporter.ts
rename to legacy/cloud/src/mcp/reporter.ts
diff --git a/apps/cloud/src/mcp/responses.ts b/legacy/cloud/src/mcp/responses.ts
similarity index 100%
rename from apps/cloud/src/mcp/responses.ts
rename to legacy/cloud/src/mcp/responses.ts
diff --git a/apps/cloud/src/mcp/session-durable-object.ts b/legacy/cloud/src/mcp/session-durable-object.ts
similarity index 100%
rename from apps/cloud/src/mcp/session-durable-object.ts
rename to legacy/cloud/src/mcp/session-durable-object.ts
diff --git a/apps/cloud/src/mcp/session-store.ts b/legacy/cloud/src/mcp/session-store.ts
similarity index 100%
rename from apps/cloud/src/mcp/session-store.ts
rename to legacy/cloud/src/mcp/session-store.ts
diff --git a/apps/cloud/src/mcp/telemetry.ts b/legacy/cloud/src/mcp/telemetry.ts
similarity index 100%
rename from apps/cloud/src/mcp/telemetry.ts
rename to legacy/cloud/src/mcp/telemetry.ts
diff --git a/apps/cloud/src/mcp/worker-transport.test.ts b/legacy/cloud/src/mcp/worker-transport.test.ts
similarity index 100%
rename from apps/cloud/src/mcp/worker-transport.test.ts
rename to legacy/cloud/src/mcp/worker-transport.test.ts
diff --git a/apps/cloud/src/observability/browser-traces.test.ts b/legacy/cloud/src/observability/browser-traces.test.ts
similarity index 100%
rename from apps/cloud/src/observability/browser-traces.test.ts
rename to legacy/cloud/src/observability/browser-traces.test.ts
diff --git a/apps/cloud/src/observability/browser-traces.ts b/legacy/cloud/src/observability/browser-traces.ts
similarity index 100%
rename from apps/cloud/src/observability/browser-traces.ts
rename to legacy/cloud/src/observability/browser-traces.ts
diff --git a/apps/cloud/src/observability/error-logging.ts b/legacy/cloud/src/observability/error-logging.ts
similarity index 100%
rename from apps/cloud/src/observability/error-logging.ts
rename to legacy/cloud/src/observability/error-logging.ts
diff --git a/apps/cloud/src/observability/index.ts b/legacy/cloud/src/observability/index.ts
similarity index 100%
rename from apps/cloud/src/observability/index.ts
rename to legacy/cloud/src/observability/index.ts
diff --git a/apps/cloud/src/observability/observability.test.ts b/legacy/cloud/src/observability/observability.test.ts
similarity index 100%
rename from apps/cloud/src/observability/observability.test.ts
rename to legacy/cloud/src/observability/observability.test.ts
diff --git a/apps/cloud/src/observability/telemetry.ts b/legacy/cloud/src/observability/telemetry.ts
similarity index 100%
rename from apps/cloud/src/observability/telemetry.ts
rename to legacy/cloud/src/observability/telemetry.ts
diff --git a/apps/cloud/src/org/api.ts b/legacy/cloud/src/org/api.ts
similarity index 100%
rename from apps/cloud/src/org/api.ts
rename to legacy/cloud/src/org/api.ts
diff --git a/apps/cloud/src/org/auth-middleware.ts b/legacy/cloud/src/org/auth-middleware.ts
similarity index 100%
rename from apps/cloud/src/org/auth-middleware.ts
rename to legacy/cloud/src/org/auth-middleware.ts
diff --git a/apps/cloud/src/org/handlers.test.ts b/legacy/cloud/src/org/handlers.test.ts
similarity index 100%
rename from apps/cloud/src/org/handlers.test.ts
rename to legacy/cloud/src/org/handlers.test.ts
diff --git a/apps/cloud/src/org/handlers.ts b/legacy/cloud/src/org/handlers.ts
similarity index 100%
rename from apps/cloud/src/org/handlers.ts
rename to legacy/cloud/src/org/handlers.ts
diff --git a/apps/cloud/src/plugins.ts b/legacy/cloud/src/plugins.ts
similarity index 100%
rename from apps/cloud/src/plugins.ts
rename to legacy/cloud/src/plugins.ts
diff --git a/apps/cloud/src/routeTree.gen.ts b/legacy/cloud/src/routeTree.gen.ts
similarity index 100%
rename from apps/cloud/src/routeTree.gen.ts
rename to legacy/cloud/src/routeTree.gen.ts
diff --git a/apps/cloud/src/router.tsx b/legacy/cloud/src/router.tsx
similarity index 100%
rename from apps/cloud/src/router.tsx
rename to legacy/cloud/src/router.tsx
diff --git a/apps/cloud/src/routes/__root.tsx b/legacy/cloud/src/routes/__root.tsx
similarity index 100%
rename from apps/cloud/src/routes/__root.tsx
rename to legacy/cloud/src/routes/__root.tsx
diff --git a/apps/cloud/src/routes/app/api-keys.tsx b/legacy/cloud/src/routes/app/api-keys.tsx
similarity index 100%
rename from apps/cloud/src/routes/app/api-keys.tsx
rename to legacy/cloud/src/routes/app/api-keys.tsx
diff --git a/apps/cloud/src/routes/app/billing.tsx b/legacy/cloud/src/routes/app/billing.tsx
similarity index 100%
rename from apps/cloud/src/routes/app/billing.tsx
rename to legacy/cloud/src/routes/app/billing.tsx
diff --git a/apps/cloud/src/routes/app/billing_.plans.tsx b/legacy/cloud/src/routes/app/billing_.plans.tsx
similarity index 100%
rename from apps/cloud/src/routes/app/billing_.plans.tsx
rename to legacy/cloud/src/routes/app/billing_.plans.tsx
diff --git a/apps/cloud/src/routes/app/org.tsx b/legacy/cloud/src/routes/app/org.tsx
similarity index 100%
rename from apps/cloud/src/routes/app/org.tsx
rename to legacy/cloud/src/routes/app/org.tsx
diff --git a/apps/cloud/src/routes/app/resume.$executionId.tsx b/legacy/cloud/src/routes/app/resume.$executionId.tsx
similarity index 100%
rename from apps/cloud/src/routes/app/resume.$executionId.tsx
rename to legacy/cloud/src/routes/app/resume.$executionId.tsx
diff --git a/apps/cloud/src/routes/app/secrets.tsx b/legacy/cloud/src/routes/app/secrets.tsx
similarity index 100%
rename from apps/cloud/src/routes/app/secrets.tsx
rename to legacy/cloud/src/routes/app/secrets.tsx
diff --git a/apps/cloud/src/routes/bare/create-org.tsx b/legacy/cloud/src/routes/bare/create-org.tsx
similarity index 100%
rename from apps/cloud/src/routes/bare/create-org.tsx
rename to legacy/cloud/src/routes/bare/create-org.tsx
diff --git a/apps/cloud/src/routes/bare/login.tsx b/legacy/cloud/src/routes/bare/login.tsx
similarity index 100%
rename from apps/cloud/src/routes/bare/login.tsx
rename to legacy/cloud/src/routes/bare/login.tsx
diff --git a/apps/cloud/src/routes/bare/setup-mcp.tsx b/legacy/cloud/src/routes/bare/setup-mcp.tsx
similarity index 100%
rename from apps/cloud/src/routes/bare/setup-mcp.tsx
rename to legacy/cloud/src/routes/bare/setup-mcp.tsx
diff --git a/apps/cloud/src/server.ts b/legacy/cloud/src/server.ts
similarity index 100%
rename from apps/cloud/src/server.ts
rename to legacy/cloud/src/server.ts
diff --git a/apps/cloud/src/start.ts b/legacy/cloud/src/start.ts
similarity index 100%
rename from apps/cloud/src/start.ts
rename to legacy/cloud/src/start.ts
diff --git a/apps/cloud/src/vite-env.d.ts b/legacy/cloud/src/vite-env.d.ts
similarity index 100%
rename from apps/cloud/src/vite-env.d.ts
rename to legacy/cloud/src/vite-env.d.ts
diff --git a/apps/cloud/src/web/auth.tsx b/legacy/cloud/src/web/auth.tsx
similarity index 100%
rename from apps/cloud/src/web/auth.tsx
rename to legacy/cloud/src/web/auth.tsx
diff --git a/apps/cloud/src/web/client.tsx b/legacy/cloud/src/web/client.tsx
similarity index 100%
rename from apps/cloud/src/web/client.tsx
rename to legacy/cloud/src/web/client.tsx
diff --git a/apps/cloud/src/web/components/create-organization-form.tsx b/legacy/cloud/src/web/components/create-organization-form.tsx
similarity index 100%
rename from apps/cloud/src/web/components/create-organization-form.tsx
rename to legacy/cloud/src/web/components/create-organization-form.tsx
diff --git a/apps/cloud/src/web/components/org-menu-slot.tsx b/legacy/cloud/src/web/components/org-menu-slot.tsx
similarity index 100%
rename from apps/cloud/src/web/components/org-menu-slot.tsx
rename to legacy/cloud/src/web/components/org-menu-slot.tsx
diff --git a/apps/cloud/src/web/components/support-options.tsx b/legacy/cloud/src/web/components/support-options.tsx
similarity index 100%
rename from apps/cloud/src/web/components/support-options.tsx
rename to legacy/cloud/src/web/components/support-options.tsx
diff --git a/apps/cloud/src/web/components/support-slot.tsx b/legacy/cloud/src/web/components/support-slot.tsx
similarity index 100%
rename from apps/cloud/src/web/components/support-slot.tsx
rename to legacy/cloud/src/web/components/support-slot.tsx
diff --git a/apps/cloud/src/web/mcp-resume-atoms.ts b/legacy/cloud/src/web/mcp-resume-atoms.ts
similarity index 100%
rename from apps/cloud/src/web/mcp-resume-atoms.ts
rename to legacy/cloud/src/web/mcp-resume-atoms.ts
diff --git a/apps/cloud/src/web/org-atoms.ts b/legacy/cloud/src/web/org-atoms.ts
similarity index 100%
rename from apps/cloud/src/web/org-atoms.ts
rename to legacy/cloud/src/web/org-atoms.ts
diff --git a/apps/cloud/src/web/pages/create-org.tsx b/legacy/cloud/src/web/pages/create-org.tsx
similarity index 100%
rename from apps/cloud/src/web/pages/create-org.tsx
rename to legacy/cloud/src/web/pages/create-org.tsx
diff --git a/apps/cloud/src/web/pages/login.tsx b/legacy/cloud/src/web/pages/login.tsx
similarity index 100%
rename from apps/cloud/src/web/pages/login.tsx
rename to legacy/cloud/src/web/pages/login.tsx
diff --git a/apps/cloud/src/web/pages/setup-mcp.tsx b/legacy/cloud/src/web/pages/setup-mcp.tsx
similarity index 100%
rename from apps/cloud/src/web/pages/setup-mcp.tsx
rename to legacy/cloud/src/web/pages/setup-mcp.tsx
diff --git a/apps/cloud/src/web/shell.tsx b/legacy/cloud/src/web/shell.tsx
similarity index 100%
rename from apps/cloud/src/web/shell.tsx
rename to legacy/cloud/src/web/shell.tsx
diff --git a/apps/cloud/test-stubs/cloudflare-workers.ts b/legacy/cloud/test-stubs/cloudflare-workers.ts
similarity index 100%
rename from apps/cloud/test-stubs/cloudflare-workers.ts
rename to legacy/cloud/test-stubs/cloudflare-workers.ts
diff --git a/apps/cloud/test-stubs/tanstack-start-entry.ts b/legacy/cloud/test-stubs/tanstack-start-entry.ts
similarity index 100%
rename from apps/cloud/test-stubs/tanstack-start-entry.ts
rename to legacy/cloud/test-stubs/tanstack-start-entry.ts
diff --git a/apps/cloud/tsconfig.json b/legacy/cloud/tsconfig.json
similarity index 97%
rename from apps/cloud/tsconfig.json
rename to legacy/cloud/tsconfig.json
index 129885505..ebc542fad 100644
--- a/apps/cloud/tsconfig.json
+++ b/legacy/cloud/tsconfig.json
@@ -8,7 +8,6 @@
"skipLibCheck": true,
"types": ["@cloudflare/workers-types"],
"outDir": "dist",
- "rootDir": ".",
"declaration": false,
"declarationMap": false,
"sourceMap": true,
diff --git a/apps/cloud/tsr.routes.ts b/legacy/cloud/tsr.routes.ts
similarity index 100%
rename from apps/cloud/tsr.routes.ts
rename to legacy/cloud/tsr.routes.ts
diff --git a/apps/cloud/vite.config.ts b/legacy/cloud/vite.config.ts
similarity index 100%
rename from apps/cloud/vite.config.ts
rename to legacy/cloud/vite.config.ts
diff --git a/apps/cloud/vitest.config.ts b/legacy/cloud/vitest.config.ts
similarity index 100%
rename from apps/cloud/vitest.config.ts
rename to legacy/cloud/vitest.config.ts
diff --git a/apps/cloud/worker-configuration.d.ts b/legacy/cloud/worker-configuration.d.ts
similarity index 100%
rename from apps/cloud/worker-configuration.d.ts
rename to legacy/cloud/worker-configuration.d.ts
diff --git a/apps/cloud/wrangler.jsonc b/legacy/cloud/wrangler.jsonc
similarity index 100%
rename from apps/cloud/wrangler.jsonc
rename to legacy/cloud/wrangler.jsonc
diff --git a/apps/desktop/.gitignore b/legacy/desktop/.gitignore
similarity index 100%
rename from apps/desktop/.gitignore
rename to legacy/desktop/.gitignore
diff --git a/apps/desktop/CHANGELOG.md b/legacy/desktop/CHANGELOG.md
similarity index 100%
rename from apps/desktop/CHANGELOG.md
rename to legacy/desktop/CHANGELOG.md
diff --git a/apps/desktop/build/entitlements.mac.plist b/legacy/desktop/build/entitlements.mac.plist
similarity index 100%
rename from apps/desktop/build/entitlements.mac.plist
rename to legacy/desktop/build/entitlements.mac.plist
diff --git a/apps/desktop/build/icon.png b/legacy/desktop/build/icon.png
similarity index 100%
rename from apps/desktop/build/icon.png
rename to legacy/desktop/build/icon.png
diff --git a/apps/desktop/electron-builder.config.ts b/legacy/desktop/electron-builder.config.ts
similarity index 100%
rename from apps/desktop/electron-builder.config.ts
rename to legacy/desktop/electron-builder.config.ts
diff --git a/apps/desktop/electron-builder.e2e.config.ts b/legacy/desktop/electron-builder.e2e.config.ts
similarity index 100%
rename from apps/desktop/electron-builder.e2e.config.ts
rename to legacy/desktop/electron-builder.e2e.config.ts
diff --git a/apps/desktop/electron.vite.config.ts b/legacy/desktop/electron.vite.config.ts
similarity index 91%
rename from apps/desktop/electron.vite.config.ts
rename to legacy/desktop/electron.vite.config.ts
index 55dd2536c..a559dd063 100644
--- a/apps/desktop/electron.vite.config.ts
+++ b/legacy/desktop/electron.vite.config.ts
@@ -3,7 +3,7 @@ import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import appPlugin from "@executor-js/app/vite";
const APP_ROOT = resolve(import.meta.dirname, "../../packages/app");
-const APPS_LOCAL = resolve(import.meta.dirname, "../local");
+const LEGACY_LOCAL = resolve(import.meta.dirname, "../local");
// Electron's runtime is provided by the launcher binary, not the bundle.
// electron-log etc. ship native modules that also must stay external.
@@ -79,8 +79,8 @@ export default defineConfig({
},
plugins: [
appPlugin({
- executorConfigPath: resolve(APPS_LOCAL, "executor.config.ts"),
- executorJsoncPath: resolve(APPS_LOCAL, "executor.jsonc"),
+ executorConfigPath: resolve(LEGACY_LOCAL, "executor.config.ts"),
+ executorJsoncPath: resolve(LEGACY_LOCAL, "executor.jsonc"),
}),
],
},
diff --git a/apps/desktop/package.json b/legacy/desktop/package.json
similarity index 100%
rename from apps/desktop/package.json
rename to legacy/desktop/package.json
diff --git a/apps/desktop/scripts/build-sidecar.ts b/legacy/desktop/scripts/build-sidecar.ts
similarity index 97%
rename from apps/desktop/scripts/build-sidecar.ts
rename to legacy/desktop/scripts/build-sidecar.ts
index cf248e910..fc6b981f8 100644
--- a/apps/desktop/scripts/build-sidecar.ts
+++ b/legacy/desktop/scripts/build-sidecar.ts
@@ -10,7 +10,7 @@ import { resolve, join } from "node:path";
const ROOT = resolve(import.meta.dir, "..");
const REPO_ROOT = resolve(ROOT, "../..");
-const CLI_ROOT = resolve(REPO_ROOT, "apps/cli");
+const CLI_ROOT = resolve(REPO_ROOT, "legacy/cli");
const EXECUTOR_OUT_DIR = resolve(ROOT, "resources/executor");
const platformName = (platform: NodeJS.Platform): string =>
diff --git a/scripts/merge-latest-mac-yml.ts b/legacy/desktop/scripts/merge-latest-mac-yml.ts
similarity index 75%
rename from scripts/merge-latest-mac-yml.ts
rename to legacy/desktop/scripts/merge-latest-mac-yml.ts
index 06811631c..49223d9ac 100644
--- a/scripts/merge-latest-mac-yml.ts
+++ b/legacy/desktop/scripts/merge-latest-mac-yml.ts
@@ -2,17 +2,17 @@
/**
* Merge per-arch electron-builder update manifests into one latest-mac.yml.
*
- * The desktop publish workflow builds mac arm64 and x64 in separate matrix
- * legs, and each electron-builder invocation emits its own latest-mac.yml
- * listing only that arch's artifacts. electron-updater clients fetch the
- * single `latest-mac.yml` from the release, so publishing either leg's file
- * as-is points every Mac at one arch (arm64 users were being served x64
- * zips). This script unions the `files` entries; electron-updater picks the
- * right one per client by matching the running arch against the file name.
+ * The archived desktop publish workflow built mac arm64 and x64 in separate
+ * matrix legs, and each electron-builder invocation emitted its own
+ * latest-mac.yml listing only that arch's artifacts. electron-updater clients
+ * fetched the single latest-mac.yml from the release, so publishing either
+ * leg's file as-is pointed every Mac at one arch. This helper is retained only
+ * with the legacy desktop source for historical maintenance.
*
- * Usage: bun scripts/merge-latest-mac-yml.ts
- * The first input's top-level path/sha512 (the legacy single-file fields read
- * by very old updaters) are kept, so pass the x64 manifest first.
+ * Usage: bun legacy/desktop/scripts/merge-latest-mac-yml.ts
+ *
+ * The first input's top-level path/sha512 fields are kept, so pass the x64
+ * manifest first.
*/
interface UpdateFileEntry {
@@ -97,4 +97,6 @@ const files = [...primary.files, ...secondary.files].filter((file) => {
});
await Bun.write(outPath, serializeManifest({ ...primary, files }));
-console.log(`Merged ${primaryPath} + ${secondaryPath} → ${outPath} (${files.length} files)`);
+console.log(`Merged ${primaryPath} + ${secondaryPath} -> ${outPath} (${files.length} files)`);
+
+export {};
diff --git a/apps/desktop/scripts/smoke-sidecar.ts b/legacy/desktop/scripts/smoke-sidecar.ts
similarity index 98%
rename from apps/desktop/scripts/smoke-sidecar.ts
rename to legacy/desktop/scripts/smoke-sidecar.ts
index 88a6ee9b5..09262c98f 100644
--- a/apps/desktop/scripts/smoke-sidecar.ts
+++ b/legacy/desktop/scripts/smoke-sidecar.ts
@@ -26,7 +26,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const ROOT = resolve(import.meta.dir, "..");
-const APPS_LOCAL_DRIZZLE = resolve(ROOT, "../local/drizzle-legacy-v1");
+const LEGACY_LOCAL_DRIZZLE = resolve(ROOT, "../local/drizzle-legacy-v1");
const BINARY = resolve(
ROOT,
"resources/executor",
@@ -56,13 +56,13 @@ const makeScopeId = (cwd: string): string => {
};
const readLegacyMigrations = async (): Promise => {
- const journal = (await Bun.file(join(APPS_LOCAL_DRIZZLE, "meta/_journal.json")).json()) as {
+ const journal = (await Bun.file(join(LEGACY_LOCAL_DRIZZLE, "meta/_journal.json")).json()) as {
readonly entries: readonly { readonly idx: number; readonly tag: string }[];
};
const migrations: { sql: string; hash: string }[] = [];
for (const entry of [...journal.entries].sort((left, right) => left.idx - right.idx)) {
- const query = await Bun.file(join(APPS_LOCAL_DRIZZLE, `${entry.tag}.sql`)).text();
+ const query = await Bun.file(join(LEGACY_LOCAL_DRIZZLE, `${entry.tag}.sql`)).text();
migrations.push({
sql: query,
hash: createHash("sha256").update(query).digest("hex"),
@@ -322,7 +322,7 @@ const completePausedResult = async (
const main = async () => {
if (!(await Bun.file(BINARY).exists())) {
fail(
- `binary not found at ${BINARY}. Run \`bun ./scripts/build-sidecar.ts\` from apps/desktop first.`,
+ `binary not found at ${BINARY}. Run \`bun ./scripts/build-sidecar.ts\` from legacy/desktop first.`,
);
}
diff --git a/apps/desktop/src/main/crash-screen.ts b/legacy/desktop/src/main/crash-screen.ts
similarity index 100%
rename from apps/desktop/src/main/crash-screen.ts
rename to legacy/desktop/src/main/crash-screen.ts
diff --git a/apps/desktop/src/main/diagnostics.ts b/legacy/desktop/src/main/diagnostics.ts
similarity index 100%
rename from apps/desktop/src/main/diagnostics.ts
rename to legacy/desktop/src/main/diagnostics.ts
diff --git a/apps/desktop/src/main/global.d.ts b/legacy/desktop/src/main/global.d.ts
similarity index 100%
rename from apps/desktop/src/main/global.d.ts
rename to legacy/desktop/src/main/global.d.ts
diff --git a/apps/desktop/src/main/index.ts b/legacy/desktop/src/main/index.ts
similarity index 99%
rename from apps/desktop/src/main/index.ts
rename to legacy/desktop/src/main/index.ts
index d4850dee9..ce9f711e0 100644
--- a/apps/desktop/src/main/index.ts
+++ b/legacy/desktop/src/main/index.ts
@@ -348,7 +348,7 @@ const installBearerAuthHeader = (origin: string, token: string | null) => {
* it programmatically (dock on mac, BrowserWindow on linux/win).
*
* In dev `app.getAppPath()` returns the directory of the app's
- * `package.json` (i.e. `apps/desktop`), which is more robust than
+ * `package.json` (i.e. `legacy/desktop`), which is more robust than
* relative path math from the compiled main bundle.
*/
const resolveSourceIconPath = (): string =>
diff --git a/apps/desktop/src/main/local-auth.ts b/legacy/desktop/src/main/local-auth.ts
similarity index 100%
rename from apps/desktop/src/main/local-auth.ts
rename to legacy/desktop/src/main/local-auth.ts
diff --git a/apps/desktop/src/main/reset-state.ts b/legacy/desktop/src/main/reset-state.ts
similarity index 100%
rename from apps/desktop/src/main/reset-state.ts
rename to legacy/desktop/src/main/reset-state.ts
diff --git a/apps/desktop/src/main/service.ts b/legacy/desktop/src/main/service.ts
similarity index 100%
rename from apps/desktop/src/main/service.ts
rename to legacy/desktop/src/main/service.ts
diff --git a/apps/desktop/src/main/settings.ts b/legacy/desktop/src/main/settings.ts
similarity index 100%
rename from apps/desktop/src/main/settings.ts
rename to legacy/desktop/src/main/settings.ts
diff --git a/apps/desktop/src/main/sidecar.ts b/legacy/desktop/src/main/sidecar.ts
similarity index 99%
rename from apps/desktop/src/main/sidecar.ts
rename to legacy/desktop/src/main/sidecar.ts
index 905e987b5..654d83efd 100644
--- a/apps/desktop/src/main/sidecar.ts
+++ b/legacy/desktop/src/main/sidecar.ts
@@ -1,7 +1,7 @@
/**
* Sidecar lifecycle manager run inside the Electron main process.
*
- * In dev: spawns `bun run apps/desktop/src/sidecar/server.ts`.
+ * In dev: spawns `bun run legacy/desktop/src/sidecar/server.ts`.
* In prod: spawns the bundled CLI binary in foreground daemon mode.
*
* Either way, the child receives EXECUTOR_PORT/EXECUTOR_HOST/EXECUTOR_AUTH_TOKEN
@@ -246,13 +246,13 @@ const resolveSidecarCommand = (input: {
}
// Dev: run the TS source directly via bun on PATH.
const repoRoot = resolve(import.meta.dirname, "..", "..", "..", "..");
- const sidecarSource = resolve(repoRoot, "apps/desktop/src/sidecar/server.ts");
+ const sidecarSource = resolve(repoRoot, "legacy/desktop/src/sidecar/server.ts");
return { command: "bun", args: ["run", sidecarSource], cwd: repoRoot, cliManagedManifest: false };
};
const resolveClientDir = (): string => {
const repoRoot = resolve(import.meta.dirname, "..", "..", "..", "..");
- return resolve(repoRoot, "apps/local/dist");
+ return resolve(repoRoot, "legacy/local/dist");
};
const delay = (ms: number): Promise =>
diff --git a/apps/desktop/src/main/supervised-connection.test.ts b/legacy/desktop/src/main/supervised-connection.test.ts
similarity index 100%
rename from apps/desktop/src/main/supervised-connection.test.ts
rename to legacy/desktop/src/main/supervised-connection.test.ts
diff --git a/apps/desktop/src/main/supervised-connection.ts b/legacy/desktop/src/main/supervised-connection.ts
similarity index 100%
rename from apps/desktop/src/main/supervised-connection.ts
rename to legacy/desktop/src/main/supervised-connection.ts
diff --git a/apps/desktop/src/main/supervised-daemon.test.ts b/legacy/desktop/src/main/supervised-daemon.test.ts
similarity index 100%
rename from apps/desktop/src/main/supervised-daemon.test.ts
rename to legacy/desktop/src/main/supervised-daemon.test.ts
diff --git a/apps/desktop/src/main/supervised-daemon.ts b/legacy/desktop/src/main/supervised-daemon.ts
similarity index 100%
rename from apps/desktop/src/main/supervised-daemon.ts
rename to legacy/desktop/src/main/supervised-daemon.ts
diff --git a/apps/desktop/src/preload/index.ts b/legacy/desktop/src/preload/index.ts
similarity index 100%
rename from apps/desktop/src/preload/index.ts
rename to legacy/desktop/src/preload/index.ts
diff --git a/apps/desktop/src/renderer/index.html b/legacy/desktop/src/renderer/index.html
similarity index 100%
rename from apps/desktop/src/renderer/index.html
rename to legacy/desktop/src/renderer/index.html
diff --git a/apps/desktop/src/shared/server-settings.ts b/legacy/desktop/src/shared/server-settings.ts
similarity index 100%
rename from apps/desktop/src/shared/server-settings.ts
rename to legacy/desktop/src/shared/server-settings.ts
diff --git a/apps/desktop/src/sidecar/native-bindings.ts b/legacy/desktop/src/sidecar/native-bindings.ts
similarity index 95%
rename from apps/desktop/src/sidecar/native-bindings.ts
rename to legacy/desktop/src/sidecar/native-bindings.ts
index d54a6400f..effd7e0ad 100644
--- a/apps/desktop/src/sidecar/native-bindings.ts
+++ b/legacy/desktop/src/sidecar/native-bindings.ts
@@ -6,7 +6,7 @@
// the binary fails. If this dev sidecar is compiled directly, copy each
// platform's `.node` next to the executable (`libsql.node`, `keyring.node`);
// here we publish their
-// on-disk paths via env vars the loaders read. Mirrors apps/cli/src/native-bindings.ts.
+// on-disk paths via env vars the loaders read. Mirrors legacy/cli/src/native-bindings.ts.
//
// This MUST be the FIRST import in server.ts. ES modules evaluate every import
// before the importer's own body, and libSQL resolves its native addon EAGERLY
diff --git a/apps/desktop/src/sidecar/server.ts b/legacy/desktop/src/sidecar/server.ts
similarity index 100%
rename from apps/desktop/src/sidecar/server.ts
rename to legacy/desktop/src/sidecar/server.ts
diff --git a/apps/desktop/tsconfig.json b/legacy/desktop/tsconfig.json
similarity index 100%
rename from apps/desktop/tsconfig.json
rename to legacy/desktop/tsconfig.json
diff --git a/apps/local/vitest.config.ts b/legacy/desktop/vitest.config.ts
similarity index 100%
rename from apps/local/vitest.config.ts
rename to legacy/desktop/vitest.config.ts
diff --git a/apps/host-cloudflare/.gitignore b/legacy/host-cloudflare/.gitignore
similarity index 100%
rename from apps/host-cloudflare/.gitignore
rename to legacy/host-cloudflare/.gitignore
diff --git a/apps/host-cloudflare/CHANGELOG.md b/legacy/host-cloudflare/CHANGELOG.md
similarity index 100%
rename from apps/host-cloudflare/CHANGELOG.md
rename to legacy/host-cloudflare/CHANGELOG.md
diff --git a/apps/host-cloudflare/README.md b/legacy/host-cloudflare/README.md
similarity index 97%
rename from apps/host-cloudflare/README.md
rename to legacy/host-cloudflare/README.md
index 1770dbbe8..080bff266 100644
--- a/apps/host-cloudflare/README.md
+++ b/legacy/host-cloudflare/README.md
@@ -33,7 +33,7 @@ without). The SPA's auth context reads `/api/account/me`.
```bash
bunx wrangler login
-bun run deploy:setup # apps/host-cloudflare — provisions D1 + secret + deploys
+bun run deploy:setup # legacy/host-cloudflare provisions D1 + secret + deploys
```
`deploy:setup` (scripts/deploy.sh) is idempotent: it creates/reuses the
diff --git a/apps/host-cloudflare/executor.config.ts b/legacy/host-cloudflare/executor.config.ts
similarity index 100%
rename from apps/host-cloudflare/executor.config.ts
rename to legacy/host-cloudflare/executor.config.ts
diff --git a/apps/host-cloudflare/package.json b/legacy/host-cloudflare/package.json
similarity index 98%
rename from apps/host-cloudflare/package.json
rename to legacy/host-cloudflare/package.json
index 159266b85..76cd07778 100644
--- a/apps/host-cloudflare/package.json
+++ b/legacy/host-cloudflare/package.json
@@ -8,6 +8,7 @@
"dev": "wrangler dev",
"dev:web": "vite dev",
"typecheck": "tsgo --noEmit",
+ "typecheck:slow": "tsc --noEmit",
"cf-typegen": "wrangler types",
"deploy:setup": "bash scripts/deploy.sh",
"vendor-wasm": "bun run scripts/vendor-quickjs-wasm.ts",
diff --git a/apps/host-cloudflare/scripts/deploy.sh b/legacy/host-cloudflare/scripts/deploy.sh
similarity index 98%
rename from apps/host-cloudflare/scripts/deploy.sh
rename to legacy/host-cloudflare/scripts/deploy.sh
index 4d8a2857e..bcdfcddda 100755
--- a/apps/host-cloudflare/scripts/deploy.sh
+++ b/legacy/host-cloudflare/scripts/deploy.sh
@@ -10,7 +10,7 @@
# 5. prints the single manual step: the Cloudflare Access application
#
# Idempotent — safe to re-run. Run from anywhere:
-# bash apps/host-cloudflare/scripts/deploy.sh
+# bash legacy/host-cloudflare/scripts/deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
diff --git a/apps/host-cloudflare/scripts/gen-routes.ts b/legacy/host-cloudflare/scripts/gen-routes.ts
similarity index 91%
rename from apps/host-cloudflare/scripts/gen-routes.ts
rename to legacy/host-cloudflare/scripts/gen-routes.ts
index 7ebf787ea..dcca4a30f 100644
--- a/apps/host-cloudflare/scripts/gen-routes.ts
+++ b/legacy/host-cloudflare/scripts/gen-routes.ts
@@ -13,4 +13,4 @@ await generateRouteTree({
generatedRouteTree: fileURLToPath(new URL("../web/routeTree.gen.ts", import.meta.url)),
virtualRouteConfig: routes,
});
-console.log("generated apps/host-cloudflare route tree");
+console.log("generated legacy/host-cloudflare route tree");
diff --git a/apps/host-cloudflare/scripts/preview.ts b/legacy/host-cloudflare/scripts/preview.ts
similarity index 100%
rename from apps/host-cloudflare/scripts/preview.ts
rename to legacy/host-cloudflare/scripts/preview.ts
diff --git a/apps/host-cloudflare/scripts/vendor-quickjs-wasm.ts b/legacy/host-cloudflare/scripts/vendor-quickjs-wasm.ts
similarity index 100%
rename from apps/host-cloudflare/scripts/vendor-quickjs-wasm.ts
rename to legacy/host-cloudflare/scripts/vendor-quickjs-wasm.ts
diff --git a/apps/host-cloudflare/src/account/account-provider.ts b/legacy/host-cloudflare/src/account/account-provider.ts
similarity index 100%
rename from apps/host-cloudflare/src/account/account-provider.ts
rename to legacy/host-cloudflare/src/account/account-provider.ts
diff --git a/apps/host-cloudflare/src/app.ts b/legacy/host-cloudflare/src/app.ts
similarity index 100%
rename from apps/host-cloudflare/src/app.ts
rename to legacy/host-cloudflare/src/app.ts
diff --git a/apps/host-cloudflare/src/auth/cloudflare-access.test.ts b/legacy/host-cloudflare/src/auth/cloudflare-access.test.ts
similarity index 100%
rename from apps/host-cloudflare/src/auth/cloudflare-access.test.ts
rename to legacy/host-cloudflare/src/auth/cloudflare-access.test.ts
diff --git a/apps/host-cloudflare/src/auth/cloudflare-access.ts b/legacy/host-cloudflare/src/auth/cloudflare-access.ts
similarity index 100%
rename from apps/host-cloudflare/src/auth/cloudflare-access.ts
rename to legacy/host-cloudflare/src/auth/cloudflare-access.ts
diff --git a/apps/host-cloudflare/src/config.ts b/legacy/host-cloudflare/src/config.ts
similarity index 100%
rename from apps/host-cloudflare/src/config.ts
rename to legacy/host-cloudflare/src/config.ts
diff --git a/apps/host-cloudflare/src/db/d1.ts b/legacy/host-cloudflare/src/db/d1.ts
similarity index 100%
rename from apps/host-cloudflare/src/db/d1.ts
rename to legacy/host-cloudflare/src/db/d1.ts
diff --git a/apps/host-cloudflare/src/db/data-migrations.test.ts b/legacy/host-cloudflare/src/db/data-migrations.test.ts
similarity index 100%
rename from apps/host-cloudflare/src/db/data-migrations.test.ts
rename to legacy/host-cloudflare/src/db/data-migrations.test.ts
diff --git a/apps/host-cloudflare/src/db/data-migrations.ts b/legacy/host-cloudflare/src/db/data-migrations.ts
similarity index 100%
rename from apps/host-cloudflare/src/db/data-migrations.ts
rename to legacy/host-cloudflare/src/db/data-migrations.ts
diff --git a/apps/host-cloudflare/src/execution.ts b/legacy/host-cloudflare/src/execution.ts
similarity index 100%
rename from apps/host-cloudflare/src/execution.ts
rename to legacy/host-cloudflare/src/execution.ts
diff --git a/apps/host-cloudflare/src/mcp/auth.ts b/legacy/host-cloudflare/src/mcp/auth.ts
similarity index 100%
rename from apps/host-cloudflare/src/mcp/auth.ts
rename to legacy/host-cloudflare/src/mcp/auth.ts
diff --git a/apps/host-cloudflare/src/mcp/index.ts b/legacy/host-cloudflare/src/mcp/index.ts
similarity index 100%
rename from apps/host-cloudflare/src/mcp/index.ts
rename to legacy/host-cloudflare/src/mcp/index.ts
diff --git a/apps/host-cloudflare/src/mcp/session-durable-object.ts b/legacy/host-cloudflare/src/mcp/session-durable-object.ts
similarity index 100%
rename from apps/host-cloudflare/src/mcp/session-durable-object.ts
rename to legacy/host-cloudflare/src/mcp/session-durable-object.ts
diff --git a/apps/host-cloudflare/src/mcp/session-store.ts b/legacy/host-cloudflare/src/mcp/session-store.ts
similarity index 100%
rename from apps/host-cloudflare/src/mcp/session-store.ts
rename to legacy/host-cloudflare/src/mcp/session-store.ts
diff --git a/apps/host-cloudflare/src/observability.ts b/legacy/host-cloudflare/src/observability.ts
similarity index 100%
rename from apps/host-cloudflare/src/observability.ts
rename to legacy/host-cloudflare/src/observability.ts
diff --git a/apps/host-cloudflare/src/plugins.ts b/legacy/host-cloudflare/src/plugins.ts
similarity index 100%
rename from apps/host-cloudflare/src/plugins.ts
rename to legacy/host-cloudflare/src/plugins.ts
diff --git a/apps/host-cloudflare/src/quickjs-engine.wasm b/legacy/host-cloudflare/src/quickjs-engine.wasm
similarity index 100%
rename from apps/host-cloudflare/src/quickjs-engine.wasm
rename to legacy/host-cloudflare/src/quickjs-engine.wasm
diff --git a/apps/host-cloudflare/src/quickjs.ts b/legacy/host-cloudflare/src/quickjs.ts
similarity index 100%
rename from apps/host-cloudflare/src/quickjs.ts
rename to legacy/host-cloudflare/src/quickjs.ts
diff --git a/apps/host-cloudflare/src/wasm.d.ts b/legacy/host-cloudflare/src/wasm.d.ts
similarity index 100%
rename from apps/host-cloudflare/src/wasm.d.ts
rename to legacy/host-cloudflare/src/wasm.d.ts
diff --git a/apps/host-cloudflare/src/worker.e2e.node.test.ts b/legacy/host-cloudflare/src/worker.e2e.node.test.ts
similarity index 100%
rename from apps/host-cloudflare/src/worker.e2e.node.test.ts
rename to legacy/host-cloudflare/src/worker.e2e.node.test.ts
diff --git a/apps/host-cloudflare/src/worker.ts b/legacy/host-cloudflare/src/worker.ts
similarity index 100%
rename from apps/host-cloudflare/src/worker.ts
rename to legacy/host-cloudflare/src/worker.ts
diff --git a/apps/host-cloudflare/tsconfig.json b/legacy/host-cloudflare/tsconfig.json
similarity index 100%
rename from apps/host-cloudflare/tsconfig.json
rename to legacy/host-cloudflare/tsconfig.json
diff --git a/apps/host-cloudflare/tsr.routes.ts b/legacy/host-cloudflare/tsr.routes.ts
similarity index 100%
rename from apps/host-cloudflare/tsr.routes.ts
rename to legacy/host-cloudflare/tsr.routes.ts
diff --git a/apps/host-cloudflare/vite.config.ts b/legacy/host-cloudflare/vite.config.ts
similarity index 100%
rename from apps/host-cloudflare/vite.config.ts
rename to legacy/host-cloudflare/vite.config.ts
diff --git a/apps/host-cloudflare/vitest.config.ts b/legacy/host-cloudflare/vitest.config.ts
similarity index 100%
rename from apps/host-cloudflare/vitest.config.ts
rename to legacy/host-cloudflare/vitest.config.ts
diff --git a/apps/host-cloudflare/web/entry-client.tsx b/legacy/host-cloudflare/web/entry-client.tsx
similarity index 100%
rename from apps/host-cloudflare/web/entry-client.tsx
rename to legacy/host-cloudflare/web/entry-client.tsx
diff --git a/apps/host-cloudflare/web/index.html b/legacy/host-cloudflare/web/index.html
similarity index 100%
rename from apps/host-cloudflare/web/index.html
rename to legacy/host-cloudflare/web/index.html
diff --git a/apps/host-cloudflare/web/routeTree.gen.ts b/legacy/host-cloudflare/web/routeTree.gen.ts
similarity index 100%
rename from apps/host-cloudflare/web/routeTree.gen.ts
rename to legacy/host-cloudflare/web/routeTree.gen.ts
diff --git a/apps/host-cloudflare/web/router.tsx b/legacy/host-cloudflare/web/router.tsx
similarity index 100%
rename from apps/host-cloudflare/web/router.tsx
rename to legacy/host-cloudflare/web/router.tsx
diff --git a/apps/host-cloudflare/web/routes/__root.tsx b/legacy/host-cloudflare/web/routes/__root.tsx
similarity index 100%
rename from apps/host-cloudflare/web/routes/__root.tsx
rename to legacy/host-cloudflare/web/routes/__root.tsx
diff --git a/apps/host-cloudflare/wrangler.jsonc b/legacy/host-cloudflare/wrangler.jsonc
similarity index 100%
rename from apps/host-cloudflare/wrangler.jsonc
rename to legacy/host-cloudflare/wrangler.jsonc
diff --git a/apps/host-selfhost/.env.example b/legacy/host-selfhost/.env.example
similarity index 100%
rename from apps/host-selfhost/.env.example
rename to legacy/host-selfhost/.env.example
diff --git a/apps/host-selfhost/CHANGELOG.md b/legacy/host-selfhost/CHANGELOG.md
similarity index 100%
rename from apps/host-selfhost/CHANGELOG.md
rename to legacy/host-selfhost/CHANGELOG.md
diff --git a/apps/host-selfhost/Dockerfile b/legacy/host-selfhost/Dockerfile
similarity index 83%
rename from apps/host-selfhost/Dockerfile
rename to legacy/host-selfhost/Dockerfile
index 689f161ab..97dbbbbe8 100644
--- a/apps/host-selfhost/Dockerfile
+++ b/legacy/host-selfhost/Dockerfile
@@ -1,6 +1,6 @@
# Self-hosted Executor, single container, no external services.
# Build context is the repo root because Bun workspaces need every member:
-# docker build -f apps/host-selfhost/Dockerfile -t executor-selfhost .
+# docker build -f legacy/host-selfhost/Dockerfile -t executor-selfhost .
#
# Runtime needs this container plus a volume for the data dir:
# docker run -p 4788:4788 -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
@@ -13,13 +13,13 @@ FROM oven/bun:1 AS prod-deps
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile --production --ignore-scripts --filter @executor-js/host-selfhost \
- && bun run apps/host-selfhost/scripts/package-runtime.ts
+ && bun run legacy/host-selfhost/scripts/package-runtime.ts
FROM oven/bun:1 AS build
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
-RUN cd apps/host-selfhost && bun run build
+RUN cd legacy/host-selfhost && bun run build
FROM gcr.io/distroless/cc-debian12 AS runtime
WORKDIR /app
@@ -32,8 +32,8 @@ ENV NODE_ENV=production \
EXECUTOR_DATA_DIR=/data
COPY --from=prod-deps /usr/local/bin/bun /usr/local/bin/bun
COPY --from=prod-deps /app/.selfhost-runtime /app
-COPY --from=build /app/apps/host-selfhost/dist /app/apps/host-selfhost/dist
-WORKDIR /app/apps/host-selfhost
+COPY --from=build /app/legacy/host-selfhost/dist /app/legacy/host-selfhost/dist
+WORKDIR /app/legacy/host-selfhost
VOLUME ["/data"]
EXPOSE 4788
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \
diff --git a/apps/host-selfhost/README.md b/legacy/host-selfhost/README.md
similarity index 100%
rename from apps/host-selfhost/README.md
rename to legacy/host-selfhost/README.md
diff --git a/apps/host-selfhost/docker-compose.yml b/legacy/host-selfhost/docker-compose.yml
similarity index 96%
rename from apps/host-selfhost/docker-compose.yml
rename to legacy/host-selfhost/docker-compose.yml
index ed6cc80de..f33caddfa 100644
--- a/apps/host-selfhost/docker-compose.yml
+++ b/legacy/host-selfhost/docker-compose.yml
@@ -17,7 +17,7 @@ services:
build:
# Build context is the repo root: the Bun workspace install needs every member.
context: ../..
- dockerfile: apps/host-selfhost/Dockerfile
+ dockerfile: legacy/host-selfhost/Dockerfile
image: executor-selfhost
restart: unless-stopped
ports:
diff --git a/apps/host-selfhost/executor.config.ts b/legacy/host-selfhost/executor.config.ts
similarity index 100%
rename from apps/host-selfhost/executor.config.ts
rename to legacy/host-selfhost/executor.config.ts
diff --git a/apps/host-selfhost/package.json b/legacy/host-selfhost/package.json
similarity index 98%
rename from apps/host-selfhost/package.json
rename to legacy/host-selfhost/package.json
index 7fe129ad0..021cbca9b 100644
--- a/apps/host-selfhost/package.json
+++ b/legacy/host-selfhost/package.json
@@ -13,6 +13,7 @@
"build": "turbo run build --filter @executor-js/vite-plugin && vite build",
"start": "bun run src/serve.ts",
"typecheck": "tsgo --noEmit",
+ "typecheck:slow": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"routes:gen": "bun scripts/gen-routes.ts"
diff --git a/apps/host-selfhost/scripts/gen-routes.ts b/legacy/host-selfhost/scripts/gen-routes.ts
similarity index 91%
rename from apps/host-selfhost/scripts/gen-routes.ts
rename to legacy/host-selfhost/scripts/gen-routes.ts
index 0c9f50acb..57cc304f2 100644
--- a/apps/host-selfhost/scripts/gen-routes.ts
+++ b/legacy/host-selfhost/scripts/gen-routes.ts
@@ -13,4 +13,4 @@ await generateRouteTree({
generatedRouteTree: fileURLToPath(new URL("../web/routeTree.gen.ts", import.meta.url)),
virtualRouteConfig: routes,
});
-console.log("generated apps/host-selfhost route tree");
+console.log("generated legacy/host-selfhost route tree");
diff --git a/apps/host-selfhost/scripts/package-runtime.ts b/legacy/host-selfhost/scripts/package-runtime.ts
similarity index 83%
rename from apps/host-selfhost/scripts/package-runtime.ts
rename to legacy/host-selfhost/scripts/package-runtime.ts
index d5c112341..3472f29bb 100644
--- a/apps/host-selfhost/scripts/package-runtime.ts
+++ b/legacy/host-selfhost/scripts/package-runtime.ts
@@ -4,8 +4,8 @@ import { createRequire } from "node:module";
const root = process.cwd();
const out = join(root, ".selfhost-runtime");
-const serverOut = join(out, "apps/host-selfhost/dist-server");
-const requireFromSelfHost = createRequire(join(root, "apps/host-selfhost/package.json"));
+const serverOut = join(out, "legacy/host-selfhost/dist-server");
+const requireFromSelfHost = createRequire(join(root, "legacy/host-selfhost/package.json"));
// libSQL ships its native addon as per-platform optional dependencies
// (`@libsql/--`), and Bun installs only the one matching the
@@ -29,7 +29,7 @@ const libsqlNativePackage = (): string => {
if (!target) {
throw new Error(
`package-runtime: no @libsql native package mapped for ${key}. ` +
- "Add it to the platform map in apps/host-selfhost/scripts/package-runtime.ts.",
+ "Add it to the platform map in legacy/host-selfhost/scripts/package-runtime.ts.",
);
}
return `@libsql/${target}`;
@@ -66,13 +66,13 @@ const copyPackage = (name: string): void => {
rmSync(out, { recursive: true, force: true });
mkdirSync(serverOut, { recursive: true });
-await Bun.$`bun build apps/host-selfhost/src/serve.ts --target=bun --format=esm --outdir=${serverOut} ${quickJsExternals.map((name) => `--external=${name}`)}`;
+await Bun.$`bun build legacy/host-selfhost/src/serve.ts --target=bun --format=esm --outdir=${serverOut} ${quickJsExternals.map((name) => `--external=${name}`)}`;
for (const name of externalPackages) copyPackage(name);
if (!existsSync(join(serverOut, "serve.js"))) {
throw new Error(
- "Expected bundled self-host server at .selfhost-runtime/apps/host-selfhost/dist-server/serve.js",
+ "Expected bundled self-host server at .selfhost-runtime/legacy/host-selfhost/dist-server/serve.js",
);
}
diff --git a/apps/host-selfhost/src/account/account-api.ts b/legacy/host-selfhost/src/account/account-api.ts
similarity index 100%
rename from apps/host-selfhost/src/account/account-api.ts
rename to legacy/host-selfhost/src/account/account-api.ts
diff --git a/apps/host-selfhost/src/account/better-auth-account-provider.ts b/legacy/host-selfhost/src/account/better-auth-account-provider.ts
similarity index 100%
rename from apps/host-selfhost/src/account/better-auth-account-provider.ts
rename to legacy/host-selfhost/src/account/better-auth-account-provider.ts
diff --git a/apps/host-selfhost/src/account/index.ts b/legacy/host-selfhost/src/account/index.ts
similarity index 100%
rename from apps/host-selfhost/src/account/index.ts
rename to legacy/host-selfhost/src/account/index.ts
diff --git a/apps/host-selfhost/src/admin/api.ts b/legacy/host-selfhost/src/admin/api.ts
similarity index 100%
rename from apps/host-selfhost/src/admin/api.ts
rename to legacy/host-selfhost/src/admin/api.ts
diff --git a/apps/host-selfhost/src/admin/handlers.ts b/legacy/host-selfhost/src/admin/handlers.ts
similarity index 100%
rename from apps/host-selfhost/src/admin/handlers.ts
rename to legacy/host-selfhost/src/admin/handlers.ts
diff --git a/apps/host-selfhost/src/admin/invites.node.test.ts b/legacy/host-selfhost/src/admin/invites.node.test.ts
similarity index 100%
rename from apps/host-selfhost/src/admin/invites.node.test.ts
rename to legacy/host-selfhost/src/admin/invites.node.test.ts
diff --git a/apps/host-selfhost/src/api/api.ts b/legacy/host-selfhost/src/api/api.ts
similarity index 100%
rename from apps/host-selfhost/src/api/api.ts
rename to legacy/host-selfhost/src/api/api.ts
diff --git a/apps/host-selfhost/src/app.ts b/legacy/host-selfhost/src/app.ts
similarity index 100%
rename from apps/host-selfhost/src/app.ts
rename to legacy/host-selfhost/src/app.ts
diff --git a/apps/host-selfhost/src/auth/better-auth.test.ts b/legacy/host-selfhost/src/auth/better-auth.test.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/better-auth.test.ts
rename to legacy/host-selfhost/src/auth/better-auth.test.ts
diff --git a/apps/host-selfhost/src/auth/better-auth.ts b/legacy/host-selfhost/src/auth/better-auth.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/better-auth.ts
rename to legacy/host-selfhost/src/auth/better-auth.ts
diff --git a/apps/host-selfhost/src/auth/force-mcp-consent.test.ts b/legacy/host-selfhost/src/auth/force-mcp-consent.test.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/force-mcp-consent.test.ts
rename to legacy/host-selfhost/src/auth/force-mcp-consent.test.ts
diff --git a/apps/host-selfhost/src/auth/force-mcp-consent.ts b/legacy/host-selfhost/src/auth/force-mcp-consent.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/force-mcp-consent.ts
rename to legacy/host-selfhost/src/auth/force-mcp-consent.ts
diff --git a/apps/host-selfhost/src/auth/identity.ts b/legacy/host-selfhost/src/auth/identity.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/identity.ts
rename to legacy/host-selfhost/src/auth/identity.ts
diff --git a/apps/host-selfhost/src/auth/index.ts b/legacy/host-selfhost/src/auth/index.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/index.ts
rename to legacy/host-selfhost/src/auth/index.ts
diff --git a/apps/host-selfhost/src/auth/invalid-origin-help.test.ts b/legacy/host-selfhost/src/auth/invalid-origin-help.test.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/invalid-origin-help.test.ts
rename to legacy/host-selfhost/src/auth/invalid-origin-help.test.ts
diff --git a/apps/host-selfhost/src/auth/invalid-origin-help.ts b/legacy/host-selfhost/src/auth/invalid-origin-help.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/invalid-origin-help.ts
rename to legacy/host-selfhost/src/auth/invalid-origin-help.ts
diff --git a/apps/host-selfhost/src/auth/invites.ts b/legacy/host-selfhost/src/auth/invites.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/invites.ts
rename to legacy/host-selfhost/src/auth/invites.ts
diff --git a/apps/host-selfhost/src/auth/oauth-callback-login.test.ts b/legacy/host-selfhost/src/auth/oauth-callback-login.test.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/oauth-callback-login.test.ts
rename to legacy/host-selfhost/src/auth/oauth-callback-login.test.ts
diff --git a/apps/host-selfhost/src/auth/oauth-callback-login.ts b/legacy/host-selfhost/src/auth/oauth-callback-login.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/oauth-callback-login.ts
rename to legacy/host-selfhost/src/auth/oauth-callback-login.ts
diff --git a/apps/host-selfhost/src/auth/origin-resolution.test.ts b/legacy/host-selfhost/src/auth/origin-resolution.test.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/origin-resolution.test.ts
rename to legacy/host-selfhost/src/auth/origin-resolution.test.ts
diff --git a/apps/host-selfhost/src/auth/return-to.test.ts b/legacy/host-selfhost/src/auth/return-to.test.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/return-to.test.ts
rename to legacy/host-selfhost/src/auth/return-to.test.ts
diff --git a/apps/host-selfhost/src/auth/return-to.ts b/legacy/host-selfhost/src/auth/return-to.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/return-to.ts
rename to legacy/host-selfhost/src/auth/return-to.ts
diff --git a/apps/host-selfhost/src/auth/seed.ts b/legacy/host-selfhost/src/auth/seed.ts
similarity index 100%
rename from apps/host-selfhost/src/auth/seed.ts
rename to legacy/host-selfhost/src/auth/seed.ts
diff --git a/apps/host-selfhost/src/boot.test.ts b/legacy/host-selfhost/src/boot.test.ts
similarity index 100%
rename from apps/host-selfhost/src/boot.test.ts
rename to legacy/host-selfhost/src/boot.test.ts
diff --git a/apps/host-selfhost/src/config.ts b/legacy/host-selfhost/src/config.ts
similarity index 100%
rename from apps/host-selfhost/src/config.ts
rename to legacy/host-selfhost/src/config.ts
diff --git a/apps/host-selfhost/src/db/auth-config-migration.ts b/legacy/host-selfhost/src/db/auth-config-migration.ts
similarity index 100%
rename from apps/host-selfhost/src/db/auth-config-migration.ts
rename to legacy/host-selfhost/src/db/auth-config-migration.ts
diff --git a/apps/host-selfhost/src/db/data-migrations.ts b/legacy/host-selfhost/src/db/data-migrations.ts
similarity index 100%
rename from apps/host-selfhost/src/db/data-migrations.ts
rename to legacy/host-selfhost/src/db/data-migrations.ts
diff --git a/apps/host-selfhost/src/db/self-host-db.ts b/legacy/host-selfhost/src/db/self-host-db.ts
similarity index 96%
rename from apps/host-selfhost/src/db/self-host-db.ts
rename to legacy/host-selfhost/src/db/self-host-db.ts
index fde91f89b..4788d10b1 100644
--- a/apps/host-selfhost/src/db/self-host-db.ts
+++ b/legacy/host-selfhost/src/db/self-host-db.ts
@@ -22,9 +22,9 @@ import type { FumaDb, FumaTables } from "@executor-js/sdk";
import { SELF_HOST_NAMESPACE, SELF_HOST_SCHEMA_VERSION } from "../config";
// ---------------------------------------------------------------------------
-// SQLite executor DB factory, inline (like apps/local's sqlite-fumadb.ts and
-// apps/cloud's fuma.ts — each app owns its DB wiring; there is no shared
-// storage package). Differences from apps/local: busy_timeout + synchronous
+// SQLite executor DB factory, inline (like legacy/local's sqlite-fumadb.ts and
+// legacy/cloud's fuma.ts, each app owns its DB wiring; there is no shared
+// storage package). Differences from legacy/local: busy_timeout + synchronous
// pragmas for the multi-user HTTP server, and the idempotent
// `ensureDrizzleRuntimeSchemaFromTables` schema-ensure (the drizzle adapter
// has no versioned migrator). Built ONCE for the process; the per-request
diff --git a/apps/host-selfhost/src/execution.ts b/legacy/host-selfhost/src/execution.ts
similarity index 100%
rename from apps/host-selfhost/src/execution.ts
rename to legacy/host-selfhost/src/execution.ts
diff --git a/apps/host-selfhost/src/first-run.node.test.ts b/legacy/host-selfhost/src/first-run.node.test.ts
similarity index 100%
rename from apps/host-selfhost/src/first-run.node.test.ts
rename to legacy/host-selfhost/src/first-run.node.test.ts
diff --git a/apps/host-selfhost/src/index.ts b/legacy/host-selfhost/src/index.ts
similarity index 100%
rename from apps/host-selfhost/src/index.ts
rename to legacy/host-selfhost/src/index.ts
diff --git a/apps/host-selfhost/src/mcp/auth.ts b/legacy/host-selfhost/src/mcp/auth.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/auth.ts
rename to legacy/host-selfhost/src/mcp/auth.ts
diff --git a/apps/host-selfhost/src/mcp/index.ts b/legacy/host-selfhost/src/mcp/index.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/index.ts
rename to legacy/host-selfhost/src/mcp/index.ts
diff --git a/apps/host-selfhost/src/mcp/mcp-oauth.test.ts b/legacy/host-selfhost/src/mcp/mcp-oauth.test.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/mcp-oauth.test.ts
rename to legacy/host-selfhost/src/mcp/mcp-oauth.test.ts
diff --git a/apps/host-selfhost/src/mcp/mcp.test.ts b/legacy/host-selfhost/src/mcp/mcp.test.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/mcp.test.ts
rename to legacy/host-selfhost/src/mcp/mcp.test.ts
diff --git a/apps/host-selfhost/src/mcp/org-path.test.ts b/legacy/host-selfhost/src/mcp/org-path.test.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/org-path.test.ts
rename to legacy/host-selfhost/src/mcp/org-path.test.ts
diff --git a/apps/host-selfhost/src/mcp/org-path.ts b/legacy/host-selfhost/src/mcp/org-path.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/org-path.ts
rename to legacy/host-selfhost/src/mcp/org-path.ts
diff --git a/apps/host-selfhost/src/mcp/session-store.ts b/legacy/host-selfhost/src/mcp/session-store.ts
similarity index 100%
rename from apps/host-selfhost/src/mcp/session-store.ts
rename to legacy/host-selfhost/src/mcp/session-store.ts
diff --git a/apps/host-selfhost/src/multi-user.test.ts b/legacy/host-selfhost/src/multi-user.test.ts
similarity index 100%
rename from apps/host-selfhost/src/multi-user.test.ts
rename to legacy/host-selfhost/src/multi-user.test.ts
diff --git a/apps/host-selfhost/src/observability.ts b/legacy/host-selfhost/src/observability.ts
similarity index 100%
rename from apps/host-selfhost/src/observability.ts
rename to legacy/host-selfhost/src/observability.ts
diff --git a/apps/host-selfhost/src/plugins.ts b/legacy/host-selfhost/src/plugins.ts
similarity index 90%
rename from apps/host-selfhost/src/plugins.ts
rename to legacy/host-selfhost/src/plugins.ts
index bbd4c65ef..ea83f6550 100644
--- a/apps/host-selfhost/src/plugins.ts
+++ b/legacy/host-selfhost/src/plugins.ts
@@ -1,5 +1,5 @@
// Single shared instantiation of the self-host plugin list, mirroring
-// `apps/cloud/src/api/cloud-plugins.ts`. The API composition
+// `legacy/cloud/src/api/cloud-plugins.ts`. The API composition
// (`composePluginApi`/`composePluginHandlerLayer`) and the per-request
// middleware (`providePluginExtensions`, `PluginExtensionServices<...>`) all
// derive their typed views from this one tuple, so adding/removing a plugin is
diff --git a/apps/host-selfhost/src/scope-isolation.test.ts b/legacy/host-selfhost/src/scope-isolation.test.ts
similarity index 100%
rename from apps/host-selfhost/src/scope-isolation.test.ts
rename to legacy/host-selfhost/src/scope-isolation.test.ts
diff --git a/apps/host-selfhost/src/secrets-integration.test.ts b/legacy/host-selfhost/src/secrets-integration.test.ts
similarity index 100%
rename from apps/host-selfhost/src/secrets-integration.test.ts
rename to legacy/host-selfhost/src/secrets-integration.test.ts
diff --git a/apps/host-selfhost/src/serve.ts b/legacy/host-selfhost/src/serve.ts
similarity index 97%
rename from apps/host-selfhost/src/serve.ts
rename to legacy/host-selfhost/src/serve.ts
index a521ab5f1..44385c2b4 100644
--- a/apps/host-selfhost/src/serve.ts
+++ b/legacy/host-selfhost/src/serve.ts
@@ -10,7 +10,7 @@
* - /docs Swagger
* - everything else: the built web SPA (static files + index.html fallback)
*
- * Run directly: bun run apps/host-selfhost/src/serve.ts (after `bun run build`)
+ * Run directly: bun run legacy/host-selfhost/src/serve.ts (after `bun run build`)
*/
import { fileURLToPath } from "node:url";
diff --git a/apps/host-selfhost/src/sources-mcp.test.ts b/legacy/host-selfhost/src/sources-mcp.test.ts
similarity index 100%
rename from apps/host-selfhost/src/sources-mcp.test.ts
rename to legacy/host-selfhost/src/sources-mcp.test.ts
diff --git a/apps/host-selfhost/src/sources.test.ts b/legacy/host-selfhost/src/sources.test.ts
similarity index 100%
rename from apps/host-selfhost/src/sources.test.ts
rename to legacy/host-selfhost/src/sources.test.ts
diff --git a/apps/host-selfhost/src/system/api.ts b/legacy/host-selfhost/src/system/api.ts
similarity index 100%
rename from apps/host-selfhost/src/system/api.ts
rename to legacy/host-selfhost/src/system/api.ts
diff --git a/apps/host-selfhost/src/system/handlers.ts b/legacy/host-selfhost/src/system/handlers.ts
similarity index 100%
rename from apps/host-selfhost/src/system/handlers.ts
rename to legacy/host-selfhost/src/system/handlers.ts
diff --git a/apps/host-selfhost/src/testing/mint-invite.ts b/legacy/host-selfhost/src/testing/mint-invite.ts
similarity index 100%
rename from apps/host-selfhost/src/testing/mint-invite.ts
rename to legacy/host-selfhost/src/testing/mint-invite.ts
diff --git a/apps/host-selfhost/src/testing/test-app.ts b/legacy/host-selfhost/src/testing/test-app.ts
similarity index 100%
rename from apps/host-selfhost/src/testing/test-app.ts
rename to legacy/host-selfhost/src/testing/test-app.ts
diff --git a/apps/host-selfhost/tsconfig.json b/legacy/host-selfhost/tsconfig.json
similarity index 100%
rename from apps/host-selfhost/tsconfig.json
rename to legacy/host-selfhost/tsconfig.json
diff --git a/apps/host-selfhost/tsr.routes.ts b/legacy/host-selfhost/tsr.routes.ts
similarity index 100%
rename from apps/host-selfhost/tsr.routes.ts
rename to legacy/host-selfhost/tsr.routes.ts
diff --git a/apps/host-selfhost/vite.config.ts b/legacy/host-selfhost/vite.config.ts
similarity index 100%
rename from apps/host-selfhost/vite.config.ts
rename to legacy/host-selfhost/vite.config.ts
diff --git a/apps/host-selfhost/vitest.config.ts b/legacy/host-selfhost/vitest.config.ts
similarity index 100%
rename from apps/host-selfhost/vitest.config.ts
rename to legacy/host-selfhost/vitest.config.ts
diff --git a/apps/host-selfhost/web/admin-atoms.tsx b/legacy/host-selfhost/web/admin-atoms.tsx
similarity index 100%
rename from apps/host-selfhost/web/admin-atoms.tsx
rename to legacy/host-selfhost/web/admin-atoms.tsx
diff --git a/apps/host-selfhost/web/admin-client.tsx b/legacy/host-selfhost/web/admin-client.tsx
similarity index 100%
rename from apps/host-selfhost/web/admin-client.tsx
rename to legacy/host-selfhost/web/admin-client.tsx
diff --git a/apps/host-selfhost/web/auth-client.ts b/legacy/host-selfhost/web/auth-client.ts
similarity index 100%
rename from apps/host-selfhost/web/auth-client.ts
rename to legacy/host-selfhost/web/auth-client.ts
diff --git a/apps/host-selfhost/web/chromeless/device-page.tsx b/legacy/host-selfhost/web/chromeless/device-page.tsx
similarity index 100%
rename from apps/host-selfhost/web/chromeless/device-page.tsx
rename to legacy/host-selfhost/web/chromeless/device-page.tsx
diff --git a/apps/host-selfhost/web/chromeless/mcp-consent-page.tsx b/legacy/host-selfhost/web/chromeless/mcp-consent-page.tsx
similarity index 100%
rename from apps/host-selfhost/web/chromeless/mcp-consent-page.tsx
rename to legacy/host-selfhost/web/chromeless/mcp-consent-page.tsx
diff --git a/apps/host-selfhost/web/entry-client.tsx b/legacy/host-selfhost/web/entry-client.tsx
similarity index 100%
rename from apps/host-selfhost/web/entry-client.tsx
rename to legacy/host-selfhost/web/entry-client.tsx
diff --git a/apps/host-selfhost/web/index.html b/legacy/host-selfhost/web/index.html
similarity index 100%
rename from apps/host-selfhost/web/index.html
rename to legacy/host-selfhost/web/index.html
diff --git a/apps/host-selfhost/web/login.tsx b/legacy/host-selfhost/web/login.tsx
similarity index 100%
rename from apps/host-selfhost/web/login.tsx
rename to legacy/host-selfhost/web/login.tsx
diff --git a/apps/host-selfhost/web/routeTree.gen.ts b/legacy/host-selfhost/web/routeTree.gen.ts
similarity index 100%
rename from apps/host-selfhost/web/routeTree.gen.ts
rename to legacy/host-selfhost/web/routeTree.gen.ts
diff --git a/apps/host-selfhost/web/router.tsx b/legacy/host-selfhost/web/router.tsx
similarity index 100%
rename from apps/host-selfhost/web/router.tsx
rename to legacy/host-selfhost/web/router.tsx
diff --git a/apps/host-selfhost/web/routes/__root.tsx b/legacy/host-selfhost/web/routes/__root.tsx
similarity index 100%
rename from apps/host-selfhost/web/routes/__root.tsx
rename to legacy/host-selfhost/web/routes/__root.tsx
diff --git a/apps/host-selfhost/web/routes/app/admin.tsx b/legacy/host-selfhost/web/routes/app/admin.tsx
similarity index 100%
rename from apps/host-selfhost/web/routes/app/admin.tsx
rename to legacy/host-selfhost/web/routes/app/admin.tsx
diff --git a/apps/host-selfhost/web/routes/app/api-keys.tsx b/legacy/host-selfhost/web/routes/app/api-keys.tsx
similarity index 100%
rename from apps/host-selfhost/web/routes/app/api-keys.tsx
rename to legacy/host-selfhost/web/routes/app/api-keys.tsx
diff --git a/apps/host-selfhost/web/routes/public/join.$code.tsx b/legacy/host-selfhost/web/routes/public/join.$code.tsx
similarity index 100%
rename from apps/host-selfhost/web/routes/public/join.$code.tsx
rename to legacy/host-selfhost/web/routes/public/join.$code.tsx
diff --git a/apps/host-selfhost/web/setup-status.ts b/legacy/host-selfhost/web/setup-status.ts
similarity index 100%
rename from apps/host-selfhost/web/setup-status.ts
rename to legacy/host-selfhost/web/setup-status.ts
diff --git a/apps/host-selfhost/web/setup.tsx b/legacy/host-selfhost/web/setup.tsx
similarity index 100%
rename from apps/host-selfhost/web/setup.tsx
rename to legacy/host-selfhost/web/setup.tsx
diff --git a/apps/local/CHANGELOG.md b/legacy/local/CHANGELOG.md
similarity index 100%
rename from apps/local/CHANGELOG.md
rename to legacy/local/CHANGELOG.md
diff --git a/apps/local/drizzle-legacy-v1/0000_overconfident_sharon_carter.sql b/legacy/local/drizzle-legacy-v1/0000_overconfident_sharon_carter.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0000_overconfident_sharon_carter.sql
rename to legacy/local/drizzle-legacy-v1/0000_overconfident_sharon_carter.sql
diff --git a/apps/local/drizzle-legacy-v1/0001_sour_catseye.sql b/legacy/local/drizzle-legacy-v1/0001_sour_catseye.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0001_sour_catseye.sql
rename to legacy/local/drizzle-legacy-v1/0001_sour_catseye.sql
diff --git a/apps/local/drizzle-legacy-v1/0002_lively_sue_storm.sql b/legacy/local/drizzle-legacy-v1/0002_lively_sue_storm.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0002_lively_sue_storm.sql
rename to legacy/local/drizzle-legacy-v1/0002_lively_sue_storm.sql
diff --git a/apps/local/drizzle-legacy-v1/0003_little_silk_fever.sql b/legacy/local/drizzle-legacy-v1/0003_little_silk_fever.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0003_little_silk_fever.sql
rename to legacy/local/drizzle-legacy-v1/0003_little_silk_fever.sql
diff --git a/apps/local/drizzle-legacy-v1/0004_add_tool_policy.sql b/legacy/local/drizzle-legacy-v1/0004_add_tool_policy.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0004_add_tool_policy.sql
rename to legacy/local/drizzle-legacy-v1/0004_add_tool_policy.sql
diff --git a/apps/local/drizzle-legacy-v1/0005_repair_mcp_oauth_session.sql b/legacy/local/drizzle-legacy-v1/0005_repair_mcp_oauth_session.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0005_repair_mcp_oauth_session.sql
rename to legacy/local/drizzle-legacy-v1/0005_repair_mcp_oauth_session.sql
diff --git a/apps/local/drizzle-legacy-v1/0006_neat_terror.sql b/legacy/local/drizzle-legacy-v1/0006_neat_terror.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0006_neat_terror.sql
rename to legacy/local/drizzle-legacy-v1/0006_neat_terror.sql
diff --git a/apps/local/drizzle-legacy-v1/0007_normalize_plugin_secret_refs.sql b/legacy/local/drizzle-legacy-v1/0007_normalize_plugin_secret_refs.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0007_normalize_plugin_secret_refs.sql
rename to legacy/local/drizzle-legacy-v1/0007_normalize_plugin_secret_refs.sql
diff --git a/apps/local/drizzle-legacy-v1/0008_scoped_credentials_cutover.sql b/legacy/local/drizzle-legacy-v1/0008_scoped_credentials_cutover.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0008_scoped_credentials_cutover.sql
rename to legacy/local/drizzle-legacy-v1/0008_scoped_credentials_cutover.sql
diff --git a/apps/local/drizzle-legacy-v1/0009_repair_openapi_oauth_cutover_residue.sql b/legacy/local/drizzle-legacy-v1/0009_repair_openapi_oauth_cutover_residue.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0009_repair_openapi_oauth_cutover_residue.sql
rename to legacy/local/drizzle-legacy-v1/0009_repair_openapi_oauth_cutover_residue.sql
diff --git a/apps/local/drizzle-legacy-v1/0010_add_credential_binding_secret_scope.sql b/legacy/local/drizzle-legacy-v1/0010_add_credential_binding_secret_scope.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0010_add_credential_binding_secret_scope.sql
rename to legacy/local/drizzle-legacy-v1/0010_add_credential_binding_secret_scope.sql
diff --git a/apps/local/drizzle-legacy-v1/0011_plugin_storage_sources.sql b/legacy/local/drizzle-legacy-v1/0011_plugin_storage_sources.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0011_plugin_storage_sources.sql
rename to legacy/local/drizzle-legacy-v1/0011_plugin_storage_sources.sql
diff --git a/apps/local/drizzle-legacy-v1/0012_connection_identity_override.sql b/legacy/local/drizzle-legacy-v1/0012_connection_identity_override.sql
similarity index 100%
rename from apps/local/drizzle-legacy-v1/0012_connection_identity_override.sql
rename to legacy/local/drizzle-legacy-v1/0012_connection_identity_override.sql
diff --git a/apps/local/drizzle-legacy-v1/meta/_journal.json b/legacy/local/drizzle-legacy-v1/meta/_journal.json
similarity index 100%
rename from apps/local/drizzle-legacy-v1/meta/_journal.json
rename to legacy/local/drizzle-legacy-v1/meta/_journal.json
diff --git a/apps/local/drizzle.config.ts b/legacy/local/drizzle.config.ts
similarity index 100%
rename from apps/local/drizzle.config.ts
rename to legacy/local/drizzle.config.ts
diff --git a/apps/local/drizzle/0000_v2_baseline.sql b/legacy/local/drizzle/0000_v2_baseline.sql
similarity index 100%
rename from apps/local/drizzle/0000_v2_baseline.sql
rename to legacy/local/drizzle/0000_v2_baseline.sql
diff --git a/apps/local/drizzle/0001_past_doctor_strange.sql b/legacy/local/drizzle/0001_past_doctor_strange.sql
similarity index 100%
rename from apps/local/drizzle/0001_past_doctor_strange.sql
rename to legacy/local/drizzle/0001_past_doctor_strange.sql
diff --git a/apps/local/drizzle/0002_empty_rictor.sql b/legacy/local/drizzle/0002_empty_rictor.sql
similarity index 100%
rename from apps/local/drizzle/0002_empty_rictor.sql
rename to legacy/local/drizzle/0002_empty_rictor.sql
diff --git a/apps/local/drizzle/meta/0000_snapshot.json b/legacy/local/drizzle/meta/0000_snapshot.json
similarity index 100%
rename from apps/local/drizzle/meta/0000_snapshot.json
rename to legacy/local/drizzle/meta/0000_snapshot.json
diff --git a/apps/local/drizzle/meta/0001_snapshot.json b/legacy/local/drizzle/meta/0001_snapshot.json
similarity index 100%
rename from apps/local/drizzle/meta/0001_snapshot.json
rename to legacy/local/drizzle/meta/0001_snapshot.json
diff --git a/apps/local/drizzle/meta/0002_snapshot.json b/legacy/local/drizzle/meta/0002_snapshot.json
similarity index 100%
rename from apps/local/drizzle/meta/0002_snapshot.json
rename to legacy/local/drizzle/meta/0002_snapshot.json
diff --git a/apps/local/drizzle/meta/_journal.json b/legacy/local/drizzle/meta/_journal.json
similarity index 100%
rename from apps/local/drizzle/meta/_journal.json
rename to legacy/local/drizzle/meta/_journal.json
diff --git a/apps/local/executor.config.ts b/legacy/local/executor.config.ts
similarity index 100%
rename from apps/local/executor.config.ts
rename to legacy/local/executor.config.ts
diff --git a/apps/local/package.json b/legacy/local/package.json
similarity index 100%
rename from apps/local/package.json
rename to legacy/local/package.json
diff --git a/apps/local/src/app.ts b/legacy/local/src/app.ts
similarity index 98%
rename from apps/local/src/app.ts
rename to legacy/local/src/app.ts
index a9915daf5..7142d6dad 100644
--- a/apps/local/src/app.ts
+++ b/legacy/local/src/app.ts
@@ -22,7 +22,7 @@ import { ErrorCaptureLive } from "./observability";
// (`-`, with `oauthEndpointUrlPolicy: { allowHttp: true }`),
// QuickJS in-process code execution, console error capture, Swagger at /docs —
// and NO account API, NO usage metering. `diff` against
-// `apps/host-selfhost/src/app.ts` is the whole product difference: local serves
+// `legacy/host-selfhost/src/app.ts` is the whole product difference: local serves
// its ONE cwd executor directly (the `fixedExecution` seam) instead of building
// a per-request `[user-org:…, org]` scoped executor from identity.
//
diff --git a/apps/local/src/auth-tool-failures.test.ts b/legacy/local/src/auth-tool-failures.test.ts
similarity index 100%
rename from apps/local/src/auth-tool-failures.test.ts
rename to legacy/local/src/auth-tool-failures.test.ts
diff --git a/apps/local/src/auth.ts b/legacy/local/src/auth.ts
similarity index 100%
rename from apps/local/src/auth.ts
rename to legacy/local/src/auth.ts
diff --git a/apps/local/src/db/auth-config-migration.ts b/legacy/local/src/db/auth-config-migration.ts
similarity index 100%
rename from apps/local/src/db/auth-config-migration.ts
rename to legacy/local/src/db/auth-config-migration.ts
diff --git a/apps/local/src/db/data-migrations.ts b/legacy/local/src/db/data-migrations.ts
similarity index 100%
rename from apps/local/src/db/data-migrations.ts
rename to legacy/local/src/db/data-migrations.ts
diff --git a/apps/local/src/db/embedded-migrations.gen.ts b/legacy/local/src/db/embedded-migrations.gen.ts
similarity index 100%
rename from apps/local/src/db/embedded-migrations.gen.ts
rename to legacy/local/src/db/embedded-migrations.gen.ts
diff --git a/apps/local/src/db/executor-schema.ts b/legacy/local/src/db/executor-schema.ts
similarity index 98%
rename from apps/local/src/db/executor-schema.ts
rename to legacy/local/src/db/executor-schema.ts
index 6d005e4c5..d86ffe7dd 100644
--- a/apps/local/src/db/executor-schema.ts
+++ b/legacy/local/src/db/executor-schema.ts
@@ -2,7 +2,7 @@
// Drizzle schema for the v2 executor core tables.
//
// This file exists ONLY to drive `drizzle-kit generate` for the committed
-// migration baseline (`apps/local/drizzle`). It is NOT used at runtime: the
+// migration baseline (`legacy/local/drizzle`). It is NOT used at runtime: the
// local server brings its SQLite schema up directly from the FumaDB
// `coreTables` definition via `createDrizzleRuntimeSchemaSqlFromTables`
// (see `./sqlite-fumadb.ts`). It mirrors the column set FumaDB derives from
diff --git a/apps/local/src/db/libsql.ts b/legacy/local/src/db/libsql.ts
similarity index 100%
rename from apps/local/src/db/libsql.ts
rename to legacy/local/src/db/libsql.ts
diff --git a/apps/local/src/db/migration-nesting.test.ts b/legacy/local/src/db/migration-nesting.test.ts
similarity index 100%
rename from apps/local/src/db/migration-nesting.test.ts
rename to legacy/local/src/db/migration-nesting.test.ts
diff --git a/apps/local/src/db/sqlite-fumadb.ts b/legacy/local/src/db/sqlite-fumadb.ts
similarity index 100%
rename from apps/local/src/db/sqlite-fumadb.ts
rename to legacy/local/src/db/sqlite-fumadb.ts
diff --git a/apps/local/src/db/v1-v2-boot-drive.test.ts b/legacy/local/src/db/v1-v2-boot-drive.test.ts
similarity index 99%
rename from apps/local/src/db/v1-v2-boot-drive.test.ts
rename to legacy/local/src/db/v1-v2-boot-drive.test.ts
index d8f78a561..a8423e68d 100644
--- a/apps/local/src/db/v1-v2-boot-drive.test.ts
+++ b/legacy/local/src/db/v1-v2-boot-drive.test.ts
@@ -95,7 +95,7 @@ const WidgetsLive = HttpApiBuilder.group(DriveApi, "widgets", (handlers) =>
// ---------------------------------------------------------------------------
// Boot the real local stack over a database file. Mirrors
-// apps/local/src/executor.ts's createLocalExecutorLayer (gate → DDL →
+// legacy/local/src/executor.ts's createLocalExecutorLayer (gate → DDL →
// ledger → createExecutor) with test-friendly plugins.
// ---------------------------------------------------------------------------
diff --git a/apps/local/src/db/v1-v2-ledger.test.ts b/legacy/local/src/db/v1-v2-ledger.test.ts
similarity index 99%
rename from apps/local/src/db/v1-v2-ledger.test.ts
rename to legacy/local/src/db/v1-v2-ledger.test.ts
index a7e3af88d..de549eca6 100644
--- a/apps/local/src/db/v1-v2-ledger.test.ts
+++ b/legacy/local/src/db/v1-v2-ledger.test.ts
@@ -138,7 +138,7 @@ const seedV1Content = async (dbPath: string): Promise => {
client.close();
};
-/** The boot sequence apps/local/src/executor.ts runs, minus the executor:
+/** The boot sequence legacy/local/src/executor.ts runs, minus the executor:
* v1 gate → fumadb DDL → ledger. Returns what each phase reported. */
const bootOnce = async (dbPath: string) => {
const migration = await migrateLocalV1ToV2IfNeeded({
diff --git a/apps/local/src/db/v1-v2-migration.test.ts b/legacy/local/src/db/v1-v2-migration.test.ts
similarity index 100%
rename from apps/local/src/db/v1-v2-migration.test.ts
rename to legacy/local/src/db/v1-v2-migration.test.ts
diff --git a/apps/local/src/db/v1-v2-migration.ts b/legacy/local/src/db/v1-v2-migration.ts
similarity index 99%
rename from apps/local/src/db/v1-v2-migration.ts
rename to legacy/local/src/db/v1-v2-migration.ts
index 651591b5c..c90d05c3b 100644
--- a/apps/local/src/db/v1-v2-migration.ts
+++ b/legacy/local/src/db/v1-v2-migration.ts
@@ -139,7 +139,7 @@ const isLocalV1Database = async (client: Client): Promise => {
//
// Once a database has passed the v1 gate (either it was migrated, or it was
// inspected and found to be v2-native), the boot ledger stamps
-// LOCAL_V1_V2_LEDGER_NAME (see apps/local/src/db/data-migrations.ts). On
+// LOCAL_V1_V2_LEDGER_NAME (see legacy/local/src/db/data-migrations.ts). On
// every later boot the stamp short-circuits this module before any schema
// probing — the stamp row, not the data shape, is the source of truth.
// ---------------------------------------------------------------------------
@@ -161,8 +161,8 @@ const hasV1GateStamp = async (client: Client): Promise => {
// The v1→v2 data migration below reads the v1-FINAL schema (it queries
// `plugin_storage`, which only exists after v1 migration 0011). A database
// last touched by an older release is still mid-chain, so replay the bundled
-// legacy drizzle migrations (`apps/local/drizzle-legacy-v1`, embedded into
-// the binary by apps/cli/src/build.ts) to bring it to v1-final first — the
+// legacy drizzle migrations (`legacy/local/drizzle-legacy-v1`, embedded into
+// the binary by legacy/cli/src/build.ts) to bring it to v1-final first, then the
// same step every pre-v1.5 release performed at startup.
// ---------------------------------------------------------------------------
diff --git a/apps/local/src/executor.ts b/legacy/local/src/executor.ts
similarity index 100%
rename from apps/local/src/executor.ts
rename to legacy/local/src/executor.ts
diff --git a/apps/local/src/identity.test.ts b/legacy/local/src/identity.test.ts
similarity index 100%
rename from apps/local/src/identity.test.ts
rename to legacy/local/src/identity.test.ts
diff --git a/apps/local/src/identity.ts b/legacy/local/src/identity.ts
similarity index 100%
rename from apps/local/src/identity.ts
rename to legacy/local/src/identity.ts
diff --git a/apps/local/src/index.ts b/legacy/local/src/index.ts
similarity index 100%
rename from apps/local/src/index.ts
rename to legacy/local/src/index.ts
diff --git a/apps/local/src/installation.ts b/legacy/local/src/installation.ts
similarity index 91%
rename from apps/local/src/installation.ts
rename to legacy/local/src/installation.ts
index 90f7ea50c..7c7cb5047 100644
--- a/apps/local/src/installation.ts
+++ b/legacy/local/src/installation.ts
@@ -8,7 +8,7 @@ const pkg = await import("../package.json");
const LOCAL_VERSION: string = pkg.version;
// A `-` in semver indicates a prerelease (beta train).
-// TODO: source channel from release infra once it lands; mirrors apps/cli.
+// TODO: source channel from release infra once it lands; mirrors legacy/cli.
const resolveChannel = (version: string): InstallationChannel => {
if (version.includes("-")) return "beta";
if (version === "0.0.0" || version === "local") return "dev";
@@ -16,7 +16,7 @@ const resolveChannel = (version: string): InstallationChannel => {
};
// The desktop main process sets `EXECUTOR_CLIENT=desktop` when it spawns the
-// sidecar so PostHog can slice desktop installs from headless apps/local
+// sidecar so PostHog can slice desktop installs from headless legacy/local
// (CLI `executor web`, `daemon run --foreground`, etc.).
const resolveClient = (): SurfaceClient =>
process.env.EXECUTOR_CLIENT === "desktop" ? "desktop" : "local";
diff --git a/apps/local/src/integrations.ts b/legacy/local/src/integrations.ts
similarity index 93%
rename from apps/local/src/integrations.ts
rename to legacy/local/src/integrations.ts
index 549cdfe91..0621513bf 100644
--- a/apps/local/src/integrations.ts
+++ b/legacy/local/src/integrations.ts
@@ -12,7 +12,7 @@ import { USER_AGENT } from "./installation";
// Module-singleton runtime for the integrations.sh registry. The layer's
// scoped fork fetches the registry at startup and refreshes on a 12-hour
// cadence for the runtime's lifetime. Shared across every long-running
-// apps/local surface (HTTP server, stdio MCP) so concurrent surfaces don't
+// legacy/local surface (HTTP server, stdio MCP) so concurrent surfaces don't
// each spin up their own refresh fork.
const integrationsRuntime = ManagedRuntime.make(
integrationsRegistryLayer({ userAgent: USER_AGENT }).pipe(
diff --git a/apps/local/src/main.ts b/legacy/local/src/main.ts
similarity index 100%
rename from apps/local/src/main.ts
rename to legacy/local/src/main.ts
diff --git a/apps/local/src/mcp-browser-resume.test.ts b/legacy/local/src/mcp-browser-resume.test.ts
similarity index 100%
rename from apps/local/src/mcp-browser-resume.test.ts
rename to legacy/local/src/mcp-browser-resume.test.ts
diff --git a/apps/local/src/mcp-oauth.test.ts b/legacy/local/src/mcp-oauth.test.ts
similarity index 100%
rename from apps/local/src/mcp-oauth.test.ts
rename to legacy/local/src/mcp-oauth.test.ts
diff --git a/apps/local/src/mcp.ts b/legacy/local/src/mcp.ts
similarity index 100%
rename from apps/local/src/mcp.ts
rename to legacy/local/src/mcp.ts
diff --git a/apps/local/src/oauth-result-store.ts b/legacy/local/src/oauth-result-store.ts
similarity index 100%
rename from apps/local/src/oauth-result-store.ts
rename to legacy/local/src/oauth-result-store.ts
diff --git a/apps/local/src/observability.ts b/legacy/local/src/observability.ts
similarity index 100%
rename from apps/local/src/observability.ts
rename to legacy/local/src/observability.ts
diff --git a/apps/local/src/serve-shared.test.ts b/legacy/local/src/serve-shared.test.ts
similarity index 100%
rename from apps/local/src/serve-shared.test.ts
rename to legacy/local/src/serve-shared.test.ts
diff --git a/apps/local/src/serve-shared.ts b/legacy/local/src/serve-shared.ts
similarity index 100%
rename from apps/local/src/serve-shared.ts
rename to legacy/local/src/serve-shared.ts
diff --git a/apps/local/src/serve.test.ts b/legacy/local/src/serve.test.ts
similarity index 100%
rename from apps/local/src/serve.test.ts
rename to legacy/local/src/serve.test.ts
diff --git a/apps/local/src/serve.ts b/legacy/local/src/serve.ts
similarity index 98%
rename from apps/local/src/serve.ts
rename to legacy/local/src/serve.ts
index 474cbd411..96c2d91a8 100644
--- a/apps/local/src/serve.ts
+++ b/legacy/local/src/serve.ts
@@ -3,7 +3,7 @@
*
* Serves the Vite-built SPA + Effect API + MCP server.
*
- * Run directly: bun run apps/local/src/serve.ts
+ * Run directly: bun run legacy/local/src/serve.ts
* Or import: import { startServer } from "@executor-js/local/serve"
*/
@@ -102,7 +102,7 @@ function embeddedToStaticRoutes(embedded: Record): Record {
const env = { ...process.env };
delete env.PORT;
// `bunx --bun vite` runs vite under Bun, matching the `dev:vite` script
- // already in apps/local. --strictPort keeps the URL we hand back stable.
+ // already in legacy/local. --strictPort keeps the URL we hand back stable.
const child: Subprocess = Bun.spawn(
[
"bunx",
diff --git a/apps/local/src/vite-env.d.ts b/legacy/local/src/vite-env.d.ts
similarity index 100%
rename from apps/local/src/vite-env.d.ts
rename to legacy/local/src/vite-env.d.ts
diff --git a/apps/local/tsconfig.json b/legacy/local/tsconfig.json
similarity index 100%
rename from apps/local/tsconfig.json
rename to legacy/local/tsconfig.json
diff --git a/apps/local/vite.config.ts b/legacy/local/vite.config.ts
similarity index 99%
rename from apps/local/vite.config.ts
rename to legacy/local/vite.config.ts
index a44a088f1..ec9d2434e 100644
--- a/apps/local/vite.config.ts
+++ b/legacy/local/vite.config.ts
@@ -92,7 +92,7 @@ function executorApiPlugin(): Plugin {
}
server.watcher.on("change", (path) => {
- if (path.includes("/apps/local/src/") || path.endsWith("/executor.config.ts")) {
+ if (path.includes("/legacy/local/src/") || path.endsWith("/executor.config.ts")) {
handlers = null;
}
});
diff --git a/legacy/local/vitest.config.ts b/legacy/local/vitest.config.ts
new file mode 100644
index 000000000..ae847ff6d
--- /dev/null
+++ b/legacy/local/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: ["src/**/*.test.ts"],
+ },
+});
diff --git a/migrations/20260623000000_initial.sql b/migrations/20260623000000_initial.sql
new file mode 100644
index 000000000..0684174f4
--- /dev/null
+++ b/migrations/20260623000000_initial.sql
@@ -0,0 +1,45 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE instance_metadata (
+ key TEXT PRIMARY KEY NOT NULL,
+ value BLOB NOT NULL
+);
+
+CREATE TABLE setup_state (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ token_digest BLOB NOT NULL,
+ created_at INTEGER NOT NULL,
+ used_at INTEGER
+);
+
+CREATE TABLE admins (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ username TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE admin_sessions (
+ session_digest BLOB PRIMARY KEY NOT NULL,
+ admin_id INTEGER NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
+ csrf_digest BLOB NOT NULL,
+ created_at INTEGER NOT NULL,
+ expires_at INTEGER NOT NULL
+);
+
+CREATE INDEX admin_sessions_expiry_idx ON admin_sessions(expires_at);
+
+CREATE TABLE api_tokens (
+ id TEXT PRIMARY KEY NOT NULL,
+ name TEXT NOT NULL,
+ token_digest BLOB NOT NULL UNIQUE,
+ token_prefix TEXT NOT NULL,
+ token_suffix TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ last_used_at INTEGER,
+ revoked_at INTEGER
+);
+
+CREATE INDEX api_tokens_active_digest_idx
+ ON api_tokens(token_digest)
+ WHERE revoked_at IS NULL;
diff --git a/migrations/20260623010000_catalog.sql b/migrations/20260623010000_catalog.sql
new file mode 100644
index 000000000..bb9741ff0
--- /dev/null
+++ b/migrations/20260623010000_catalog.sql
@@ -0,0 +1,175 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE catalog_state (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+) STRICT;
+
+INSERT INTO catalog_state (id, revision, created_at, updated_at)
+VALUES (1, 0, unixepoch(), unixepoch());
+
+CREATE TABLE sources (
+ id TEXT PRIMARY KEY NOT NULL CHECK (length(id) BETWEEN 1 AND 128),
+ kind TEXT NOT NULL CHECK (kind IN ('openapi', 'graphql', 'mcp_http', 'mcp_stdio')),
+ slug TEXT NOT NULL UNIQUE
+ CHECK (length(slug) BETWEEN 1 AND 63)
+ CHECK (substr(slug, 1, 1) BETWEEN 'a' AND 'z')
+ CHECK (slug NOT GLOB '*[^a-z0-9_]*')
+ CHECK (slug NOT IN ('tools', 'search', 'describe', 'executor')),
+ search_short_grams TEXT NOT NULL DEFAULT '',
+ display_name TEXT NOT NULL CHECK (length(display_name) BETWEEN 1 AND 200),
+ description TEXT CHECK (description IS NULL OR length(description) <= 2000),
+ configuration_json TEXT NOT NULL DEFAULT '{}'
+ CHECK (json_valid(configuration_json))
+ CHECK (json_type(configuration_json) = 'object'),
+ mode_override TEXT CHECK (mode_override IS NULL OR mode_override IN ('enabled', 'ask', 'disabled')),
+ health_status TEXT NOT NULL DEFAULT 'unknown'
+ CHECK (health_status IN ('unknown', 'healthy', 'error')),
+ health_error_code TEXT
+ CHECK (health_error_code IS NULL OR length(health_error_code) BETWEEN 1 AND 128),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ catalog_revision INTEGER NOT NULL DEFAULT 0 CHECK (catalog_revision >= 0),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ last_refreshed_at INTEGER,
+ CHECK (
+ (health_status = 'error' AND health_error_code IS NOT NULL)
+ OR (health_status <> 'error' AND health_error_code IS NULL)
+ )
+) STRICT;
+
+CREATE INDEX sources_kind_slug_idx ON sources(kind, slug);
+
+CREATE TABLE source_credentials (
+ source_id TEXT PRIMARY KEY NOT NULL
+ REFERENCES sources(id) ON DELETE CASCADE,
+ schema_version INTEGER NOT NULL CHECK (schema_version > 0),
+ payload_ciphertext BLOB NOT NULL CHECK (length(payload_ciphertext) > 0),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+) STRICT;
+
+CREATE TABLE source_artifacts (
+ id TEXT PRIMARY KEY NOT NULL CHECK (length(id) BETWEEN 1 AND 128),
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
+ artifact_kind TEXT NOT NULL
+ CHECK (artifact_kind IN ('openapi_document', 'graphql_schema', 'mcp_capabilities', 'metadata')),
+ stable_key TEXT NOT NULL CHECK (length(stable_key) BETWEEN 1 AND 512),
+ content_json TEXT NOT NULL CHECK (json_valid(content_json)),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ UNIQUE (source_id, artifact_kind, stable_key)
+) STRICT;
+
+CREATE TABLE tools (
+ id TEXT PRIMARY KEY NOT NULL CHECK (length(id) BETWEEN 1 AND 128),
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
+ stable_key TEXT NOT NULL CHECK (length(stable_key) BETWEEN 1 AND 1024),
+ local_name TEXT NOT NULL
+ CHECK (length(local_name) BETWEEN 1 AND 128)
+ CHECK (substr(local_name, 1, 1) BETWEEN 'a' AND 'z')
+ CHECK (local_name NOT GLOB '*[^a-z0-9_]*'),
+ display_name TEXT NOT NULL CHECK (length(display_name) BETWEEN 1 AND 300),
+ description TEXT CHECK (description IS NULL OR length(description) <= 4000),
+ search_description TEXT NOT NULL DEFAULT '',
+ search_short_grams TEXT NOT NULL DEFAULT '',
+ input_schema_json TEXT NOT NULL CHECK (json_valid(input_schema_json)),
+ output_schema_json TEXT CHECK (output_schema_json IS NULL OR json_valid(output_schema_json)),
+ input_typescript TEXT CHECK (input_typescript IS NULL OR length(input_typescript) <= 100000),
+ output_typescript TEXT CHECK (output_typescript IS NULL OR length(output_typescript) <= 100000),
+ typescript_definitions_json TEXT NOT NULL DEFAULT '{}'
+ CHECK (json_valid(typescript_definitions_json))
+ CHECK (json_type(typescript_definitions_json) = 'object'),
+ intrinsic_mode TEXT NOT NULL CHECK (intrinsic_mode IN ('enabled', 'ask', 'disabled')),
+ mode_override TEXT CHECK (mode_override IS NULL OR mode_override IN ('enabled', 'ask', 'disabled')),
+ present INTEGER NOT NULL DEFAULT 1 CHECK (present IN (0, 1)),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ last_seen_at INTEGER NOT NULL,
+ tombstoned_at INTEGER,
+ UNIQUE (source_id, stable_key),
+ UNIQUE (source_id, local_name),
+ CHECK (
+ (present = 1 AND tombstoned_at IS NULL)
+ OR (present = 0 AND tombstoned_at IS NOT NULL)
+ )
+) STRICT;
+
+CREATE INDEX tools_source_presence_name_idx
+ ON tools(source_id, present, local_name);
+CREATE INDEX tools_presence_mode_idx
+ ON tools(present, intrinsic_mode, mode_override);
+
+CREATE VIRTUAL TABLE tool_search USING fts5(
+ source_id UNINDEXED,
+ tool_id UNINDEXED,
+ source_slug,
+ local_name,
+ description,
+ sandbox_path,
+ tokenize = 'unicode61'
+);
+
+CREATE VIRTUAL TABLE tool_search_trigram USING fts5(
+ source_id UNINDEXED,
+ tool_id UNINDEXED,
+ source_slug,
+ local_name,
+ description,
+ sandbox_path,
+ tokenize = 'trigram'
+);
+
+CREATE VIRTUAL TABLE tool_search_short USING fts5(
+ source_id UNINDEXED,
+ tool_id UNINDEXED,
+ grams,
+ tokenize = 'unicode61'
+);
+
+CREATE TABLE request_logs (
+ request_id TEXT PRIMARY KEY NOT NULL CHECK (length(request_id) BETWEEN 1 AND 128),
+ actor_api_token_id TEXT REFERENCES api_tokens(id) ON DELETE SET NULL,
+ surface TEXT NOT NULL CHECK (surface IN ('admin', 'gateway', 'cli', 'mcp')),
+ source_id TEXT REFERENCES sources(id) ON DELETE SET NULL,
+ tool_id TEXT REFERENCES tools(id) ON DELETE SET NULL,
+ path_snapshot TEXT CHECK (path_snapshot IS NULL OR length(path_snapshot) BETWEEN 1 AND 512),
+ outcome TEXT NOT NULL CHECK (outcome IN ('succeeded', 'failed', 'pending_approval', 'denied')),
+ error_code TEXT CHECK (error_code IS NULL OR length(error_code) BETWEEN 1 AND 128),
+ duration_ms INTEGER NOT NULL CHECK (duration_ms >= 0),
+ approval_id TEXT CHECK (approval_id IS NULL OR length(approval_id) BETWEEN 1 AND 128),
+ created_at INTEGER NOT NULL,
+ CHECK ((source_id IS NULL AND tool_id IS NULL) OR path_snapshot IS NOT NULL)
+) STRICT;
+
+CREATE INDEX request_logs_created_cursor_idx
+ ON request_logs(created_at DESC, request_id DESC);
+CREATE INDEX request_logs_actor_created_idx
+ ON request_logs(actor_api_token_id, created_at DESC);
+CREATE INDEX request_logs_tool_created_idx
+ ON request_logs(tool_id, created_at DESC);
+
+CREATE TABLE audit_events (
+ id TEXT PRIMARY KEY NOT NULL CHECK (length(id) BETWEEN 1 AND 128),
+ request_id TEXT CHECK (request_id IS NULL OR length(request_id) BETWEEN 1 AND 128),
+ actor_admin_id INTEGER REFERENCES admins(id) ON DELETE SET NULL,
+ action TEXT NOT NULL CHECK (length(action) BETWEEN 1 AND 128),
+ source_id TEXT REFERENCES sources(id) ON DELETE SET NULL,
+ tool_id TEXT REFERENCES tools(id) ON DELETE SET NULL,
+ target_path_snapshot TEXT
+ CHECK (target_path_snapshot IS NULL OR length(target_path_snapshot) BETWEEN 1 AND 512),
+ metadata_json TEXT NOT NULL DEFAULT '{}'
+ CHECK (json_valid(metadata_json))
+ CHECK (json_type(metadata_json) = 'object'),
+ created_at INTEGER NOT NULL
+) STRICT;
+
+CREATE INDEX audit_events_created_idx
+ ON audit_events(created_at DESC, id DESC);
+CREATE INDEX audit_events_source_created_idx
+ ON audit_events(source_id, created_at DESC);
diff --git a/migrations/20260623020000_tool_bindings.sql b/migrations/20260623020000_tool_bindings.sql
new file mode 100644
index 000000000..1bfe67f0f
--- /dev/null
+++ b/migrations/20260623020000_tool_bindings.sql
@@ -0,0 +1,17 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE tool_bindings (
+ tool_id TEXT PRIMARY KEY NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
+ protocol TEXT NOT NULL
+ CHECK (protocol IN ('openapi', 'graphql', 'mcp_http', 'mcp_stdio')),
+ binding_version INTEGER NOT NULL CHECK (binding_version > 0),
+ definition_json TEXT NOT NULL
+ CHECK (json_valid(definition_json))
+ CHECK (json_type(definition_json) = 'object'),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+) STRICT;
+
+CREATE INDEX tool_bindings_protocol_idx
+ ON tool_bindings(protocol, binding_version, tool_id);
diff --git a/migrations/20260623030000_approvals.sql b/migrations/20260623030000_approvals.sql
new file mode 100644
index 000000000..d44834c9a
--- /dev/null
+++ b/migrations/20260623030000_approvals.sql
@@ -0,0 +1,212 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE approval_clock (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ effective_now INTEGER NOT NULL CHECK (effective_now >= 0)
+) STRICT;
+
+INSERT INTO approval_clock (id, effective_now) VALUES (1, 0);
+
+CREATE TABLE approvals (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ id TEXT NOT NULL UNIQUE CHECK (length(id) BETWEEN 1 AND 128),
+ execution_id TEXT NOT NULL CHECK (length(execution_id) BETWEEN 1 AND 128),
+ call_id TEXT NOT NULL CHECK (length(call_id) BETWEEN 1 AND 128),
+ worker_generation INTEGER NOT NULL CHECK (worker_generation >= 0),
+ actor_kind TEXT NOT NULL CHECK (actor_kind IN ('api_token', 'admin', 'system')),
+ actor_id TEXT NOT NULL CHECK (length(actor_id) BETWEEN 1 AND 128),
+ actor_api_token_id TEXT REFERENCES api_tokens(id) ON DELETE RESTRICT,
+ actor_name_snapshot TEXT
+ CHECK (actor_name_snapshot IS NULL OR length(actor_name_snapshot) BETWEEN 1 AND 200),
+ surface TEXT NOT NULL CHECK (surface IN ('gateway', 'cli', 'mcp')),
+ source_id TEXT NOT NULL CHECK (length(source_id) BETWEEN 1 AND 128),
+ tool_id TEXT NOT NULL CHECK (length(tool_id) BETWEEN 1 AND 128),
+ callable_path_snapshot TEXT NOT NULL
+ CHECK (length(callable_path_snapshot) BETWEEN 1 AND 512),
+ source_display_name_snapshot TEXT
+ CHECK (source_display_name_snapshot IS NULL OR length(source_display_name_snapshot) BETWEEN 1 AND 300),
+ tool_display_name_snapshot TEXT
+ CHECK (tool_display_name_snapshot IS NULL OR length(tool_display_name_snapshot) BETWEEN 1 AND 300),
+ mode_provenance TEXT NOT NULL
+ CHECK (mode_provenance IN ('tool_override', 'source_override', 'intrinsic')),
+ source_revision INTEGER NOT NULL CHECK (source_revision >= 0),
+ catalog_revision INTEGER NOT NULL CHECK (catalog_revision >= 0),
+ tool_revision INTEGER NOT NULL CHECK (tool_revision >= 0),
+ binding_revision INTEGER NOT NULL CHECK (binding_revision >= 0),
+ credential_revision INTEGER CHECK (credential_revision IS NULL OR credential_revision >= 0),
+ arguments_digest BLOB NOT NULL CHECK (length(arguments_digest) = 32),
+ arguments_ciphertext BLOB NOT NULL CHECK (length(arguments_ciphertext) BETWEEN 42 AND 8388650),
+ redacted_arguments_ciphertext BLOB NOT NULL
+ CHECK (length(redacted_arguments_ciphertext) BETWEEN 42 AND 8388650),
+ input_schema_ciphertext BLOB NOT NULL CHECK (length(input_schema_ciphertext) BETWEEN 42 AND 2097194),
+ output_schema_ciphertext BLOB CHECK (
+ output_schema_ciphertext IS NULL
+ OR length(output_schema_ciphertext) BETWEEN 42 AND 2097194
+ ),
+ invocation_snapshot_ciphertext BLOB NOT NULL
+ CHECK (length(invocation_snapshot_ciphertext) BETWEEN 42 AND 8388650),
+ result_ciphertext BLOB CHECK (
+ result_ciphertext IS NULL
+ OR length(result_ciphertext) BETWEEN 42 AND 8388650
+ ),
+ status TEXT NOT NULL CHECK (
+ status IN (
+ 'pending', 'approved', 'denied', 'expired', 'canceled',
+ 'executing', 'succeeded', 'failed', 'stale', 'interrupted'
+ )
+ ),
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
+ decision_id TEXT CHECK (decision_id IS NULL OR length(decision_id) BETWEEN 1 AND 128),
+ decision TEXT CHECK (decision IS NULL OR decision IN ('approve', 'deny')),
+ decided_by_admin_id INTEGER REFERENCES admins(id) ON DELETE SET NULL,
+ failure_code TEXT CHECK (failure_code IS NULL OR length(failure_code) BETWEEN 1 AND 128),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ expires_at INTEGER NOT NULL CHECK (expires_at > created_at),
+ decided_at INTEGER,
+ execution_started_at INTEGER,
+ completed_at INTEGER,
+ CHECK (
+ (actor_kind = 'api_token' AND actor_api_token_id IS NOT NULL AND actor_id = actor_api_token_id)
+ OR (actor_kind IN ('admin', 'system') AND actor_api_token_id IS NULL)
+ ),
+ CHECK (
+ (decision_id IS NULL AND decision IS NULL AND decided_at IS NULL AND decided_by_admin_id IS NULL)
+ OR (decision_id IS NOT NULL AND decision IS NOT NULL AND decided_at IS NOT NULL)
+ ),
+ CHECK (
+ status NOT IN ('approved', 'denied')
+ OR decision IS NOT NULL
+ ),
+ CHECK (
+ (status = 'executing' AND execution_started_at IS NOT NULL)
+ OR status <> 'executing'
+ ),
+ CHECK (
+ (status IN ('succeeded', 'failed', 'stale', 'interrupted', 'denied', 'expired', 'canceled')
+ AND completed_at IS NOT NULL)
+ OR status IN ('pending', 'approved', 'executing')
+ ),
+ UNIQUE (actor_kind, actor_id, execution_id, call_id)
+) STRICT;
+
+CREATE INDEX approvals_admin_cursor_idx
+ ON approvals(sequence DESC);
+CREATE INDEX approvals_status_cursor_idx
+ ON approvals(status, sequence DESC);
+CREATE INDEX approvals_actor_cursor_idx
+ ON approvals(actor_kind, actor_id, sequence DESC);
+CREATE INDEX approvals_execution_idx
+ ON approvals(execution_id, status);
+
+CREATE TABLE approval_terminal_order (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ approval_id TEXT NOT NULL UNIQUE
+ REFERENCES approvals(id) ON DELETE CASCADE
+) STRICT;
+
+CREATE TABLE approval_log_outbox (
+ request_id TEXT PRIMARY KEY NOT NULL CHECK (length(request_id) BETWEEN 1 AND 128),
+ approval_id TEXT NOT NULL CHECK (length(approval_id) BETWEEN 1 AND 128),
+ actor_api_token_id TEXT CHECK (
+ actor_api_token_id IS NULL OR length(actor_api_token_id) BETWEEN 1 AND 128
+ ),
+ surface TEXT NOT NULL CHECK (surface IN ('gateway', 'cli', 'mcp')),
+ source_id TEXT NOT NULL CHECK (length(source_id) BETWEEN 1 AND 128),
+ tool_id TEXT NOT NULL CHECK (length(tool_id) BETWEEN 1 AND 128),
+ path_snapshot TEXT NOT NULL CHECK (length(path_snapshot) BETWEEN 1 AND 512),
+ outcome TEXT NOT NULL CHECK (outcome IN ('succeeded', 'failed', 'denied')),
+ error_code TEXT CHECK (error_code IS NULL OR length(error_code) BETWEEN 1 AND 128),
+ created_at INTEGER NOT NULL
+) STRICT;
+
+CREATE TABLE approval_log_outbox_state (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ dropped_count INTEGER NOT NULL DEFAULT 0 CHECK (dropped_count >= 0)
+) STRICT;
+
+INSERT INTO approval_log_outbox_state (id, dropped_count) VALUES (1, 0);
+
+CREATE TRIGGER approvals_fixed_expiry
+BEFORE UPDATE OF created_at, expires_at ON approvals
+WHEN NEW.created_at <> OLD.created_at OR NEW.expires_at <> OLD.expires_at
+BEGIN
+ SELECT RAISE(ABORT, 'approval expiry is immutable');
+END;
+
+CREATE TRIGGER approvals_immutable_snapshot
+BEFORE UPDATE OF
+ id, execution_id, call_id, worker_generation, actor_kind, actor_id, actor_api_token_id,
+ surface,
+ actor_name_snapshot, source_id, tool_id, callable_path_snapshot,
+ source_display_name_snapshot, tool_display_name_snapshot, mode_provenance,
+ source_revision, catalog_revision, tool_revision, binding_revision,
+ credential_revision, arguments_digest, arguments_ciphertext,
+ redacted_arguments_ciphertext,
+ input_schema_ciphertext, output_schema_ciphertext,
+ invocation_snapshot_ciphertext
+ON approvals
+BEGIN
+ SELECT RAISE(ABORT, 'approval snapshot is immutable');
+END;
+
+CREATE TRIGGER approvals_legal_status_transition
+BEFORE UPDATE OF status ON approvals
+WHEN NEW.status <> OLD.status AND NOT (
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'expired', 'canceled'))
+ OR (OLD.status = 'approved' AND NEW.status IN ('executing', 'stale', 'canceled'))
+ OR (OLD.status = 'executing' AND NEW.status IN ('succeeded', 'failed', 'interrupted'))
+)
+BEGIN
+ SELECT RAISE(ABORT, 'illegal approval status transition');
+END;
+
+CREATE TRIGGER approvals_terminal_log_outbox
+AFTER UPDATE OF status ON approvals
+WHEN NEW.status <> OLD.status
+ AND NEW.status IN ('denied', 'expired', 'canceled', 'succeeded', 'failed', 'stale', 'interrupted')
+BEGIN
+ INSERT OR IGNORE INTO approval_log_outbox (
+ request_id, approval_id, actor_api_token_id, surface, source_id, tool_id,
+ path_snapshot, outcome, error_code, created_at
+ ) VALUES (
+ substr(NEW.execution_id, 1, 48) || ':approval:' || NEW.id || ':' || NEW.status,
+ NEW.id,
+ NEW.actor_api_token_id,
+ NEW.surface,
+ NEW.source_id,
+ NEW.tool_id,
+ NEW.callable_path_snapshot,
+ CASE
+ WHEN NEW.status = 'succeeded' THEN 'succeeded'
+ WHEN NEW.status IN ('denied', 'expired', 'canceled') THEN 'denied'
+ ELSE 'failed'
+ END,
+ CASE NEW.status
+ WHEN 'denied' THEN 'approval_denied'
+ WHEN 'expired' THEN 'approval_expired'
+ WHEN 'canceled' THEN 'approval_canceled'
+ WHEN 'failed' THEN coalesce(NEW.failure_code, 'approval_execution_failed')
+ WHEN 'stale' THEN coalesce(NEW.failure_code, 'approval_stale')
+ WHEN 'interrupted' THEN coalesce(NEW.failure_code, 'approval_interrupted')
+ ELSE NULL
+ END,
+ NEW.updated_at
+ );
+END;
+
+CREATE TRIGGER approval_log_outbox_retention
+AFTER INSERT ON approval_log_outbox
+WHEN (SELECT count(*) FROM approval_log_outbox) > 10000
+BEGIN
+ UPDATE approval_log_outbox_state
+ SET dropped_count = dropped_count + (
+ (SELECT count(*) FROM approval_log_outbox) - 10000
+ )
+ WHERE id = 1;
+ DELETE FROM approval_log_outbox
+ WHERE rowid IN (
+ SELECT rowid FROM approval_log_outbox
+ ORDER BY rowid DESC LIMIT -1 OFFSET 10000
+ );
+END;
diff --git a/migrations/20260623040000_gateway_idempotency.sql b/migrations/20260623040000_gateway_idempotency.sql
new file mode 100644
index 000000000..f40b73f8c
--- /dev/null
+++ b/migrations/20260623040000_gateway_idempotency.sql
@@ -0,0 +1,127 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE gateway_idempotency_clock (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ effective_now INTEGER NOT NULL CHECK (effective_now >= 0)
+) STRICT;
+
+INSERT INTO gateway_idempotency_clock (id, effective_now) VALUES (1, 0);
+
+CREATE TABLE gateway_invocation_idempotency (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ id TEXT NOT NULL UNIQUE CHECK (length(id) BETWEEN 1 AND 128),
+ owner_api_token_id TEXT NOT NULL
+ REFERENCES api_tokens(id) ON DELETE CASCADE,
+ key_digest BLOB NOT NULL CHECK (length(key_digest) = 32),
+ request_digest BLOB NOT NULL CHECK (length(request_digest) = 32),
+ state TEXT NOT NULL CHECK (
+ state IN ('reserved', 'executing', 'completed', 'indeterminate')
+ ),
+ approval_id TEXT REFERENCES approvals(id) ON DELETE SET NULL,
+ approval_correlation_id TEXT CHECK (
+ approval_correlation_id IS NULL OR length(approval_correlation_id) BETWEEN 1 AND 128
+ ),
+ response_kind TEXT CHECK (
+ response_kind IS NULL OR response_kind IN ('tool', 'approval')
+ ),
+ response_ciphertext BLOB CHECK (
+ response_ciphertext IS NULL
+ OR length(response_ciphertext) BETWEEN 42 AND 12582954
+ ),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ completed_at INTEGER,
+ expires_at INTEGER,
+ UNIQUE (owner_api_token_id, key_digest),
+ CHECK (
+ (state IN ('reserved', 'executing')
+ AND response_kind IS NULL
+ AND response_ciphertext IS NULL
+ AND completed_at IS NULL
+ AND expires_at IS NULL)
+ OR (state = 'completed'
+ AND response_kind IS NOT NULL
+ AND response_ciphertext IS NOT NULL
+ AND completed_at IS NOT NULL
+ AND expires_at > completed_at)
+ OR (state = 'indeterminate'
+ AND response_kind IS NULL
+ AND response_ciphertext IS NULL
+ AND completed_at IS NOT NULL
+ AND expires_at > completed_at)
+ )
+) STRICT;
+
+CREATE INDEX gateway_invocation_idempotency_expiry_idx
+ ON gateway_invocation_idempotency(expires_at)
+ WHERE expires_at IS NOT NULL;
+
+CREATE INDEX gateway_invocation_idempotency_owner_idx
+ ON gateway_invocation_idempotency(owner_api_token_id, sequence DESC);
+
+CREATE INDEX gateway_invocation_idempotency_approval_correlation_idx
+ ON gateway_invocation_idempotency(approval_correlation_id)
+ WHERE approval_correlation_id IS NOT NULL;
+
+CREATE TRIGGER gateway_idempotency_immutable_binding
+BEFORE UPDATE OF id, owner_api_token_id, key_digest, request_digest, created_at
+ON gateway_invocation_idempotency
+BEGIN
+ SELECT RAISE(ABORT, 'gateway idempotency binding is immutable');
+END;
+
+CREATE TRIGGER gateway_idempotency_legal_transition
+BEFORE UPDATE OF state ON gateway_invocation_idempotency
+WHEN NEW.state <> OLD.state AND NOT (
+ (OLD.state = 'reserved' AND NEW.state IN ('executing', 'completed', 'indeterminate'))
+ OR (OLD.state = 'executing' AND NEW.state IN ('completed', 'indeterminate'))
+)
+BEGIN
+ SELECT RAISE(ABORT, 'illegal gateway idempotency state transition');
+END;
+
+CREATE TRIGGER gateway_idempotency_immutable_terminal
+BEFORE UPDATE OF response_kind, response_ciphertext, completed_at, expires_at
+ON gateway_invocation_idempotency
+WHEN OLD.state IN ('completed', 'indeterminate')
+BEGIN
+ SELECT RAISE(ABORT, 'terminal gateway idempotency response is immutable');
+END;
+
+CREATE TRIGGER gateway_idempotency_capture_approval_correlation_insert
+AFTER INSERT ON gateway_invocation_idempotency
+WHEN NEW.approval_id IS NOT NULL AND NEW.approval_correlation_id IS NULL
+BEGIN
+ UPDATE gateway_invocation_idempotency
+ SET approval_correlation_id = NEW.approval_id
+ WHERE id = NEW.id;
+END;
+
+CREATE TRIGGER gateway_idempotency_capture_approval_correlation_update
+AFTER UPDATE OF approval_id ON gateway_invocation_idempotency
+WHEN OLD.approval_id IS NULL AND NEW.approval_id IS NOT NULL
+BEGIN
+ UPDATE gateway_invocation_idempotency
+ SET approval_correlation_id = NEW.approval_id
+ WHERE id = NEW.id;
+END;
+
+CREATE TRIGGER gateway_idempotency_approval_correlation_transition
+BEFORE UPDATE OF approval_correlation_id ON gateway_invocation_idempotency
+WHEN NOT (
+ OLD.approval_correlation_id IS NULL
+ AND NEW.approval_correlation_id = NEW.approval_id
+ AND NEW.approval_correlation_id IS NOT NULL
+)
+BEGIN
+ SELECT RAISE(ABORT, 'gateway approval correlation transition is invalid');
+END;
+
+CREATE TRIGGER gateway_idempotency_approval_reference_transition
+BEFORE UPDATE OF approval_id ON gateway_invocation_idempotency
+WHEN OLD.approval_id IS NOT NULL
+ AND NEW.approval_id IS NOT NULL
+ AND NEW.approval_id <> OLD.approval_id
+BEGIN
+ SELECT RAISE(ABORT, 'gateway approval reference is immutable');
+END;
diff --git a/migrations/20260623050000_approval_correlations.sql b/migrations/20260623050000_approval_correlations.sql
new file mode 100644
index 000000000..e06cdc610
--- /dev/null
+++ b/migrations/20260623050000_approval_correlations.sql
@@ -0,0 +1,131 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE approval_correlations (
+ execution_id TEXT NOT NULL CHECK (length(execution_id) BETWEEN 1 AND 128),
+ call_id TEXT NOT NULL CHECK (length(call_id) BETWEEN 1 AND 128),
+ approval_id TEXT NOT NULL UNIQUE CHECK (length(approval_id) BETWEEN 1 AND 128),
+ actor_kind TEXT NOT NULL CHECK (actor_kind IN ('api_token', 'admin', 'system')),
+ actor_id TEXT NOT NULL CHECK (length(actor_id) BETWEEN 1 AND 128),
+ surface TEXT NOT NULL CHECK (surface IN ('gateway', 'cli', 'mcp')),
+ worker_generation INTEGER NOT NULL CHECK (worker_generation >= 0),
+ callable_path TEXT NOT NULL CHECK (length(callable_path) BETWEEN 1 AND 512),
+ arguments_digest BLOB NOT NULL CHECK (length(arguments_digest) = 32),
+ expires_at INTEGER CHECK (expires_at IS NULL OR expires_at >= 0),
+ PRIMARY KEY (actor_kind, actor_id, execution_id, call_id)
+) STRICT;
+
+CREATE INDEX approval_correlations_expiry_idx
+ ON approval_correlations(expires_at)
+ WHERE expires_at IS NOT NULL;
+
+CREATE TABLE approval_correlation_state (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ correlation_count INTEGER NOT NULL CHECK (correlation_count BETWEEN 0 AND 100000)
+) STRICT;
+
+INSERT INTO approval_correlation_state (id, correlation_count) VALUES (1, 0);
+
+INSERT INTO approval_correlations (
+ execution_id, call_id, approval_id, actor_kind, actor_id, surface,
+ worker_generation, callable_path, arguments_digest, expires_at
+)
+SELECT
+ execution_id, call_id, id, actor_kind, actor_id, surface,
+ worker_generation, callable_path_snapshot, arguments_digest,
+ CASE
+ WHEN status IN ('denied', 'expired', 'canceled', 'succeeded', 'failed', 'stale', 'interrupted')
+ THEN completed_at + 86400
+ ELSE NULL
+ END
+FROM approvals;
+
+UPDATE approval_correlation_state
+SET correlation_count = (SELECT count(*) FROM approval_correlations)
+WHERE id = 1;
+
+CREATE TRIGGER approval_correlations_capacity
+BEFORE INSERT ON approval_correlations
+WHEN (SELECT correlation_count FROM approval_correlation_state WHERE id = 1) >= 100000
+BEGIN
+ SELECT RAISE(ABORT, 'approval correlation capacity reached');
+END;
+
+CREATE TRIGGER approval_correlations_count_insert
+AFTER INSERT ON approval_correlations
+BEGIN
+ UPDATE approval_correlation_state
+ SET correlation_count = correlation_count + 1
+ WHERE id = 1;
+END;
+
+CREATE TRIGGER approval_correlations_initial_expiry
+BEFORE INSERT ON approval_correlations
+WHEN NEW.expires_at IS NOT NULL
+BEGIN
+ SELECT RAISE(ABORT, 'new approval correlation expiry must be null');
+END;
+
+CREATE TRIGGER approvals_insert_correlation
+AFTER INSERT ON approvals
+BEGIN
+ INSERT INTO approval_correlations (
+ execution_id, call_id, approval_id, actor_kind, actor_id, surface,
+ worker_generation, callable_path, arguments_digest, expires_at
+ ) VALUES (
+ NEW.execution_id, NEW.call_id, NEW.id, NEW.actor_kind, NEW.actor_id, NEW.surface,
+ NEW.worker_generation, NEW.callable_path_snapshot, NEW.arguments_digest, NULL
+ );
+END;
+
+CREATE TRIGGER approval_correlations_immutable
+BEFORE UPDATE OF
+ execution_id, call_id, approval_id, actor_kind, actor_id, surface,
+ worker_generation, callable_path, arguments_digest
+ON approval_correlations
+BEGIN
+ SELECT RAISE(ABORT, 'approval correlation is immutable');
+END;
+
+CREATE TRIGGER approval_correlations_expiry_transition
+BEFORE UPDATE OF expires_at ON approval_correlations
+WHEN NOT (
+ OLD.expires_at IS NULL
+ AND NEW.expires_at = (
+ SELECT completed_at + 86400
+ FROM approvals
+ WHERE id = OLD.approval_id
+ AND status IN ('denied', 'expired', 'canceled', 'succeeded', 'failed', 'stale', 'interrupted')
+ )
+)
+BEGIN
+ SELECT RAISE(ABORT, 'approval correlation expiry transition is invalid');
+END;
+
+CREATE TRIGGER approvals_terminalize_correlation
+AFTER UPDATE OF status ON approvals
+WHEN NEW.status <> OLD.status
+ AND NEW.status IN ('denied', 'expired', 'canceled', 'succeeded', 'failed', 'stale', 'interrupted')
+BEGIN
+ UPDATE approval_correlations
+ SET expires_at = NEW.completed_at + 86400
+ WHERE approval_id = NEW.id AND expires_at IS NULL;
+END;
+
+CREATE TRIGGER approval_correlations_delete_guard
+BEFORE DELETE ON approval_correlations
+WHEN OLD.expires_at IS NULL
+ OR OLD.expires_at > (SELECT effective_now FROM approval_clock WHERE id = 1)
+BEGIN
+ SELECT RAISE(ABORT, 'unexpired approval correlation cannot be deleted');
+END;
+
+CREATE TRIGGER approval_correlations_count_delete
+AFTER DELETE ON approval_correlations
+BEGIN
+ UPDATE approval_correlation_state
+ SET correlation_count = correlation_count - 1
+ WHERE id = 1;
+ DELETE FROM approvals
+ WHERE id = OLD.approval_id
+ AND status IN ('denied', 'expired', 'canceled', 'succeeded', 'failed', 'stale', 'interrupted');
+END;
diff --git a/migrations/20260623060000_approval_delivery_pins.sql b/migrations/20260623060000_approval_delivery_pins.sql
new file mode 100644
index 000000000..4cefa3a16
--- /dev/null
+++ b/migrations/20260623060000_approval_delivery_pins.sql
@@ -0,0 +1,10 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE approval_delivery_pins (
+ approval_id TEXT PRIMARY KEY NOT NULL
+ REFERENCES approvals(id) ON DELETE RESTRICT,
+ ref_count INTEGER NOT NULL CHECK (ref_count BETWEEN 1 AND 128),
+ snapshot_ciphertext_bytes INTEGER NOT NULL
+ CHECK (snapshot_ciphertext_bytes BETWEEN 1 AND 67108864),
+ created_at INTEGER NOT NULL
+) STRICT;
diff --git a/migrations/20260624000000_oauth.sql b/migrations/20260624000000_oauth.sql
new file mode 100644
index 000000000..f19a4ef5b
--- /dev/null
+++ b/migrations/20260624000000_oauth.sql
@@ -0,0 +1,246 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE oauth_connections (
+ id TEXT PRIMARY KEY NOT NULL CHECK (length(id) BETWEEN 1 AND 128),
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
+ credential_key TEXT NOT NULL CHECK (length(credential_key) BETWEEN 1 AND 128),
+ revision INTEGER NOT NULL CHECK (revision >= 0),
+ current_config_revision INTEGER NOT NULL
+ CHECK (current_config_revision > 0),
+ current_secret_revision INTEGER
+ CHECK (current_secret_revision IS NULL OR current_secret_revision > 0),
+ status TEXT NOT NULL
+ CHECK (status IN ('pending_authorization', 'connecting', 'active', 'reauth_required')),
+ granted_scopes_json TEXT NOT NULL DEFAULT '[]'
+ CHECK (json_valid(granted_scopes_json))
+ CHECK (json_type(granted_scopes_json) = 'array'),
+ has_client_secret INTEGER NOT NULL DEFAULT 0 CHECK (has_client_secret IN (0, 1)),
+ has_refresh_token INTEGER NOT NULL DEFAULT 0 CHECK (has_refresh_token IN (0, 1)),
+ access_expires_at INTEGER,
+ authorized_at INTEGER,
+ last_refreshed_at INTEGER,
+ error_code TEXT
+ CHECK (error_code IS NULL OR length(error_code) BETWEEN 1 AND 128),
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ UNIQUE (source_id, credential_key),
+ FOREIGN KEY (id, current_config_revision)
+ REFERENCES oauth_connection_config_revisions(connection_id, revision)
+ DEFERRABLE INITIALLY DEFERRED,
+ FOREIGN KEY (id, current_secret_revision)
+ REFERENCES oauth_connection_secret_revisions(connection_id, revision)
+ DEFERRABLE INITIALLY DEFERRED,
+ CHECK (
+ status <> 'reauth_required' OR error_code IS NOT NULL
+ )
+) STRICT;
+
+CREATE TABLE oauth_connection_config_revisions (
+ connection_id TEXT NOT NULL
+ REFERENCES oauth_connections(id) ON DELETE CASCADE,
+ revision INTEGER NOT NULL CHECK (revision > 0),
+ config_json TEXT NOT NULL
+ CHECK (length(config_json) BETWEEN 2 AND 65536)
+ CHECK (json_valid(config_json))
+ CHECK (json_type(config_json) = 'object'),
+ created_at INTEGER NOT NULL,
+ PRIMARY KEY (connection_id, revision)
+) STRICT;
+
+CREATE TABLE oauth_connection_secret_revisions (
+ connection_id TEXT NOT NULL
+ REFERENCES oauth_connections(id) ON DELETE CASCADE,
+ revision INTEGER NOT NULL CHECK (revision > 0),
+ payload_ciphertext BLOB NOT NULL
+ CHECK (length(payload_ciphertext) BETWEEN 42 AND 262186),
+ access_token_expires_at INTEGER,
+ created_at INTEGER NOT NULL,
+ PRIMARY KEY (connection_id, revision)
+) STRICT;
+
+CREATE TABLE oauth_authorization_transactions (
+ id TEXT PRIMARY KEY NOT NULL CHECK (length(id) BETWEEN 1 AND 128),
+ connection_id TEXT NOT NULL
+ REFERENCES oauth_connections(id) ON DELETE CASCADE,
+ connection_revision INTEGER NOT NULL CHECK (connection_revision >= 0),
+ config_revision INTEGER NOT NULL CHECK (config_revision > 0),
+ base_secret_revision INTEGER
+ CHECK (base_secret_revision IS NULL OR base_secret_revision > 0),
+ state_digest BLOB NOT NULL UNIQUE CHECK (length(state_digest) = 32),
+ admin_session_digest BLOB NOT NULL CHECK (length(admin_session_digest) = 32),
+ pkce_verifier_ciphertext BLOB NOT NULL
+ CHECK (length(pkce_verifier_ciphertext) BETWEEN 42 AND 8192),
+ exchange_claim_digest BLOB CHECK (
+ exchange_claim_digest IS NULL OR length(exchange_claim_digest) = 32
+ ),
+ status TEXT NOT NULL
+ CHECK (status IN ('pending', 'exchanging', 'succeeded', 'failed', 'expired')),
+ result_secret_revision INTEGER
+ CHECK (result_secret_revision IS NULL OR result_secret_revision > 0),
+ error_code TEXT CHECK (error_code IS NULL OR length(error_code) BETWEEN 1 AND 128),
+ created_at INTEGER NOT NULL,
+ expires_at INTEGER NOT NULL CHECK (expires_at > created_at),
+ claimed_at INTEGER,
+ exchange_expires_at INTEGER CHECK (
+ exchange_expires_at IS NULL OR exchange_expires_at > claimed_at
+ ),
+ completed_at INTEGER,
+ CHECK (
+ (status = 'pending'
+ AND exchange_claim_digest IS NULL
+ AND claimed_at IS NULL
+ AND exchange_expires_at IS NULL
+ AND completed_at IS NULL
+ AND result_secret_revision IS NULL
+ AND error_code IS NULL)
+ OR (status = 'exchanging'
+ AND exchange_claim_digest IS NOT NULL
+ AND claimed_at IS NOT NULL
+ AND exchange_expires_at IS NOT NULL
+ AND completed_at IS NULL
+ AND result_secret_revision IS NULL
+ AND error_code IS NULL)
+ OR (status = 'succeeded'
+ AND exchange_claim_digest IS NOT NULL
+ AND claimed_at IS NOT NULL
+ AND completed_at IS NOT NULL
+ AND result_secret_revision IS NOT NULL
+ AND error_code IS NULL)
+ OR (status IN ('failed', 'expired')
+ AND completed_at IS NOT NULL
+ AND result_secret_revision IS NULL
+ AND error_code IS NOT NULL)
+ )
+) STRICT;
+
+CREATE INDEX oauth_authorization_transactions_connection_idx
+ ON oauth_authorization_transactions(connection_id, created_at DESC);
+CREATE INDEX oauth_authorization_transactions_status_expiry_idx
+ ON oauth_authorization_transactions(status, expires_at);
+
+CREATE TABLE oauth_refresh_leases (
+ connection_id TEXT PRIMARY KEY NOT NULL
+ REFERENCES oauth_connections(id) ON DELETE CASCADE,
+ lease_digest BLOB NOT NULL UNIQUE CHECK (length(lease_digest) = 32),
+ base_secret_revision INTEGER NOT NULL CHECK (base_secret_revision > 0),
+ claimed_at INTEGER NOT NULL,
+ expires_at INTEGER NOT NULL CHECK (expires_at > claimed_at)
+) STRICT;
+
+CREATE INDEX oauth_refresh_leases_expiry_idx
+ ON oauth_refresh_leases(expires_at);
+
+CREATE TRIGGER oauth_config_revisions_immutable
+BEFORE UPDATE ON oauth_connection_config_revisions
+BEGIN
+ SELECT RAISE(ABORT, 'OAuth config revisions are immutable');
+END;
+
+CREATE TRIGGER oauth_secret_revisions_immutable
+BEFORE UPDATE ON oauth_connection_secret_revisions
+BEGIN
+ SELECT RAISE(ABORT, 'OAuth secret revisions are immutable');
+END;
+
+CREATE TRIGGER oauth_current_config_revision_delete_guard
+BEFORE DELETE ON oauth_connection_config_revisions
+WHEN EXISTS (
+ SELECT 1 FROM oauth_connections
+ WHERE id = OLD.connection_id AND current_config_revision = OLD.revision
+)
+BEGIN
+ SELECT RAISE(ABORT, 'current OAuth config revision cannot be deleted');
+END;
+
+CREATE TRIGGER oauth_current_secret_revision_delete_guard
+BEFORE DELETE ON oauth_connection_secret_revisions
+WHEN EXISTS (
+ SELECT 1 FROM oauth_connections
+ WHERE id = OLD.connection_id AND current_secret_revision = OLD.revision
+)
+BEGIN
+ SELECT RAISE(ABORT, 'current OAuth secret revision cannot be deleted');
+END;
+
+CREATE TRIGGER oauth_connections_config_pointer_valid
+BEFORE UPDATE OF current_config_revision ON oauth_connections
+WHEN NOT EXISTS (
+ SELECT 1 FROM oauth_connection_config_revisions
+ WHERE connection_id = OLD.id AND revision = NEW.current_config_revision
+)
+BEGIN
+ SELECT RAISE(ABORT, 'OAuth config revision does not exist');
+END;
+
+CREATE TRIGGER oauth_connections_secret_pointer_valid
+BEFORE UPDATE OF current_secret_revision ON oauth_connections
+WHEN NEW.current_secret_revision IS NOT NULL AND NOT EXISTS (
+ SELECT 1 FROM oauth_connection_secret_revisions
+ WHERE connection_id = OLD.id AND revision = NEW.current_secret_revision
+)
+BEGIN
+ SELECT RAISE(ABORT, 'OAuth secret revision does not exist');
+END;
+
+CREATE TRIGGER oauth_authorization_transactions_immutable_binding
+BEFORE UPDATE OF
+ id, connection_id, connection_revision, config_revision, base_secret_revision, state_digest,
+ admin_session_digest, pkce_verifier_ciphertext, created_at, expires_at
+ON oauth_authorization_transactions
+BEGIN
+ SELECT RAISE(ABORT, 'OAuth authorization transaction binding is immutable');
+END;
+
+CREATE TRIGGER oauth_authorization_transactions_legal_transition
+BEFORE UPDATE OF status ON oauth_authorization_transactions
+WHEN NEW.status <> OLD.status AND NOT (
+ (OLD.status = 'pending' AND NEW.status IN ('exchanging', 'failed', 'expired'))
+ OR (OLD.status = 'exchanging' AND NEW.status IN ('succeeded', 'failed'))
+)
+BEGIN
+ SELECT RAISE(ABORT, 'illegal OAuth authorization transaction transition');
+END;
+
+CREATE TRIGGER oauth_authorization_transactions_claim_transition
+BEFORE UPDATE OF exchange_claim_digest, claimed_at, exchange_expires_at
+ON oauth_authorization_transactions
+WHEN NOT (
+ OLD.status = 'pending'
+ AND NEW.status = 'exchanging'
+ AND OLD.exchange_claim_digest IS NULL
+ AND NEW.exchange_claim_digest IS NOT NULL
+ AND OLD.claimed_at IS NULL
+ AND NEW.claimed_at IS NOT NULL
+ AND OLD.exchange_expires_at IS NULL
+ AND NEW.exchange_expires_at > NEW.claimed_at
+)
+BEGIN
+ SELECT RAISE(ABORT, 'invalid OAuth authorization exchange claim');
+END;
+
+CREATE TRIGGER oauth_authorization_transactions_capacity
+BEFORE INSERT ON oauth_authorization_transactions
+WHEN (
+ SELECT count(*) FROM oauth_authorization_transactions
+ WHERE connection_id = NEW.connection_id AND status IN ('pending', 'exchanging')
+) >= 128
+BEGIN
+ SELECT RAISE(ABORT, 'OAuth authorization transaction capacity reached');
+END;
+
+CREATE TRIGGER oauth_authorization_transactions_terminal_retention
+AFTER UPDATE OF status ON oauth_authorization_transactions
+WHEN NEW.status <> OLD.status
+ AND NEW.status IN ('succeeded', 'failed', 'expired')
+ AND (
+ SELECT count(*) FROM oauth_authorization_transactions
+ WHERE status IN ('succeeded', 'failed', 'expired')
+ ) > 10000
+BEGIN
+ DELETE FROM oauth_authorization_transactions
+ WHERE rowid IN (
+ SELECT rowid FROM oauth_authorization_transactions
+ WHERE status IN ('succeeded', 'failed', 'expired')
+ ORDER BY rowid DESC LIMIT -1 OFFSET 10000
+ );
+END;
diff --git a/migrations/20260624010000_source_creation_idempotency.sql b/migrations/20260624010000_source_creation_idempotency.sql
new file mode 100644
index 000000000..d8a46d2b7
--- /dev/null
+++ b/migrations/20260624010000_source_creation_idempotency.sql
@@ -0,0 +1,83 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE source_creation_idempotency_clock (
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
+ effective_now INTEGER NOT NULL CHECK (effective_now >= 0)
+) STRICT;
+
+INSERT INTO source_creation_idempotency_clock (id, effective_now) VALUES (1, 0);
+
+CREATE TABLE source_creation_idempotency (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ id TEXT NOT NULL UNIQUE CHECK (length(id) BETWEEN 1 AND 128),
+ admin_id INTEGER NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
+ route TEXT NOT NULL CHECK (length(route) BETWEEN 1 AND 200),
+ key_digest BLOB NOT NULL CHECK (length(key_digest) = 32),
+ request_digest BLOB CHECK (
+ request_digest IS NULL OR length(request_digest) = 32
+ ),
+ state TEXT NOT NULL CHECK (
+ state IN ('reserved', 'completed', 'failed', 'abandoned', 'interrupted')
+ ),
+ response_ciphertext BLOB CHECK (
+ response_ciphertext IS NULL
+ OR length(response_ciphertext) BETWEEN 42 AND 25165824
+ ),
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
+ updated_at INTEGER NOT NULL CHECK (updated_at >= created_at),
+ completed_at INTEGER,
+ expires_at INTEGER,
+ purge_at INTEGER,
+ UNIQUE (admin_id, route, key_digest),
+ CHECK (
+ (state = 'reserved'
+ AND request_digest IS NOT NULL
+ AND response_ciphertext IS NULL
+ AND completed_at IS NULL
+ AND expires_at IS NULL
+ AND purge_at IS NULL)
+ OR (state IN ('completed', 'failed')
+ AND request_digest IS NOT NULL
+ AND response_ciphertext IS NOT NULL
+ AND completed_at IS NOT NULL
+ AND expires_at > completed_at
+ AND purge_at > expires_at)
+ OR (state IN ('abandoned', 'interrupted')
+ AND response_ciphertext IS NULL
+ AND completed_at IS NOT NULL
+ AND expires_at > completed_at
+ AND purge_at > expires_at)
+ )
+) STRICT;
+
+CREATE INDEX source_creation_idempotency_expiry_idx
+ ON source_creation_idempotency(purge_at)
+ WHERE purge_at IS NOT NULL;
+
+CREATE INDEX source_creation_idempotency_admin_route_idx
+ ON source_creation_idempotency(admin_id, route, sequence DESC);
+
+CREATE TRIGGER source_creation_idempotency_immutable_binding
+BEFORE UPDATE OF id, admin_id, route, key_digest, request_digest, created_at
+ON source_creation_idempotency
+BEGIN
+ SELECT RAISE(ABORT, 'source creation idempotency binding is immutable');
+END;
+
+CREATE TRIGGER source_creation_idempotency_legal_transition
+BEFORE UPDATE OF state ON source_creation_idempotency
+WHEN NEW.state <> OLD.state AND NOT (
+ OLD.state = 'reserved'
+ AND NEW.state IN ('completed', 'failed', 'abandoned', 'interrupted')
+)
+BEGIN
+ SELECT RAISE(ABORT, 'illegal source creation idempotency state transition');
+END;
+
+CREATE TRIGGER source_creation_idempotency_immutable_terminal
+BEFORE UPDATE OF response_ciphertext, completed_at, expires_at, purge_at, updated_at
+ON source_creation_idempotency
+WHEN OLD.state IN ('completed', 'failed', 'abandoned', 'interrupted')
+BEGIN
+ SELECT RAISE(ABORT, 'terminal source creation idempotency response is immutable');
+END;
diff --git a/migrations/20260624020000_oauth_refresh_state_fence.sql b/migrations/20260624020000_oauth_refresh_state_fence.sql
new file mode 100644
index 000000000..80a6a9730
--- /dev/null
+++ b/migrations/20260624020000_oauth_refresh_state_fence.sql
@@ -0,0 +1,28 @@
+ALTER TABLE oauth_refresh_leases RENAME TO oauth_refresh_leases_unfenced;
+
+CREATE TABLE oauth_refresh_leases (
+ connection_id TEXT PRIMARY KEY NOT NULL
+ REFERENCES oauth_connections(id) ON DELETE CASCADE,
+ lease_digest BLOB NOT NULL UNIQUE CHECK (length(lease_digest) = 32),
+ base_secret_revision INTEGER NOT NULL CHECK (base_secret_revision > 0),
+ connection_revision INTEGER NOT NULL CHECK (connection_revision >= 0),
+ config_revision INTEGER NOT NULL CHECK (config_revision > 0),
+ claimed_at INTEGER NOT NULL,
+ expires_at INTEGER NOT NULL CHECK (expires_at > claimed_at)
+) STRICT;
+
+INSERT INTO oauth_refresh_leases (
+ connection_id, lease_digest, base_secret_revision,
+ connection_revision, config_revision, claimed_at, expires_at
+)
+SELECT
+ lease.connection_id, lease.lease_digest, lease.base_secret_revision,
+ connection.revision, connection.current_config_revision,
+ lease.claimed_at, lease.expires_at
+FROM oauth_refresh_leases_unfenced lease
+JOIN oauth_connections connection ON connection.id = lease.connection_id;
+
+DROP TABLE oauth_refresh_leases_unfenced;
+
+CREATE INDEX oauth_refresh_leases_expiry_idx
+ ON oauth_refresh_leases(expires_at);
diff --git a/migrations/20260624030000_api_token_creation_idempotency.sql b/migrations/20260624030000_api_token_creation_idempotency.sql
new file mode 100644
index 000000000..7ff342484
--- /dev/null
+++ b/migrations/20260624030000_api_token_creation_idempotency.sql
@@ -0,0 +1,20 @@
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE api_token_creation_idempotency (
+ record_id TEXT PRIMARY KEY NOT NULL CHECK (length(record_id) BETWEEN 1 AND 128),
+ admin_id INTEGER NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
+ key_digest BLOB NOT NULL CHECK (length(key_digest) = 32),
+ request_digest BLOB NOT NULL CHECK (length(request_digest) = 32),
+ token_id TEXT NOT NULL UNIQUE REFERENCES api_tokens(id) ON DELETE RESTRICT,
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
+ UNIQUE (admin_id, key_digest)
+) STRICT;
+
+CREATE INDEX api_token_creation_idempotency_admin_idx
+ ON api_token_creation_idempotency(admin_id, created_at DESC);
+
+CREATE TRIGGER api_token_creation_idempotency_immutable
+BEFORE UPDATE ON api_token_creation_idempotency
+BEGIN
+ SELECT RAISE(ABORT, 'API token creation idempotency records are immutable');
+END;
diff --git a/opencode.json b/opencode.json
index 915fccd69..8f9b80710 100644
--- a/opencode.json
+++ b/opencode.json
@@ -1,19 +1,4 @@
{
"$schema": "https://opencode.ai/config.json",
- "mcp": {
- "executor": {
- "type": "remote",
- "url": "https://executor-local.localhost:1355/mcp",
- "enabled": true,
- "command": [
- "bun",
- "run",
- "dev:cli",
- "mcp",
- "--scope",
- "/Users/rhyssullivan/src/executor/apps/local"
- ],
- "environment": {}
- }
- }
+ "mcp": {}
}
diff --git a/package.json b/package.json
index 8f40bea46..3ae315110 100644
--- a/package.json
+++ b/package.json
@@ -11,77 +11,73 @@
"executor",
"local-first"
],
- "homepage": "https://github.com/RhysSullivan/executor",
+ "homepage": "https://github.com/davis7dotsh/executor-fork-testing",
"bugs": {
- "url": "https://github.com/RhysSullivan/executor/issues"
+ "url": "https://github.com/davis7dotsh/executor-fork-testing/issues"
},
"license": "MIT",
"repository": {
"type": "git",
- "url": "git+https://github.com/RhysSullivan/executor.git"
+ "url": "git+https://github.com/davis7dotsh/executor-fork-testing.git"
},
"workspaces": [
"packages/*/*",
"packages/react",
"packages/app",
"apps/*",
+ "legacy/*",
"examples/*",
- "e2e"
+ "e2e",
+ "web"
],
"type": "module",
"scripts": {
"bootstrap": "bun run scripts/bootstrap.ts",
"reap": "bun run scripts/reap-dev-servers.ts",
- "dev": "turbo run dev --filter='!@executor-js/desktop' --filter='!@executor-js/cloud'",
- "dev:desktop": "turbo run dev",
- "dev:cli": "EXECUTOR_DEV=1 EXECUTOR_DATA_DIR=${EXECUTOR_DATA_DIR:-apps/local/.executor-dev} bun run apps/cli/src/main.ts",
- "test": "turbo run test --filter=!@executor-js/e2e",
- "test:e2e": "bun run --cwd e2e test",
+ "dev": "turbo run dev --filter='!executor' --filter='!@executor-js/local' --filter='!@executor-js/host-cloudflare' --filter='!@executor-js/host-selfhost' --filter='!@executor-js/cloud' --filter='!@executor-js/desktop'",
+ "test": "turbo run test --filter='!@executor-js/e2e' --filter='!executor' --filter='!@executor-js/local' --filter='!@executor-js/host-cloudflare' --filter='!@executor-js/host-selfhost' --filter='!@executor-js/cloud' --filter='!@executor-js/desktop'",
+ "test:e2e": "bun run scripts/run-local-e2e.ts",
+ "test:release:installer": "bash scripts/test-install-release-archive.sh",
+ "test:release:systemd": "bash scripts/test-install-systemd-master-key.sh",
+ "test:release:shell": "bash -n scripts/install.sh scripts/install-systemd.sh scripts/install-launchd.sh scripts/lib/install-systemd-master-key.sh scripts/test-install-release-archive.sh scripts/smoke-release-server.sh scripts/smoke-release-container.sh scripts/verify-apple-toolchain.sh packaging/launchd/bounded-log.sh",
+ "test:release:static": "vitest run tests/release-workflows.test.ts tests/release-dockerfile.test.ts",
"test:release:bootstrap": "vitest run tests/release-bootstrap-smoke.test.ts",
"build:packages": "bun run --filter='@executor-js/fumadb' build && bun run --filter='@executor-js/codemode-core' build && bun run --filter='@executor-js/runtime-quickjs' build && bun run --filter='@executor-js/sdk' build && bun run --filter='@executor-js/config' build && bun run --filter='@executor-js/execution' build && bun run --filter='@executor-js/cli' build && bun run --filter='@executor-js/plugin-*' build",
- "typecheck": "turbo run typecheck",
- "typecheck:slow": "turbo run typecheck:slow",
+ "typecheck": "turbo run typecheck --filter='!executor' --filter='!@executor-js/local' --filter='!@executor-js/host-cloudflare' --filter='!@executor-js/host-selfhost' --filter='!@executor-js/cloud' --filter='!@executor-js/desktop'",
+ "typecheck:slow": "turbo run typecheck:slow --filter='!executor' --filter='!@executor-js/local' --filter='!@executor-js/host-cloudflare' --filter='!@executor-js/host-selfhost' --filter='!@executor-js/cloud' --filter='!@executor-js/desktop'",
+ "legacy:dev": "turbo run dev --filter='@executor-js/local' --filter='@executor-js/host-cloudflare' --filter='@executor-js/host-selfhost' --filter='@executor-js/cloud' --filter='@executor-js/desktop'",
+ "legacy:dev:cli": "EXECUTOR_DEV=1 EXECUTOR_DATA_DIR=${EXECUTOR_DATA_DIR:-legacy/local/.executor-dev} bun run legacy/cli/src/main.ts",
+ "legacy:test": "turbo run test --filter='executor' --filter='@executor-js/local' --filter='@executor-js/host-cloudflare' --filter='@executor-js/host-selfhost' --filter='@executor-js/cloud' --filter='@executor-js/desktop'",
+ "legacy:test:e2e:cloud": "bun run --cwd e2e test:legacy:cloud",
+ "legacy:test:e2e:desktop": "bun run --cwd e2e test:legacy:desktop",
+ "legacy:typecheck": "turbo run typecheck --filter='executor' --filter='@executor-js/local' --filter='@executor-js/host-cloudflare' --filter='@executor-js/host-selfhost' --filter='@executor-js/cloud' --filter='@executor-js/desktop'",
+ "legacy:typecheck:slow": "bun run --filter='./legacy/*' typecheck:slow",
"ci": "bun run lint && bun run typecheck && bun run test",
- "lint": "oxlint -c .oxlintrc.jsonc . --deny-warnings && bun run lint:changelog-stubs",
- "lint:changelog-stubs": "bun run scripts/check-changelog-stubs.ts",
+ "lint": "oxlint -c .oxlintrc.jsonc . --deny-warnings && bun run lint:docker-workspace-context && bun run lint:warden-targets",
+ "lint:docker-workspace-context": "bun run scripts/check-docker-workspace-context.ts",
+ "lint:warden-targets": "bun run scripts/check-warden-targets.ts",
"lint:fix": "oxlint -c .oxlintrc.jsonc --fix .",
"docs:snippets": "bun run scripts/generate-doc-snippets.ts",
- "docs:smoke:install": "bun run scripts/smoke-docs-install.ts",
"smoke:agent-config": "bun run scripts/agent-config-smoke.ts",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"pull:references": "bun run scripts/pull-references.ts",
- "changeset": "changeset",
- "changeset:version": "changeset version && bun install --lockfile-only && oxfmt .",
- "release:check": "bun run --cwd apps/cli typecheck && bun run test:release:bootstrap && bun run release:publish:dry-run",
- "release:pre:enter": "changeset pre enter beta",
- "release:pre:exit": "changeset pre exit",
- "release:beta:start": "changeset pre enter beta",
- "release:beta:stop": "changeset pre exit",
- "release:publish:dry-run": "bun run --cwd apps/cli release:publish:dry-run",
- "release:publish": "bun run --cwd apps/cli release:publish",
- "release:publish:packages": "bun run scripts/publish-packages.ts",
- "release:publish:packages:dry-run": "bun run scripts/publish-packages.ts --dry-run",
- "release:publish:packages:prepare": "bun run scripts/publish-packages.ts --prepare-only",
- "release:smoke:packages": "bun run scripts/smoke-test-packed.ts",
"clean": "bun run scripts/clean.ts",
"prepare": "effect-language-service patch && effect-tsgo patch && bun run --cwd packages/core/vite-plugin build:bundle && bun run --cwd packages/react build"
},
"dependencies": {},
"devDependencies": {
- "@changesets/changelog-github": "^0.7.0",
- "@changesets/cli": "^2.30.0",
"@effect/language-service": "^0.85.1",
"@effect/tsgo": "^0.5.2",
"@effect/vitest": "catalog:",
"@typescript/native-preview": "^7.0.0-dev.20260410.1",
- "@vitest/expect": "catalog:",
- "@vitest/mocker": "catalog:",
- "@vitest/pretty-format": "catalog:",
- "@vitest/runner": "catalog:",
- "@vitest/snapshot": "catalog:",
- "@vitest/spy": "catalog:",
- "@vitest/utils": "catalog:",
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"atmn": "^1.1.8",
"effect": "catalog:",
"knip": "^6.3.0",
@@ -89,7 +85,7 @@
"oxlint": "^1.56.0",
"turbo": "^2.5.6",
"typescript": "^5.9.3",
- "vitest": "catalog:"
+ "vitest": "4.1.9"
},
"packageManager": "bun@1.3.11",
"catalog": {
diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts
index fcff0014b..0a1fdba4c 100644
--- a/packages/app/vite.config.ts
+++ b/packages/app/vite.config.ts
@@ -3,10 +3,12 @@ import { defineConfig } from "vite";
import appPlugin from "./vite";
// Standalone Vite config for iterating on the SPA in isolation, without
-// the apps/local API middleware or Electron sidecar. API/MCP calls fail
+// the legacy/local API middleware or Electron sidecar. API/MCP calls fail
// here unless a separate server is running.
-const LOCAL_CONFIG = fileURLToPath(new URL("../../apps/local/executor.config.ts", import.meta.url));
-const LOCAL_JSONC = fileURLToPath(new URL("../../apps/local/executor.jsonc", import.meta.url));
+const LOCAL_CONFIG = fileURLToPath(
+ new URL("../../legacy/local/executor.config.ts", import.meta.url),
+);
+const LOCAL_JSONC = fileURLToPath(new URL("../../legacy/local/executor.jsonc", import.meta.url));
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
export default defineConfig({
diff --git a/packages/app/vite.ts b/packages/app/vite.ts
index 51e27e890..4d1beaff0 100644
--- a/packages/app/vite.ts
+++ b/packages/app/vite.ts
@@ -14,7 +14,7 @@ interface AppPluginOptions {
* Absolute path to the `executor.config.ts` whose plugin list should
* feed `virtual:executor/plugins-client`. The Vite root for this app
* is `packages/app/` which has no executor.config, so consumers must
- * point this at their own (typically `apps/local/executor.config.ts`).
+ * point this at their own (typically `legacy/local/executor.config.ts`).
*
* If omitted, no plugin UIs get bundled — the renderer still works
* for built-in pages but Sources / Connections / etc. plugin pages
@@ -28,7 +28,7 @@ interface AppPluginOptions {
/**
* Vite plugin bundle for the executor React app.
*
- * Layered into apps/local's vite config (web build) and apps/desktop's
+ * Layered into legacy/local's vite config (web build) and legacy/desktop's
* electron.vite renderer config. Consumers must pass `executorConfigPath`
* so plugin client bundles get included — see option docs above.
*
diff --git a/packages/core/api/src/observability.ts b/packages/core/api/src/observability.ts
index 1bda74d5a..74999cc55 100644
--- a/packages/core/api/src/observability.ts
+++ b/packages/core/api/src/observability.ts
@@ -25,7 +25,7 @@
// once; catches any cause that slipped past the typed channel and
// produces the same `InternalError({ traceId })` shape.
//
-// Distinct from `apps/cloud/src/services/telemetry.ts` — that's the
+// Distinct from `legacy/cloud/src/services/telemetry.ts`, that's the
// OTEL bridge wiring spans to Axiom; this is exception capture in the
// Sentry sense.
// ---------------------------------------------------------------------------
diff --git a/packages/core/api/src/server/request-scoped.ts b/packages/core/api/src/server/request-scoped.ts
index 1436d851b..f01ac1cdb 100644
--- a/packages/core/api/src/server/request-scoped.ts
+++ b/packages/core/api/src/server/request-scoped.ts
@@ -25,7 +25,7 @@
// shared across two request handlers, which the runtime forbids). A
// per-request MemoMap scopes memoization to a single request fiber.
//
-// See `apps/cloud/src/api.request-scope.node.test.ts` for the regression
+// See `legacy/cloud/src/api.request-scope.node.test.ts` for the regression
// coverage that pins this rule down (sequential AND concurrent cases).
// ---------------------------------------------------------------------------
diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json
index d62995c70..b7dd916cb 100644
--- a/packages/core/cli/package.json
+++ b/packages/core/cli/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/cli",
"version": "0.2.26",
+ "private": true,
"description": "CLI for the executor SDK — schema generation, migrations",
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/core/cli",
"bugs": {
diff --git a/packages/core/config/package.json b/packages/core/config/package.json
index 738cf8159..d3887cb84 100644
--- a/packages/core/config/package.json
+++ b/packages/core/config/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/config",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/core/config",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/core/execution/package.json b/packages/core/execution/package.json
index b0bf17e25..b91220ce5 100644
--- a/packages/core/execution/package.json
+++ b/packages/core/execution/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/execution",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/core/execution",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/core/execution/src/promise.ts b/packages/core/execution/src/promise.ts
index cf57304a5..5995825f8 100644
--- a/packages/core/execution/src/promise.ts
+++ b/packages/core/execution/src/promise.ts
@@ -135,7 +135,7 @@ const wrapPromiseExecutor = (pe: PromiseExecutor): EffectExecutor => {
/**
* Promise-wrap an Effect-native `ExecutionEngine` (from `./engine`).
* Exposed separately so callers that already hold an Effect engine
- * (apps/cloud's execution-stack composes both) can convert it for hosts
+ * (legacy/cloud's execution-stack composes both) can convert it for hosts
* that need the Promise surface (host-mcp).
*/
export const toPromiseExecutionEngine = (
diff --git a/packages/core/fumadb/package.json b/packages/core/fumadb/package.json
index ccd101adf..d6152f961 100644
--- a/packages/core/fumadb/package.json
+++ b/packages/core/fumadb/package.json
@@ -2,6 +2,7 @@
"name": "@executor-js/fumadb",
"description": "A library for interacting with different databases for your package.",
"version": "1.5.7",
+ "private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/RhysSullivan/executor.git",
diff --git a/packages/core/fumadb/src/adapters/drizzle/runtime-ensure.test.ts b/packages/core/fumadb/src/adapters/drizzle/runtime-ensure.test.ts
index e49552d7e..5c0dff20b 100644
--- a/packages/core/fumadb/src/adapters/drizzle/runtime-ensure.test.ts
+++ b/packages/core/fumadb/src/adapters/drizzle/runtime-ensure.test.ts
@@ -38,8 +38,8 @@ const columnNames = async (client: ReturnType): Promise String(row["name"]));
};
-// This is the boot bring-up that backs apps/local, apps/host-selfhost, and
-// apps/host-cloudflare: `CREATE TABLE IF NOT EXISTS` never alters an existing
+// This is the boot bring-up that backs legacy/local, legacy/host-selfhost, and
+// legacy/host-cloudflare: `CREATE TABLE IF NOT EXISTS` never alters an existing
// table, so without column evolution a file from an earlier baseline would
// 500 on the first query against a new column. (Cloud Postgres gets the same
// columns from a generated drizzle migration instead.)
diff --git a/packages/core/sdk/package.json b/packages/core/sdk/package.json
index 2d367417e..29bc0a674 100644
--- a/packages/core/sdk/package.json
+++ b/packages/core/sdk/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/sdk",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/core/sdk",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/core/sdk/src/plugin-storage.test.ts b/packages/core/sdk/src/plugin-storage.test.ts
index a547d06a5..95c7c0b71 100644
--- a/packages/core/sdk/src/plugin-storage.test.ts
+++ b/packages/core/sdk/src/plugin-storage.test.ts
@@ -222,29 +222,32 @@ describe("plugin storage collections", () => {
}),
);
- it.effect("bulk puts large plugin storage row sets in bounded batches", () =>
- Effect.gen(function* () {
- const executor = yield* makeTestExecutor({
- backend: "sqlite",
- plugins: [executionHistoryPlugin] as const,
- });
- const rows = Array.from({ length: 7_000 }, (_, index) => ({
- key: `large-call-${String(index).padStart(5, "0")}`,
- data: call({
- runId: "run-large-bulk",
- toolId: index % 2 === 0 ? "browser" : "shell",
- status: "ok",
- startedAt: new Date(Date.UTC(2026, 4, 29, 12, 0, index)).toISOString(),
- }),
- }));
+ it.effect(
+ "bulk puts large plugin storage row sets in bounded batches",
+ () =>
+ Effect.gen(function* () {
+ const executor = yield* makeTestExecutor({
+ backend: "sqlite",
+ plugins: [executionHistoryPlugin] as const,
+ });
+ const rows = Array.from({ length: 7_000 }, (_, index) => ({
+ key: `large-call-${String(index).padStart(5, "0")}`,
+ data: call({
+ runId: "run-large-bulk",
+ toolId: index % 2 === 0 ? "browser" : "shell",
+ status: "ok",
+ startedAt: new Date(Date.UTC(2026, 4, 29, 12, 0, index)).toISOString(),
+ }),
+ }));
- yield* executor.executionHistory.recordMany("org", rows);
+ yield* executor.executionHistory.recordMany("org", rows);
- const count = yield* executor.executionHistory.count({
- where: { runId: "run-large-bulk" },
- });
- expect(count).toBe(rows.length);
- }),
+ const count = yield* executor.executionHistory.count({
+ where: { runId: "run-large-bulk" },
+ });
+ expect(count).toBe(rows.length);
+ }),
+ 15_000,
);
it.effect("user rows shadow org rows on read; both share one plugin_storage table", () =>
diff --git a/packages/core/vite-plugin/package.json b/packages/core/vite-plugin/package.json
index 3b138ce30..b9bacda90 100644
--- a/packages/core/vite-plugin/package.json
+++ b/packages/core/vite-plugin/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/vite-plugin",
"version": "0.0.37",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/core/vite-plugin",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/core/vite-plugin/src/index.ts b/packages/core/vite-plugin/src/index.ts
index 4b89e87dc..77a551a3d 100644
--- a/packages/core/vite-plugin/src/index.ts
+++ b/packages/core/vite-plugin/src/index.ts
@@ -187,7 +187,7 @@ export default function executorVitePlugin(options: ExecutorVitePluginOptions =
const ident = `__executor_plugin_${exportExpressions.length}`;
// Emit the absolute file path rather than `${pkg}/client`. Vite
// resolves bare specifiers from its `root`, which for hosts like
- // apps/local is `packages/app/` — a location that doesn't see
+ // legacy/local is `packages/app/`, a location that doesn't see
// plugin packages installed under the consumer's node_modules.
// Resolving here (from the consumer's executor.config dir) and
// emitting the absolute path bypasses Vite's node_modules walk
diff --git a/packages/hosts/mcp/src/browser-approval-store.ts b/packages/hosts/mcp/src/browser-approval-store.ts
index 812b7c49e..825acac0f 100644
--- a/packages/hosts/mcp/src/browser-approval-store.ts
+++ b/packages/hosts/mcp/src/browser-approval-store.ts
@@ -1,6 +1,6 @@
// ---------------------------------------------------------------------------
// In-process browser-approval store — the single-process equivalent of the
-// Durable Object's persisted approval responses (apps/cloud, host-cloudflare).
+// Durable Object's persisted approval responses (legacy/cloud, host-cloudflare).
//
// It is the bridge between the two halves of a browser approval:
// - the MCP `resume` tool long-polls `store.waitForResponse(executionId)`,
diff --git a/packages/hosts/mcp/src/browser-approval.ts b/packages/hosts/mcp/src/browser-approval.ts
index fa6e34829..87ae439b0 100644
--- a/packages/hosts/mcp/src/browser-approval.ts
+++ b/packages/hosts/mcp/src/browser-approval.ts
@@ -9,7 +9,7 @@
// host's `BrowserApprovalStore` — consumes it.
//
// Three hosts implement that flow over two transports: the in-process handler
-// (apps/local, host-selfhost) and the Durable Object (cloud, host-cloudflare).
+// (legacy/local, host-selfhost) and the Durable Object (cloud, host-cloudflare).
// The wire-shape pieces are identical across all of them, so they live here
// once: how the mode is read off the request, how the approval URL is built,
// the resume-payload schema, and the acknowledgement text/structured content.
diff --git a/packages/hosts/mcp/src/stdio-integration.test.ts b/packages/hosts/mcp/src/stdio-integration.test.ts
index d66f9893f..1c039021d 100644
--- a/packages/hosts/mcp/src/stdio-integration.test.ts
+++ b/packages/hosts/mcp/src/stdio-integration.test.ts
@@ -7,8 +7,8 @@ import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const repoRoot = resolve(import.meta.dirname, "../../../..");
-const cliEntry = resolve(repoRoot, "apps/cli/src/main.ts");
-const testScope = resolve(repoRoot, "apps/local");
+const cliEntry = resolve(repoRoot, "legacy/cli/src/main.ts");
+const testScope = resolve(repoRoot, "legacy/local");
describe("MCP stdio integration", () => {
it.effect(
diff --git a/packages/kernel/core/package.json b/packages/kernel/core/package.json
index 85bf6b721..fc2517036 100644
--- a/packages/kernel/core/package.json
+++ b/packages/kernel/core/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/codemode-core",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/kernel/core",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/kernel/runtime-quickjs/package.json b/packages/kernel/runtime-quickjs/package.json
index 79cb1562e..47cbec5c5 100644
--- a/packages/kernel/runtime-quickjs/package.json
+++ b/packages/kernel/runtime-quickjs/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/runtime-quickjs",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/kernel/runtime-quickjs",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/desktop-settings/package.json b/packages/plugins/desktop-settings/package.json
index 2e48df7c3..bc360afec 100644
--- a/packages/plugins/desktop-settings/package.json
+++ b/packages/plugins/desktop-settings/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-desktop-settings",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/desktop-settings",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/desktop-settings/src/client.tsx b/packages/plugins/desktop-settings/src/client.tsx
index 3241cc4ac..ecf361aaa 100644
--- a/packages/plugins/desktop-settings/src/client.tsx
+++ b/packages/plugins/desktop-settings/src/client.tsx
@@ -5,9 +5,9 @@
* A single page mounted at `/plugins/desktop-settings/` that lets the user
* inspect and configure the Electron sidecar's server connection. Talks to
* the main process via `window.executor.*` (exposed by
- * `apps/desktop/src/preload`).
+ * `legacy/desktop/src/preload`).
*
- * The plugin is bundled into apps/local's renderer too (because executor
+ * The plugin is bundled into legacy/local's renderer too (because executor
* web + desktop share the same client bundle pipeline), but the page
* only registers a nav entry when `window.executor` is present at
* module-init time — so the web UI doesn't show a non-functional link.
diff --git a/packages/plugins/example/package.json b/packages/plugins/example/package.json
index 0c4aadfeb..eec827a56 100644
--- a/packages/plugins/example/package.json
+++ b/packages/plugins/example/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-example",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/example",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/file-secrets/package.json b/packages/plugins/file-secrets/package.json
index 0e1d913a9..ff99aa725 100644
--- a/packages/plugins/file-secrets/package.json
+++ b/packages/plugins/file-secrets/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-file-secrets",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/file-secrets",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/google/package.json b/packages/plugins/google/package.json
index 06d70811b..56deef143 100644
--- a/packages/plugins/google/package.json
+++ b/packages/plugins/google/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-google",
"version": "1.5.19",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/google",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/graphql/package.json b/packages/plugins/graphql/package.json
index e9ef0f9ea..c98196229 100644
--- a/packages/plugins/graphql/package.json
+++ b/packages/plugins/graphql/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-graphql",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/graphql",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/keychain/package.json b/packages/plugins/keychain/package.json
index 0328b672f..8ef95a99f 100644
--- a/packages/plugins/keychain/package.json
+++ b/packages/plugins/keychain/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-keychain",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/keychain",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/keychain/src/keyring.ts b/packages/plugins/keychain/src/keyring.ts
index aeb8ad855..c5a1c5dbc 100644
--- a/packages/plugins/keychain/src/keyring.ts
+++ b/packages/plugins/keychain/src/keyring.ts
@@ -39,8 +39,8 @@ let entryCtorPromise: Promise | null = null;
// In compiled bun binaries (`bun build --compile`) `.node` modules aren't
// included in bunfs and there's no node_modules at runtime, so
// @napi-rs/keyring's loader can't find its platform-specific binding.
-// `apps/cli/src/build.ts` copies the .node next to the executor and
-// `apps/cli/src/main.ts` exports its absolute path here. We load it
+// `legacy/cli/src/build.ts` copies the .node next to the executor and
+// `legacy/cli/src/main.ts` exports its absolute path here. We load it
// directly because @napi-rs/keyring@1.2.0's NAPI_RS_NATIVE_LIBRARY_PATH
// branch is buggy (assigns to a local that gets overwritten before return).
const loadEntryCtor = async (): Promise => {
diff --git a/packages/plugins/mcp/package.json b/packages/plugins/mcp/package.json
index 0f601c7a2..55d7266a7 100644
--- a/packages/plugins/mcp/package.json
+++ b/packages/plugins/mcp/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-mcp",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/mcp",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/mcp/src/sdk/elicitation.test.ts b/packages/plugins/mcp/src/sdk/elicitation.test.ts
index 9c0a6e50a..e9ec5a380 100644
--- a/packages/plugins/mcp/src/sdk/elicitation.test.ts
+++ b/packages/plugins/mcp/src/sdk/elicitation.test.ts
@@ -6,7 +6,6 @@ import type { JsonSchemaType } from "@modelcontextprotocol/sdk/validation/types"
import {
AuthTemplateSlug,
ConnectionName,
- createExecutor,
FormElicitation,
ElicitationResponse,
IntegrationSlug,
@@ -16,7 +15,7 @@ import {
type Tool,
} from "@executor-js/sdk";
import {
- makeTestConfig,
+ makeTestWorkspaceHarness,
memoryCredentialsPlugin,
typeCheckOutputTypeScript,
} from "@executor-js/sdk/testing";
@@ -56,10 +55,10 @@ const TEMPLATE = AuthTemplateSlug.make("none");
// ---------------------------------------------------------------------------
const makeTestExecutor = (serverUrl: string) =>
- createExecutor(
- makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),
- ).pipe(
- Effect.tap((executor) =>
+ makeTestWorkspaceHarness({
+ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const,
+ }).pipe(
+ Effect.tap(({ executor }) =>
Effect.gen(function* () {
yield* executor.mcp.addServer({
name: "test-mcp",
@@ -75,6 +74,7 @@ const makeTestExecutor = (serverUrl: string) =>
});
}),
),
+ Effect.map(({ executor }) => executor),
);
const findTool = (tools: readonly Tool[], name: string): Tool =>
@@ -304,7 +304,9 @@ describe("MCP elicitation (end-to-end)", () => {
it.effect("addServer preserves the configured display name as the integration description", () =>
Effect.gen(function* () {
const server = yield* serveElicitationTestServer;
- const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));
+ const { executor } = yield* makeTestWorkspaceHarness({
+ plugins: [mcpPlugin()] as const,
+ });
yield* executor.mcp.addServer({
name: "Gmail",
diff --git a/packages/plugins/mcp/src/sdk/multi-placement-auth.test.ts b/packages/plugins/mcp/src/sdk/multi-placement-auth.test.ts
index 4729eb39e..2180027df 100644
--- a/packages/plugins/mcp/src/sdk/multi-placement-auth.test.ts
+++ b/packages/plugins/mcp/src/sdk/multi-placement-auth.test.ts
@@ -12,14 +12,8 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect } from "effect";
-import {
- AuthTemplateSlug,
- ConnectionName,
- IntegrationSlug,
- ToolAddress,
- createExecutor,
-} from "@executor-js/sdk";
-import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing";
+import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ToolAddress } from "@executor-js/sdk";
+import { makeTestExecutor, memoryCredentialsPlugin } from "@executor-js/sdk/testing";
import { mcpPlugin } from "./plugin";
import { variable } from "@executor-js/sdk/http-auth";
@@ -39,9 +33,9 @@ describe("MCP multi-placement auth", () => {
it.effect("one method renders a bearer header AND a team-id query param", () =>
Effect.gen(function* () {
const server = yield* serveRecordingServer;
- const executor = yield* createExecutor(
- makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),
- );
+ const executor = yield* makeTestExecutor({
+ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const,
+ });
yield* executor.mcp.addServer({
name: "Mixed MCP",
@@ -76,7 +70,9 @@ describe("MCP multi-placement auth", () => {
data: { content: [{ type: "text", text: "ok:mixed" }] },
});
- const requests = (yield* server.requests).slice(before);
+ const requests = (yield* server.requests)
+ .slice(before)
+ .filter((request) => request.method === "POST");
expect(requests.length).toBeGreaterThan(0);
// BOTH carriers rendered, each from its own credential input.
expect(requests.every((request) => request.authorization === "Bearer tok_A")).toBe(true);
@@ -87,9 +83,9 @@ describe("MCP multi-placement auth", () => {
it.effect("a query-only method renders ?token= and no Authorization header (ui.sh)", () =>
Effect.gen(function* () {
const server = yield* serveRecordingServer;
- const executor = yield* createExecutor(
- makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),
- );
+ const executor = yield* makeTestExecutor({
+ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const,
+ });
yield* executor.mcp.addServer({
name: "Query MCP",
@@ -115,7 +111,9 @@ describe("MCP multi-placement auth", () => {
);
expect(result).toMatchObject({ ok: true });
- const requests = (yield* server.requests).slice(before);
+ const requests = (yield* server.requests)
+ .slice(before)
+ .filter((request) => request.method === "POST");
expect(requests.length).toBeGreaterThan(0);
expect(requests.every((request) => request.url.includes("token=tok_secret_123"))).toBe(true);
expect(requests.every((request) => request.authorization === undefined)).toBe(true);
@@ -125,9 +123,9 @@ describe("MCP multi-placement auth", () => {
it.effect("a connection binding one method does not leak into another method's shape", () =>
Effect.gen(function* () {
const server = yield* serveRecordingServer;
- const executor = yield* createExecutor(
- makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),
- );
+ const executor = yield* makeTestExecutor({
+ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const,
+ });
// Two declared methods; each connection picks one by template slug.
yield* executor.mcp.addServer({
@@ -169,7 +167,9 @@ describe("MCP multi-placement auth", () => {
{ marker: "h" },
{ onElicitation: "accept-all" },
);
- const headerRequests = (yield* server.requests).slice(beforeHeader);
+ const headerRequests = (yield* server.requests)
+ .slice(beforeHeader)
+ .filter((request) => request.method === "POST");
expect(headerRequests.every((r) => r.authorization === "Bearer header-secret")).toBe(true);
expect(headerRequests.every((r) => !r.url.includes("auth_token="))).toBe(true);
@@ -179,7 +179,9 @@ describe("MCP multi-placement auth", () => {
{ marker: "q" },
{ onElicitation: "accept-all" },
);
- const queryRequests = (yield* server.requests).slice(beforeQuery);
+ const queryRequests = (yield* server.requests)
+ .slice(beforeQuery)
+ .filter((request) => request.method === "POST");
expect(queryRequests.every((r) => r.url.includes("auth_token=query-secret"))).toBe(true);
expect(queryRequests.every((r) => r.authorization === undefined)).toBe(true);
}),
@@ -188,9 +190,9 @@ describe("MCP multi-placement auth", () => {
it.effect("invoking with a missing credential input fails explicitly, not silently", () =>
Effect.gen(function* () {
const server = yield* serveRecordingServer;
- const executor = yield* createExecutor(
- makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),
- );
+ const executor = yield* makeTestExecutor({
+ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const,
+ });
yield* executor.mcp.addServer({
name: "Strict MCP",
diff --git a/packages/plugins/mcp/src/testing/server.ts b/packages/plugins/mcp/src/testing/server.ts
index 2567253c7..9e8a0dc7b 100644
--- a/packages/plugins/mcp/src/testing/server.ts
+++ b/packages/plugins/mcp/src/testing/server.ts
@@ -398,18 +398,21 @@ export const makeElicitationMcpServer = () => {
description: "Asks for approval before echoing a value",
inputSchema: { value: z.string() },
},
- async ({ value }: { value: string }) => {
- const response = await server.server.elicitInput({
- mode: "form",
- message: `Approve echo for "${value}"?`,
- requestedSchema: {
- type: "object",
- properties: {
- approved: { type: "boolean", title: "Approve" },
+ async ({ value }: { value: string }, extra) => {
+ const response = await server.server.elicitInput(
+ {
+ mode: "form",
+ message: `Approve echo for "${value}"?`,
+ requestedSchema: {
+ type: "object",
+ properties: {
+ approved: { type: "boolean", title: "Approve" },
+ },
+ required: ["approved"],
},
- required: ["approved"],
},
- });
+ { relatedRequestId: extra.requestId },
+ );
if (response.action !== "accept" || !response.content || response.content.approved !== true) {
return {
diff --git a/packages/plugins/microsoft/package.json b/packages/plugins/microsoft/package.json
index f833cb720..e1a97f0ef 100644
--- a/packages/plugins/microsoft/package.json
+++ b/packages/plugins/microsoft/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-microsoft",
"version": "1.5.19",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/microsoft",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/onepassword/package.json b/packages/plugins/onepassword/package.json
index 13e5d5669..9bfcb7099 100644
--- a/packages/plugins/onepassword/package.json
+++ b/packages/plugins/onepassword/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-onepassword",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/onepassword",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json
index 6c7c6e5af..595d890d8 100644
--- a/packages/plugins/openapi/package.json
+++ b/packages/plugins/openapi/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-openapi",
"version": "1.5.20",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/openapi",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/openapi/src/sdk/output-schema-migration.ts b/packages/plugins/openapi/src/sdk/output-schema-migration.ts
index c5a5421cb..864479609 100644
--- a/packages/plugins/openapi/src/sdk/output-schema-migration.ts
+++ b/packages/plugins/openapi/src/sdk/output-schema-migration.ts
@@ -5,7 +5,7 @@
// `http` side channel), so persisted schemas must describe the payload
// only — otherwise describe previews show an envelope invocations no
// longer return. Mirrors the cloud drizzle migration
-// (apps/cloud/drizzle/0002_unwrap_openapi_output_envelope.sql) for the
+// (legacy/cloud/drizzle/0002_unwrap_openapi_output_envelope.sql) for the
// libSQL-backed apps, where it runs once through the data-migration ledger.
//
// Idempotent: payload-shaped rows don't match the envelope signature, so
diff --git a/packages/plugins/openapi/src/sdk/real-specs.test.ts b/packages/plugins/openapi/src/sdk/real-specs.test.ts
index a2b3f895b..59963c6f2 100644
--- a/packages/plugins/openapi/src/sdk/real-specs.test.ts
+++ b/packages/plugins/openapi/src/sdk/real-specs.test.ts
@@ -1,6 +1,6 @@
// Parse / extract / preview coverage against a big real-world spec.
// DB-touching behaviour (addSpec, removeSpec, tool registration) moved
-// to apps/cloud/src/services/sources-api.node.test.ts — those run
+// to legacy/cloud/src/services/sources-api.node.test.ts, those run
// through the real Drizzle/FumaDB path so storage regressions
// (e.g. a per-row createMany fallback) surface automatically instead
// of needing a dedicated budget assertion.
diff --git a/packages/plugins/toolkits/package.json b/packages/plugins/toolkits/package.json
index 432588b46..c55899e8d 100644
--- a/packages/plugins/toolkits/package.json
+++ b/packages/plugins/toolkits/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-toolkits",
"version": "1.5.12",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/toolkits",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/plugins/workos-vault/package.json b/packages/plugins/workos-vault/package.json
index 1f1017918..d2f832119 100644
--- a/packages/plugins/workos-vault/package.json
+++ b/packages/plugins/workos-vault/package.json
@@ -1,6 +1,7 @@
{
"name": "@executor-js/plugin-workos-vault",
"version": "0.0.2",
+ "private": true,
"homepage": "https://github.com/RhysSullivan/executor/tree/main/packages/plugins/workos-vault",
"bugs": {
"url": "https://github.com/RhysSullivan/executor/issues"
diff --git a/packages/react/src/api/oauth-popup.ts b/packages/react/src/api/oauth-popup.ts
index 3c9bb7834..cd1d49e25 100644
--- a/packages/react/src/api/oauth-popup.ts
+++ b/packages/react/src/api/oauth-popup.ts
@@ -228,7 +228,7 @@ export const openOAuthPopup = (input: OpenOAuthPopupInput): (() =>
// URL in the user's real browser. There's no shared origin, so the
// renderer polls `/api/oauth/await/:sessionId` for the result. The local
// server publishes there via `setOAuthCompletionListener` (see
-// apps/local/src/serve.ts).
+// legacy/local/src/serve.ts).
// ---------------------------------------------------------------------------
export type OpenOAuthSystemBrowserInput = {
diff --git a/packages/react/src/components/mcp-install-card.test.ts b/packages/react/src/components/mcp-install-card.test.ts
index df31a2094..1ace98e58 100644
--- a/packages/react/src/components/mcp-install-card.test.ts
+++ b/packages/react/src/components/mcp-install-card.test.ts
@@ -91,11 +91,11 @@ describe("MCP install command rendering", () => {
mode: "stdio",
isDev: true,
origin: null,
- scopeDir: "/Users/rhyssullivan/src/executor/apps/local",
- devCliCwd: "/Users/rhyssullivan/src/executor",
+ scopeDir: "/Users/example/src/executor/legacy/local",
+ devCliCwd: "/Users/example/src/executor",
}),
).toBe(
- "npx add-mcp 'bun run --cwd /Users/rhyssullivan/src/executor dev:cli mcp --scope /Users/rhyssullivan/src/executor/apps/local' --name executor",
+ "npx add-mcp 'bun run --cwd /Users/example/src/executor legacy:dev:cli mcp --scope /Users/example/src/executor/legacy/local' --name executor",
);
});
diff --git a/packages/react/src/components/mcp-install-card.tsx b/packages/react/src/components/mcp-install-card.tsx
index d24920d21..007979429 100644
--- a/packages/react/src/components/mcp-install-card.tsx
+++ b/packages/react/src/components/mcp-install-card.tsx
@@ -103,8 +103,8 @@ export const buildMcpInstallCommand = (input: {
const innerArgs = input.isDev
? input.devCliCwd
- ? ["bun", "run", "--cwd", input.devCliCwd, "dev:cli", "mcp"]
- : ["bun", "run", "dev:cli", "mcp"]
+ ? ["bun", "run", "--cwd", input.devCliCwd, "legacy:dev:cli", "mcp"]
+ : ["bun", "run", "legacy:dev:cli", "mcp"]
: ["executor", "mcp"];
if (input.scopeDir) {
innerArgs.push("--scope", input.scopeDir);
diff --git a/packages/react/src/styles/globals.css b/packages/react/src/styles/globals.css
index 595864e01..716a23fa9 100644
--- a/packages/react/src/styles/globals.css
+++ b/packages/react/src/styles/globals.css
@@ -3,8 +3,7 @@
@custom-variant dark (&:is(.dark *));
@source "../**/*.{ts,tsx}";
-@source "../../../../apps/local/src/**/*.{ts,tsx}";
-@source "../../../../apps/cloud/src/**/*.{ts,tsx}";
+@source "../../../../legacy/local/src/**/*.{ts,tsx}";
@source "../../../plugins/*/src/react/**/*.{ts,tsx}";
@theme inline {
@@ -151,7 +150,7 @@
/* The sidebar header sits at the top-left of the desktop window, under the
native macOS traffic-light controls (trafficLightPosition {x:16,y:17} in
- apps/desktop/src/main/index.ts). Offset its content past the lights and make
+ legacy/desktop/src/main/index.ts). Offset its content past the lights and make
the freed strip draggable like a native title bar. The desktop window stays
at or above the mobile breakpoint (minWidth 768), so the sidebar header is
the only header the lights ever overlap. */
diff --git a/packaging/launchd/bounded-log.sh b/packaging/launchd/bounded-log.sh
new file mode 100755
index 000000000..25cf59c32
--- /dev/null
+++ b/packaging/launchd/bounded-log.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+set -euo pipefail
+
+readonly MAX_BYTES=$((1024 * 1024))
+readonly GENERATIONS=3
+readonly CHUNK_BYTES=$((64 * 1024))
+readonly LOG_PATH=${1:?a log path is required}
+
+umask 0077
+LC_ALL=C
+export LC_ALL
+
+temporary_file=""
+cleanup() {
+ if [[ -n "$temporary_file" ]]; then
+ rm -f "$temporary_file"
+ fi
+}
+trap cleanup EXIT
+
+require_regular_file_or_absent() {
+ local path=$1
+
+ if [[ -L "$path" ]]; then
+ printf 'refusing symbolic-link log path: %s\n' "$path" >&2
+ exit 1
+ fi
+ if [[ -e "$path" && ! -f "$path" ]]; then
+ printf 'log path is not a regular file: %s\n' "$path" >&2
+ exit 1
+ fi
+}
+
+require_regular_file_or_absent "$LOG_PATH"
+generation=1
+while [[ "$generation" -le "$GENERATIONS" ]]; do
+ require_regular_file_or_absent "${LOG_PATH}.${generation}"
+ generation=$((generation + 1))
+done
+
+touch "$LOG_PATH"
+chmod 0600 "$LOG_PATH"
+generation=1
+while [[ "$generation" -le "$GENERATIONS" ]]; do
+ if [[ -f "${LOG_PATH}.${generation}" ]]; then
+ chmod 0600 "${LOG_PATH}.${generation}"
+ fi
+ generation=$((generation + 1))
+done
+bytes=$(wc -c < "$LOG_PATH")
+
+if [[ "$bytes" -gt "$MAX_BYTES" ]]; then
+ trimmed_log=$(mktemp "${LOG_PATH}.trim.XXXXXXXX")
+ temporary_file=$trimmed_log
+ tail -c "$MAX_BYTES" "$LOG_PATH" > "$trimmed_log"
+ chmod 0600 "$trimmed_log"
+ mv -f "$trimmed_log" "$LOG_PATH"
+ temporary_file=""
+ bytes=$MAX_BYTES
+fi
+
+rotate() {
+ local generation
+ rm -f "${LOG_PATH}.${GENERATIONS}"
+ generation=$((GENERATIONS - 1))
+ while [[ "$generation" -ge 1 ]]; do
+ if [[ -f "${LOG_PATH}.${generation}" ]]; then
+ mv -f "${LOG_PATH}.${generation}" "${LOG_PATH}.$((generation + 1))"
+ fi
+ generation=$((generation - 1))
+ done
+ if [[ -f "$LOG_PATH" ]]; then
+ mv -f "$LOG_PATH" "${LOG_PATH}.1"
+ fi
+ : > "$LOG_PATH"
+ chmod 0600 "$LOG_PATH"
+ bytes=0
+}
+
+chunk_file=$(mktemp "${LOG_PATH}.chunk.XXXXXXXX")
+temporary_file=$chunk_file
+
+while :; do
+ if [[ "$bytes" -ge "$MAX_BYTES" ]]; then
+ read_size=$CHUNK_BYTES
+ else
+ read_size=$((MAX_BYTES - bytes))
+ if [[ "$read_size" -gt "$CHUNK_BYTES" ]]; then
+ read_size=$CHUNK_BYTES
+ fi
+ fi
+
+ : > "$chunk_file"
+ if ! dd bs="$read_size" count=1 of="$chunk_file" 2>/dev/null; then
+ printf 'failed to read launchd log input\n' >&2
+ exit 1
+ fi
+ chunk_size=$(wc -c < "$chunk_file")
+ if [[ "$chunk_size" -eq 0 ]]; then
+ break
+ fi
+ if [[ "$bytes" -ge "$MAX_BYTES" ]]; then
+ rotate
+ fi
+ cat "$chunk_file" >> "$LOG_PATH"
+ bytes=$((bytes + chunk_size))
+done
diff --git a/packaging/launchd/dev.executor.gateway.plist b/packaging/launchd/dev.executor.gateway.plist
new file mode 100644
index 000000000..2b7623ca5
--- /dev/null
+++ b/packaging/launchd/dev.executor.gateway.plist
@@ -0,0 +1,27 @@
+
+
+
+
+ Label
+ dev.executor.gateway
+ ProgramArguments
+
+ @EXECUTOR_WRAPPER@
+
+ RunAtLoad
+
+ KeepAlive
+
+ SuccessfulExit
+
+
+ ThrottleInterval
+ 5
+ ExitTimeOut
+ 30
+ Umask
+ 63
+ ProcessType
+ Background
+
+
diff --git a/packaging/systemd/executor.env.example b/packaging/systemd/executor.env.example
new file mode 100644
index 000000000..0c801291b
--- /dev/null
+++ b/packaging/systemd/executor.env.example
@@ -0,0 +1,14 @@
+# Executor defaults to the loopback origin derived from its bind address.
+# Set the exact browser-facing HTTPS origin when using a reverse proxy.
+# EXECUTOR_PUBLIC_ORIGIN=https://executor.example.com
+
+# Trust forwarded client addresses only from explicitly listed proxy networks.
+# Every proxy in the chain must be listed.
+# EXECUTOR_TRUSTED_PROXIES=127.0.0.1/32,::1/128
+
+# Optional path to an externally managed 32-byte master key. This is a path,
+# never key material. See docs/systemd.md for ownership and mode requirements.
+# EXECUTOR_MASTER_KEY_FILE=/etc/executor/external-master.key
+
+# Rust tracing filter. Do not place credentials or API tokens in this file.
+RUST_LOG=executor=info
diff --git a/packaging/systemd/executor.service b/packaging/systemd/executor.service
new file mode 100644
index 000000000..1fc4cfa58
--- /dev/null
+++ b/packaging/systemd/executor.service
@@ -0,0 +1,57 @@
+[Unit]
+Description=Executor self-hosted gateway
+Documentation=https://github.com/davis7dotsh/executor-fork-testing/blob/main/docs/systemd.md
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=executor
+Group=executor
+WorkingDirectory=/var/lib/executor
+EnvironmentFile=-/etc/executor/executor.env
+ExecStart=/usr/local/bin/executor server \
+ --bind 127.0.0.1:4788 \
+ --data-dir /var/lib/executor \
+ --mcp-stdio-templates /etc/executor/mcp-stdio-templates.json
+Restart=on-failure
+RestartSec=2s
+KillSignal=SIGTERM
+TimeoutStartSec=30s
+TimeoutStopSec=90s
+UMask=0077
+
+# systemd creates these paths before dropping privileges.
+StateDirectory=executor
+StateDirectoryMode=0700
+ConfigurationDirectory=executor
+ConfigurationDirectoryMode=0750
+
+# The server and any approved MCP stdio children inherit this sandbox. Add
+# narrowly scoped ReadOnlyPaths= or ReadWritePaths= entries in a drop-in when a
+# template needs access outside the Executor state directory.
+NoNewPrivileges=yes
+CapabilityBoundingSet=
+AmbientCapabilities=
+PrivateDevices=yes
+PrivateTmp=yes
+ProtectSystem=strict
+ProtectHome=yes
+ReadWritePaths=/var/lib/executor
+ProtectControlGroups=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectClock=yes
+ProtectHostname=yes
+ProtectProc=invisible
+ProcSubset=pid
+LockPersonality=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+SystemCallArchitectures=native
+KeyringMode=private
+
+[Install]
+WantedBy=multi-user.target
diff --git a/packaging/systemd/mcp-stdio-templates.example.json b/packaging/systemd/mcp-stdio-templates.example.json
new file mode 100644
index 000000000..834f94b8c
--- /dev/null
+++ b/packaging/systemd/mcp-stdio-templates.example.json
@@ -0,0 +1,14 @@
+{
+ "templates": [
+ {
+ "name": "example",
+ "executable": "/usr/local/libexec/executor/example-mcp-server",
+ "cwd": "/var/lib/executor/mcp/example",
+ "arguments": ["--stdio"],
+ "environment": {
+ "LOG_LEVEL": "warn"
+ },
+ "secretEnvironment": ["API_TOKEN"]
+ }
+ ]
+}
diff --git a/scripts/agent-config-smoke.ts b/scripts/agent-config-smoke.ts
index 5bab28383..c184be3f9 100644
--- a/scripts/agent-config-smoke.ts
+++ b/scripts/agent-config-smoke.ts
@@ -23,7 +23,7 @@ import {
import { Effect } from "effect";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
-const cliEntrypoint = join(repoRoot, "apps/cli/src/main.ts");
+const cliEntrypoint = join(repoRoot, "legacy/cli/src/main.ts");
type CliContext = {
readonly dataDir: string;
diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts
index 923d868d7..45c86b1b5 100644
--- a/scripts/bootstrap.ts
+++ b/scripts/bootstrap.ts
@@ -11,24 +11,43 @@ import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
-const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
+export type BootstrapExecutor = (
+ label: string,
+ command: string,
+ args: ReadonlyArray,
+) => void;
-const run = (label: string, cmd: string, args: ReadonlyArray) => {
- console.log(`\n[bootstrap] ${label}: ${cmd} ${args.join(" ")}`);
- execFileSync(cmd, [...args], { cwd: repoRoot, stdio: "inherit" });
+export const runBootstrap = (execute: BootstrapExecutor) => {
+ execute("dependencies (+ prepare builds)", "bun", ["install", "--frozen-lockfile"]);
+ execute("playwright chromium", "bun", [
+ "run",
+ "--cwd",
+ "e2e",
+ "playwright",
+ "install",
+ "--with-deps",
+ "chromium",
+ ]);
};
-// `bun install` runs the workspace prepare hook, which builds
-// @executor-js/vite-plugin and @executor-js/react — the two artifacts the
-// apps' vite dev servers fail without in a fresh worktree.
-run("dependencies (+ prepare builds)", "bun", ["install"]);
+const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
+
+const main = () => {
+ // `bun install --frozen-lockfile` runs the workspace prepare hook, which builds
+ // @executor-js/vite-plugin and @executor-js/react, the two artifacts the
+ // legacy apps' Vite dev servers fail without in a fresh worktree.
+ // Resolve Playwright from e2e so the browser revision matches its locked
+ // dependency. The cache is shared per-machine when Chromium is already present.
+ runBootstrap((label, command, args) => {
+ console.log(`\n[bootstrap] ${label}: ${command} ${args.join(" ")}`);
+ execFileSync(command, [...args], { cwd: repoRoot, stdio: "inherit" });
+ });
-// e2e browser scenarios need Playwright's chromium; the cache is shared
-// per-machine so this is a fast no-op when already present.
-run("playwright chromium", "bunx", ["playwright", "install", "chromium"]);
+ if (!existsSync(resolve(repoRoot, "node_modules/.bin/vitest"))) {
+ throw new Error("bootstrap: vitest missing after install, bun install likely failed");
+ }
-if (!existsSync(resolve(repoRoot, "node_modules/.bin/vitest"))) {
- throw new Error("bootstrap: vitest missing after install — bun install likely failed");
-}
+ console.log("\n[bootstrap] done. See RUNNING.md for current verification commands.");
+};
-console.log("\n[bootstrap] done — `cd e2e && bun run test` runs the full suite.");
+if (import.meta.main) main();
diff --git a/scripts/check-changelog-stubs.ts b/scripts/check-changelog-stubs.ts
deleted file mode 100644
index 98dbee22f..000000000
--- a/scripts/check-changelog-stubs.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env bun
-/**
- * Verifies every workspace package directory has a `CHANGELOG.md` file.
- *
- * `changesets/action@v1` (the GitHub Action wrapping the Changesets CLI in
- * `release.yml`) creates the Version Packages PR after `changeset version`
- * updates package versions and changelogs. Every workspace package should have
- * a `CHANGELOG.md` seed so Changesets has a stable file to update and the
- * action never falls back to missing-file behavior. Keep seed files H1-only:
- * Changesets inserts generated version sections immediately after the H1, so
- * any placeholder prose would become part of the first generated release notes.
- *
- * GitHub Release notes are still authored separately at
- * `apps/cli/release-notes/next.md`; package changelogs are generated from
- * `.changeset/*.md`.
- *
- * Usage:
- * bun run scripts/check-changelog-stubs.ts # fail on missing
- * bun run scripts/check-changelog-stubs.ts --fix # create missing stubs
- */
-import { existsSync, readFileSync, writeFileSync } from "node:fs";
-import { dirname, relative, resolve } from "node:path";
-
-const repoRoot = resolve(import.meta.dir, "..");
-
-type Pkg = { name?: string; private?: boolean };
-
-const findWorkspacePackages = (): string[] => {
- const root = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8")) as {
- workspaces?: string[];
- };
- const patterns = root.workspaces ?? [];
- const dirs = new Set();
- for (const pattern of patterns) {
- // Bun.Glob — handles workspace patterns like "packages/*/*", "apps/*"
- for (const match of new Bun.Glob(`${pattern}/package.json`).scanSync({ cwd: repoRoot })) {
- dirs.add(dirname(resolve(repoRoot, match)));
- }
- }
- return [...dirs].sort();
-};
-
-const STUB_TEMPLATE = (name: string) => `# ${name}\n`;
-
-const fix = process.argv.includes("--fix");
-const missing: string[] = [];
-
-for (const pkgDir of findWorkspacePackages()) {
- const changelogPath = resolve(pkgDir, "CHANGELOG.md");
- if (existsSync(changelogPath)) continue;
-
- const pkg = JSON.parse(readFileSync(resolve(pkgDir, "package.json"), "utf8")) as Pkg;
- const name = pkg.name ?? relative(repoRoot, pkgDir);
-
- if (fix) {
- writeFileSync(changelogPath, STUB_TEMPLATE(name));
- console.log(`Created stub: ${relative(repoRoot, changelogPath)}`);
- } else {
- missing.push(`${relative(repoRoot, pkgDir)} (${name})`);
- }
-}
-
-if (!fix && missing.length > 0) {
- console.error(
- `\nMissing CHANGELOG.md in ${missing.length} workspace package(s):\n - ${missing.join("\n - ")}\n\n` +
- "These seed files are required so Changesets can update every affected\n" +
- "workspace changelog during the Version Packages PR.\n\n" +
- "Run `bun run scripts/check-changelog-stubs.ts --fix` to create stubs.\n",
- );
- process.exit(1);
-}
diff --git a/scripts/check-docker-workspace-context.ts b/scripts/check-docker-workspace-context.ts
new file mode 100644
index 000000000..458c57c45
--- /dev/null
+++ b/scripts/check-docker-workspace-context.ts
@@ -0,0 +1,173 @@
+#!/usr/bin/env bun
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+import { runBootstrap, type BootstrapExecutor } from "./bootstrap";
+
+const repoRoot = resolve(import.meta.dir, "..");
+const sourceWorkspaceRoots = new Set(["web"]);
+
+const read = (path: string) => readFileSync(resolve(repoRoot, path), "utf8");
+const normalizedLines = (contents: string) =>
+ contents
+ .split(/\r?\n/u)
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
+
+const rootPackage = JSON.parse(read("package.json")) as { readonly workspaces?: unknown };
+if (
+ !Array.isArray(rootPackage.workspaces) ||
+ !rootPackage.workspaces.every((workspace): workspace is string => typeof workspace === "string")
+) {
+ throw new Error("package.json must declare a string array of workspaces");
+}
+
+const failures: string[] = [];
+const embeddedServiceFiles = [
+ ...read("src/service.rs").matchAll(/include_bytes!\("\.\.\/([^"\n]+)"\)/gu),
+].map((match) => match[1]);
+const rustDockerfiles = ["Dockerfile", "Dockerfile.release-native"] as const;
+for (const dockerfileName of rustDockerfiles) {
+ const contents = read(dockerfileName);
+ for (const embeddedFile of embeddedServiceFiles) {
+ const copy = `COPY ${embeddedFile} ./${embeddedFile}`;
+ if (!contents.includes(copy)) {
+ failures.push(`${dockerfileName} must stage embedded Rust input: ${copy}`);
+ }
+ }
+}
+
+const requiredChromiumInstall = "run: bun run --cwd e2e playwright install --with-deps chromium";
+const ciWorkflow = read(".github/workflows/ci.yml");
+const chromiumInstallCount = ciWorkflow.split(requiredChromiumInstall).length - 1;
+if (chromiumInstallCount !== 1) {
+ failures.push(
+ `CI must install Chromium exactly once through the e2e workspace: ${requiredChromiumInstall}`,
+ );
+}
+if (ciWorkflow.includes("bunx playwright install")) {
+ failures.push("CI must not install Playwright through root bunx resolution");
+}
+
+const bootstrapCommands: Parameters[] = [];
+runBootstrap((label, command, args) => bootstrapCommands.push([label, command, args]));
+const actualBootstrapCommands = bootstrapCommands.map(([, command, args]) => [command, ...args]);
+const expectedBootstrapCommands = [
+ ["bun", "install", "--frozen-lockfile"],
+ ["bun", "run", "--cwd", "e2e", "playwright", "install", "--with-deps", "chromium"],
+];
+if (JSON.stringify(actualBootstrapCommands) !== JSON.stringify(expectedBootstrapCommands)) {
+ failures.push(
+ "Bootstrap command plan must run only the frozen dependency install followed by the locked e2e Chromium install",
+ );
+}
+
+const workspacePatterns = rootPackage.workspaces.map((workspace) => workspace.replace(/\/+$/u, ""));
+const workspaceManifests = new Set();
+const patternsByRoot = new Map();
+
+for (const pattern of workspacePatterns) {
+ const root = pattern.split("/", 1)[0];
+ if (!root || /[*?[\]{}]/u.test(root)) {
+ failures.push(`workspace pattern needs a literal top-level directory: ${pattern}`);
+ continue;
+ }
+
+ const patterns = patternsByRoot.get(root) ?? [];
+ patterns.push(pattern);
+ patternsByRoot.set(root, patterns);
+
+ const matches = [
+ ...new Bun.Glob(`${pattern}/package.json`).scanSync({ cwd: repoRoot, onlyFiles: true }),
+ ].map((path) => path.replaceAll("\\", "/"));
+ if (matches.length === 0) {
+ failures.push(`workspace pattern matches no package manifests: ${pattern}`);
+ }
+ for (const manifest of matches) workspaceManifests.add(manifest);
+}
+
+const manifestOnlyRoots = [...patternsByRoot.keys()]
+ .filter((root) => !sourceWorkspaceRoots.has(root))
+ .sort();
+const dockerignoreRules = normalizedLines(read("Dockerfile.dockerignore"));
+const requiredDockerContextPaths = [...embeddedServiceFiles, "scripts/package-release-archive.py"];
+const requiredDockerContextDirectories = new Set(
+ requiredDockerContextPaths.flatMap((path) => {
+ const segments = path.split("/");
+ return segments.slice(0, -1).map((_, index) => segments.slice(0, index + 1).join("/"));
+ }),
+);
+for (const path of [...requiredDockerContextDirectories, ...requiredDockerContextPaths]) {
+ if (!dockerignoreRules.includes(`!${path}`)) {
+ failures.push(`Dockerfile.dockerignore must include release build input: !${path}`);
+ }
+}
+
+const packageDockerfile = read("Dockerfile.release-package");
+if (!packageDockerfile.includes("COPY scripts/package-release-archive.py")) {
+ failures.push("Dockerfile.release-package must copy the deterministic archive packager");
+}
+const actualManifestRules = dockerignoreRules.filter((rule) => {
+ const path = rule.startsWith("!") ? rule.slice(1) : rule;
+ return manifestOnlyRoots.some((root) => path === root || path.startsWith(`${root}/`));
+});
+const expectedManifestRules = manifestOnlyRoots.flatMap((root) => [
+ `!${root}`,
+ `${root}/**`,
+ ...(patternsByRoot.get(root) ?? []).toSorted().map((pattern) => `!${pattern}/package.json`),
+]);
+
+if (actualManifestRules.join("\n") !== expectedManifestRules.join("\n")) {
+ failures.push(
+ [
+ "Dockerfile.dockerignore workspace rules are out of sync.",
+ "Expected this manifest-only rule sequence:",
+ ...expectedManifestRules.map((rule) => ` ${rule}`),
+ ].join("\n"),
+ );
+}
+
+const dockerfileLines = normalizedLines(read("Dockerfile")).map((line) =>
+ line.replace(/\s+/gu, " "),
+);
+const installIndex = dockerfileLines.findIndex((line) =>
+ line.includes("bun install --frozen-lockfile"),
+);
+if (installIndex === -1) {
+ failures.push("Dockerfile has no frozen Bun install to validate");
+} else {
+ const stagedBeforeInstall = new Set(dockerfileLines.slice(0, installIndex));
+ for (const root of manifestOnlyRoots) {
+ const instruction = `COPY ${root} ./${root}`;
+ if (!stagedBeforeInstall.has(instruction)) {
+ failures.push(
+ `Dockerfile must stage ${root} manifests before the frozen install: ${instruction}`,
+ );
+ }
+ }
+ for (const root of sourceWorkspaceRoots) {
+ for (const manifest of [...workspaceManifests].filter((path) => path.startsWith(`${root}/`))) {
+ const instruction = `COPY ${manifest} ./${manifest}`;
+ if (!stagedBeforeInstall.has(instruction)) {
+ failures.push(
+ `Dockerfile must stage ${manifest} before the frozen install: ${instruction}`,
+ );
+ }
+ }
+ }
+}
+
+for (const root of sourceWorkspaceRoots) {
+ if (!patternsByRoot.has(root)) {
+ failures.push(`source workspace root is no longer declared: ${root}`);
+ }
+}
+
+if (failures.length > 0) {
+ console.error(`Docker workspace context check failed:\n\n${failures.join("\n\n")}\n`);
+ process.exit(1);
+}
+
+console.log(
+ `Docker workspace context stages ${workspaceManifests.size} manifests across ${patternsByRoot.size} roots.`,
+);
diff --git a/scripts/check-release-channel-order.py b/scripts/check-release-channel-order.py
new file mode 100644
index 000000000..064701ef3
--- /dev/null
+++ b/scripts/check-release-channel-order.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import pathlib
+import re
+import sys
+
+
+NUMERIC_IDENTIFIER = r"(?:0|[1-9][0-9]*)"
+PRERELEASE_IDENTIFIER = rf"(?:{NUMERIC_IDENTIFIER}|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
+SEMVER_PATTERN = re.compile(
+ rf"^({NUMERIC_IDENTIFIER})\.({NUMERIC_IDENTIFIER})\.({NUMERIC_IDENTIFIER})"
+ rf"(?:-({PRERELEASE_IDENTIFIER}(?:\.{PRERELEASE_IDENTIFIER})*))?"
+ r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
+)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Refuse release channel moves that would lower SemVer precedence"
+ )
+ parser.add_argument("--channel", choices=("latest", "beta"), required=True)
+ current_source = parser.add_mutually_exclusive_group(required=True)
+ current_source.add_argument("--current")
+ current_source.add_argument("--history-file")
+ parser.add_argument("--candidate", required=True)
+ return parser.parse_args()
+
+
+def parse_version(value):
+ match = SEMVER_PATTERN.fullmatch(value)
+ if match is None:
+ raise ValueError(f"invalid SemVer: {value}")
+ major, minor, patch, prerelease = match.groups()
+ return (
+ (int(major), int(minor), int(patch)),
+ None if prerelease is None else prerelease.split("."),
+ )
+
+
+def compare_prerelease(left, right):
+ if left is None:
+ return 0 if right is None else 1
+ if right is None:
+ return -1
+
+ for left_identifier, right_identifier in zip(left, right, strict=False):
+ if left_identifier == right_identifier:
+ continue
+ left_numeric = left_identifier.isdigit()
+ right_numeric = right_identifier.isdigit()
+ if left_numeric and right_numeric:
+ return 1 if int(left_identifier) > int(right_identifier) else -1
+ if left_numeric != right_numeric:
+ return -1 if left_numeric else 1
+ return 1 if left_identifier > right_identifier else -1
+
+ if len(left) == len(right):
+ return 0
+ return 1 if len(left) > len(right) else -1
+
+
+def compare_versions(left, right):
+ left_core, left_prerelease = parse_version(left)
+ right_core, right_prerelease = parse_version(right)
+ if left_core != right_core:
+ return 1 if left_core > right_core else -1
+ return compare_prerelease(left_prerelease, right_prerelease)
+
+
+def validate_channel_version(channel, value, role):
+ parsed = parse_version(value)
+ is_prerelease = parsed[1] is not None
+ expected_prerelease = channel == "beta"
+ if is_prerelease != expected_prerelease:
+ raise ValueError(f"{channel} channel has an incompatible {role} version")
+ return parsed
+
+
+def load_release_history(path):
+ if path == "-":
+ pages = json.load(sys.stdin)
+ else:
+ pages = json.loads(pathlib.Path(path).read_text(encoding="utf-8"))
+ if not isinstance(pages, list):
+ raise ValueError("release history must be a JSON array of pages")
+ for page in pages:
+ if not isinstance(page, list):
+ raise ValueError("release history page must be a JSON array")
+ for release in page:
+ if not isinstance(release, dict):
+ raise ValueError("release history entry must be a JSON object")
+ yield release
+
+
+def latest_published_version(channel, history_file):
+ latest = None
+ for release in load_release_history(history_file):
+ if release.get("draft") is not False:
+ continue
+ tag = release.get("tag_name")
+ if not isinstance(tag, str) or not tag.startswith("v"):
+ continue
+ version = tag[1:]
+ try:
+ validate_channel_version(channel, version, "published")
+ except ValueError:
+ continue
+ if latest is None or compare_versions(version, latest) > 0:
+ latest = version
+ return latest
+
+
+def main():
+ args = parse_args()
+ try:
+ validate_channel_version(args.channel, args.candidate, "candidate")
+ current = (
+ args.current
+ if args.current is not None
+ else latest_published_version(args.channel, args.history_file)
+ )
+ if current is not None:
+ validate_channel_version(args.channel, current, "current")
+ except ValueError as error:
+ raise SystemExit(str(error)) from error
+
+ if current is None:
+ print("first")
+ return
+
+ relation = compare_versions(args.candidate, current)
+ if relation < 0:
+ raise SystemExit(
+ f"candidate would move {args.channel} backward from {current} to {args.candidate}"
+ )
+ print("equal" if relation == 0 else "newer")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/check-warden-targets.ts b/scripts/check-warden-targets.ts
new file mode 100644
index 000000000..05e343849
--- /dev/null
+++ b/scripts/check-warden-targets.ts
@@ -0,0 +1,49 @@
+#!/usr/bin/env bun
+
+import { Schema } from "effect";
+import { resolve } from "node:path";
+
+const WardenConfig = Schema.Struct({
+ defaults: Schema.Struct({
+ ignorePaths: Schema.Array(Schema.String),
+ }),
+ skills: Schema.Array(
+ Schema.Struct({
+ name: Schema.String,
+ paths: Schema.Array(Schema.String),
+ }),
+ ),
+});
+
+const decodeWardenConfig = Schema.decodeUnknownSync(WardenConfig);
+const repoRoot = resolve(import.meta.dirname, "..");
+const configPath = resolve(repoRoot, process.argv[2] ?? "warden.toml");
+const config = decodeWardenConfig(Bun.TOML.parse(await Bun.file(configPath).text()));
+const ignored = config.defaults.ignorePaths.map((pattern) => new Bun.Glob(pattern));
+const zeroMatchTargets: string[] = [];
+let targetCount = 0;
+
+for (const skill of config.skills) {
+ for (const pattern of skill.paths) {
+ targetCount += 1;
+ const matches = [
+ ...new Bun.Glob(pattern).scanSync({
+ cwd: repoRoot,
+ dot: true,
+ onlyFiles: true,
+ }),
+ ].filter((path) => !ignored.some((glob) => glob.match(path)));
+
+ if (matches.length === 0) {
+ zeroMatchTargets.push(`${skill.name}: ${pattern}`);
+ }
+ }
+}
+
+if (zeroMatchTargets.length > 0) {
+ console.error("Warden targets resolving to zero non-ignored files:");
+ for (const target of zeroMatchTargets) console.error(`- ${target}`);
+ process.exit(1);
+}
+
+console.log(`Warden target guard checked ${targetCount} configured targets.`);
diff --git a/scripts/clean.ts b/scripts/clean.ts
index 859eaf641..ab5451ca2 100644
--- a/scripts/clean.ts
+++ b/scripts/clean.ts
@@ -20,7 +20,7 @@ for (const name of rootTargets) {
const nestedTargets = new Set(["node_modules", "dist", ".turbo", ".output", ".astro"]);
const nestedGlobs = /\.tsbuildinfo$/;
-const searchRoots = ["apps", "packages"];
+const searchRoots = ["apps", "legacy", "packages"];
const maxDepth = 5;
function clean(dir: string, depth: number) {
diff --git a/scripts/install-launchd.sh b/scripts/install-launchd.sh
new file mode 100755
index 000000000..4bed9c60e
--- /dev/null
+++ b/scripts/install-launchd.sh
@@ -0,0 +1,812 @@
+#!/usr/bin/env bash
+set -eo pipefail
+export LC_ALL=C
+
+LABEL="dev.executor.gateway"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+REPOSITORY_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)"
+TEMPLATE="${REPOSITORY_ROOT}/packaging/launchd/${LABEL}.plist"
+LOG_WRITER_SOURCE="${REPOSITORY_ROOT}/packaging/launchd/bounded-log.sh"
+PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
+EXECUTOR_BINARY="${EXECUTOR_BINARY:-}"
+DEFAULT_DATA_DIR="${HOME}/Library/Application Support/Executor"
+EXECUTOR_ROOT="${HOME}/.executor"
+INSTALL_ROOT="${EXECUTOR_ROOT}/service"
+MANIFEST="${INSTALL_ROOT}/service-install.manifest"
+RECOVERY_MARKER="${INSTALL_ROOT}/service-install.recovery"
+CONFIG_FILE="${INSTALL_ROOT}/service-config"
+CURRENT_UID="$(id -u)"
+data_dir_explicit=false
+templates_explicit=false
+public_origin_explicit=false
+trusted_proxies_explicit=false
+data_dir_override=""
+templates_override=""
+public_origin_override=""
+trusted_proxies_override=""
+if [[ ${EXECUTOR_DATA_DIR+x} == x ]]; then
+ data_dir_explicit=true
+ data_dir_override=$EXECUTOR_DATA_DIR
+fi
+if [[ ${EXECUTOR_MCP_STDIO_TEMPLATES_FILE+x} == x ]]; then
+ templates_explicit=true
+ templates_override=$EXECUTOR_MCP_STDIO_TEMPLATES_FILE
+fi
+if [[ ${EXECUTOR_PUBLIC_ORIGIN+x} == x ]]; then
+ public_origin_explicit=true
+ public_origin_override=$EXECUTOR_PUBLIC_ORIGIN
+fi
+if [[ ${EXECUTOR_TRUSTED_PROXIES+x} == x ]]; then
+ trusted_proxies_explicit=true
+ trusted_proxies_override=$EXECUTOR_TRUSTED_PROXIES
+fi
+trusted_proxy_options=()
+start_service=true
+validate_only=false
+
+usage() {
+ cat < Executor binary (default: executor from PATH)
+ --data-dir Persistent data directory
+ --templates MCP stdio template file
+ --public-origin Browser-facing origin when using an HTTPS proxy
+ --trusted-proxy Trust one proxy network (repeatable)
+ --no-start Install without loading the service
+ --validate-only Validate paths and configuration without changes
+ -h, --help Display this help message
+EOF
+}
+
+fail() {
+ printf 'Error: %s\n' "$1" >&2
+ exit 1
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --binary)
+ [[ -n "${2:-}" ]] || fail "--binary requires a path"
+ EXECUTOR_BINARY="$2"
+ shift 2
+ ;;
+ --data-dir)
+ [[ -n "${2:-}" ]] || fail "--data-dir requires a path"
+ data_dir_explicit=true
+ data_dir_override="$2"
+ shift 2
+ ;;
+ --templates)
+ [[ -n "${2:-}" ]] || fail "--templates requires a path"
+ templates_explicit=true
+ templates_override="$2"
+ shift 2
+ ;;
+ --public-origin)
+ [[ -n "${2:-}" ]] || fail "--public-origin requires an origin"
+ public_origin_explicit=true
+ public_origin_override="$2"
+ shift 2
+ ;;
+ --trusted-proxy)
+ [[ -n "${2:-}" ]] || fail "--trusted-proxy requires a CIDR"
+ trusted_proxies_explicit=true
+ trusted_proxy_options+=("$2")
+ shift 2
+ ;;
+ --no-start)
+ start_service=false
+ shift
+ ;;
+ --validate-only)
+ validate_only=true
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *) fail "unknown option: $1" ;;
+ esac
+done
+
+[[ "$(uname -s)" == "Darwin" ]] || fail "launchd installation is supported only on macOS"
+command -v shasum >/dev/null 2>&1 || fail "shasum is required"
+[[ -f "$TEMPLATE" ]] || fail "launchd template not found at $TEMPLATE"
+[[ -f "$LOG_WRITER_SOURCE" ]] || fail "bounded log writer not found at $LOG_WRITER_SOURCE"
+
+if [[ -z "$EXECUTOR_BINARY" ]]; then
+ EXECUTOR_BINARY="$(command -v executor || true)"
+fi
+[[ -n "$EXECUTOR_BINARY" && -x "$EXECUTOR_BINARY" ]] \
+ || fail "an executable Executor binary is required"
+
+absolute_path() {
+ local path=$1 directory base
+ directory="$(dirname "$path")"
+ base="$(basename "$path")"
+ [[ -d "$directory" ]] || fail "directory does not exist: $directory"
+ printf '%s/%s\n' "$(cd "$directory" && pwd -P)" "$base"
+}
+
+EXECUTOR_BINARY="$(absolute_path "$EXECUTOR_BINARY")"
+
+require_directory_or_absent() {
+ local path=$1 description=$2
+
+ [[ ! -L "$path" ]] || fail "$description must not be a symbolic link: $path"
+ if [[ -e "$path" && ! -d "$path" ]]; then
+ fail "$description must be a directory: $path"
+ fi
+}
+
+require_regular_file_or_absent() {
+ local path=$1 description=$2
+
+ [[ ! -L "$path" ]] || fail "$description must not be a symbolic link: $path"
+ if [[ -e "$path" && ! -f "$path" ]]; then
+ fail "$description must be a regular file: $path"
+ fi
+}
+
+private_file_metadata() {
+ local path=$1
+ if stat --version >/dev/null 2>&1; then
+ stat -c '%u|%a|%h' "$path"
+ else
+ stat -f '%u|%Lp|%l' "$path"
+ fi
+}
+
+private_file_identity() {
+ local path=$1
+ if stat --version >/dev/null 2>&1; then
+ stat -c '%u|%a|%h|%d|%i|%s' "$path"
+ else
+ stat -f '%u|%Lp|%l|%d|%i|%z' "$path"
+ fi
+}
+
+require_private_control_file() {
+ local path=$1 description=$2 metadata owner mode links
+
+ require_regular_file_or_absent "$path" "$description"
+ [[ -e "$path" ]] || return 0
+ metadata="$(private_file_metadata "$path")" \
+ || fail "could not inspect $description: $path"
+ IFS='|' read -r owner mode links <<< "$metadata"
+ if [[ "$owner" != "$CURRENT_UID" || "$mode" != 600 || "$links" != 1 ]]; then
+ fail "$description must be owned by the current user with mode 0600 and one hard link"
+ fi
+}
+
+require_recoverable_template_file() {
+ local recover=$1 path=$TEMPLATES_FILE parent candidate candidate_name
+ local metadata owner mode links destination_identity expected_digest actual_digest
+ local matching_alias=""
+ local -a candidates=()
+
+ require_regular_file_or_absent "$path" "the MCP stdio template path"
+ [[ -e "$path" ]] || return 0
+ metadata="$(private_file_metadata "$path")" \
+ || fail "could not inspect the MCP stdio template path: $path"
+ IFS='|' read -r owner mode links <<< "$metadata"
+ if [[ "$owner" == "$CURRENT_UID" && "$mode" == 600 && "$links" == 1 ]]; then
+ return 0
+ fi
+ if [[ "$owner" != "$CURRENT_UID" || "$mode" != 600 || "$links" != 2 ]]; then
+ fail "the MCP stdio template path has unsafe metadata"
+ fi
+
+ parent="$(dirname "$path")"
+ destination_identity="$(private_file_identity "$path")" \
+ || fail "could not inspect the MCP stdio template path"
+ shopt -s nullglob
+ candidates=("${parent}"/.executor-templates.????????)
+ shopt -u nullglob
+ for candidate in "${candidates[@]}"; do
+ [[ "$candidate" != "$path" ]] || continue
+ candidate_name="$(basename "$candidate")"
+ [[ "$candidate_name" =~ ^\.executor-templates\.[A-Za-z0-9]{8}$ ]] \
+ || continue
+ [[ -f "$candidate" && ! -L "$candidate" ]] || continue
+ if [[ "$(private_file_identity "$candidate")" == "$destination_identity" ]]; then
+ [[ -z "$matching_alias" ]] \
+ || fail "multiple MCP template recovery aliases were found"
+ matching_alias=$candidate
+ fi
+ done
+ [[ -n "$matching_alias" ]] \
+ || fail "the MCP stdio template path has an unknown hard link"
+ expected_digest="$(printf '{\n "templates": []\n}\n' | shasum -a 256 | awk '{print $1}')"
+ actual_digest="$(shasum -a 256 "$path" | awk '{print $1}')"
+ [[ "$actual_digest" == "$expected_digest" ]] \
+ || fail "the MCP template recovery alias has unexpected contents"
+
+ if [[ "$recover" == true ]]; then
+ require_private_control_file "$RECOVERY_MARKER" \
+ "the service installation recovery marker"
+ [[ "$(cat "$RECOVERY_MARKER")" == executor-service-install-recovery-v1 ]] \
+ || fail "the service installation recovery marker is malformed"
+ rm -f "$matching_alias"
+ sync
+ require_private_control_file "$path" "the MCP stdio template path"
+ fi
+}
+
+require_safe_directory_metadata() {
+ local path=$1 description=$2 owner_requirement=${3:-trusted}
+ local metadata owner mode links permissions
+
+ require_directory_or_absent "$path" "$description"
+ [[ -d "$path" ]] || fail "$description does not exist: $path"
+ metadata="$(private_file_metadata "$path")" \
+ || fail "could not inspect $description: $path"
+ IFS='|' read -r owner mode links <<< "$metadata"
+ if [[ "$owner" != 0 && "$owner" != "$CURRENT_UID" ]]; then
+ fail "$description must be owned by root or the current user: $path"
+ fi
+ if [[ "$owner_requirement" == current && "$owner" != "$CURRENT_UID" ]]; then
+ fail "$description must be owned by the current user: $path"
+ fi
+ permissions=$((8#$mode))
+ if (( permissions & 0022 )); then
+ fail "$description must not be group-writable or world-writable: $path"
+ fi
+}
+
+check_safe_directory_path() {
+ local path=$1 description=$2 create_missing=$3 current relative component
+ local -a components
+
+ [[ "$path" == /* ]] || fail "$description must be an absolute path: $path"
+ if [[ "$path" == "$HOME" ]]; then
+ require_safe_directory_metadata "$HOME" "the home directory" current
+ return
+ fi
+ if [[ "$path" == "$HOME/"* ]]; then
+ current=$HOME
+ relative=${path#"$HOME"/}
+ require_safe_directory_metadata "$current" "the home directory" current
+ else
+ current=/
+ relative=${path#/}
+ require_safe_directory_metadata "$current" "the filesystem root"
+ fi
+ IFS='/' read -r -a components <<< "$relative"
+ for component in "${components[@]}"; do
+ [[ -n "$component" && "$component" != . && "$component" != .. ]] \
+ || fail "$description contains an unsafe path component: $path"
+ current="${current%/}/${component}"
+ [[ ! -L "$current" ]] || fail "$description has a symbolic-link ancestor: $current"
+ if [[ ! -e "$current" ]]; then
+ if [[ "$create_missing" != true ]]; then
+ return
+ fi
+ if ! mkdir -m 0700 "$current"; then
+ fail "could not create $description: $current"
+ fi
+ fi
+ require_safe_directory_metadata "$current" "$description"
+ done
+}
+
+validate_safe_directory_path() {
+ check_safe_directory_path "$1" "$2" false
+}
+
+ensure_safe_directory_path() {
+ check_safe_directory_path "$1" "$2" true
+}
+
+normalize_absolute_path() {
+ local path=$1 description=$2 relative component result=""
+ local -a components
+
+ [[ "$path" == /* ]] || fail "$description must be an absolute path: $path"
+ [[ "$path" != *//* ]] \
+ || fail "$description must not contain empty path components: $path"
+ if [[ "$path" != / && "$path" == */ ]]; then
+ fail "$description must not end with an empty path component: $path"
+ fi
+ relative=${path#/}
+ if [[ -z "$relative" ]]; then
+ normalized_path=/
+ return
+ fi
+ IFS='/' read -r -a components <<< "$relative"
+ for component in "${components[@]}"; do
+ [[ -n "$component" && "$component" != . && "$component" != .. ]] \
+ || fail "$description contains an unsafe path component: $path"
+ result="${result}/${component}"
+ done
+ normalized_path=$result
+}
+
+canonicalize_path_without_creation() {
+ local path=$1 description=$2 ancestor suffix="" component physical
+
+ normalize_absolute_path "$path" "$description"
+ ancestor=$normalized_path
+ while [[ ! -d "$ancestor" ]]; do
+ component=${ancestor##*/}
+ suffix="/${component}${suffix}"
+ ancestor=${ancestor%/*}
+ [[ -n "$ancestor" ]] || ancestor=/
+ done
+ physical="$(cd "$ancestor" && pwd -P)" \
+ || fail "could not resolve $description: $path"
+ if [[ "$physical" == / ]]; then
+ canonical_path="${physical}${suffix#/}"
+ else
+ canonical_path="${physical}${suffix}"
+ fi
+}
+
+casefold_path() {
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
+}
+
+paths_overlap() {
+ local left=$1 right=$2 left_fold right_fold
+
+ canonicalize_path_without_creation "$left" "a configured path"
+ left_fold="$(casefold_path "$canonical_path")"
+ canonicalize_path_without_creation "$right" "a reserved path"
+ right_fold="$(casefold_path "$canonical_path")"
+ if [[ "$left_fold" == / || "$right_fold" == / \
+ || "$left_fold" == "$right_fold" \
+ || "$left_fold" == "$right_fold/"* \
+ || "$right_fold" == "$left_fold/"* ]]; then
+ return 0
+ fi
+ return 1
+}
+
+path_is_same_or_ancestor() {
+ local possible_ancestor=$1 path=$2 ancestor_fold path_fold
+
+ canonicalize_path_without_creation "$possible_ancestor" "a configured path"
+ ancestor_fold="$(casefold_path "$canonical_path")"
+ canonicalize_path_without_creation "$path" "a configured path"
+ path_fold="$(casefold_path "$canonical_path")"
+ [[ "$ancestor_fold" == / || "$ancestor_fold" == "$path_fold" \
+ || "$path_fold" == "$ancestor_fold/"* ]]
+}
+
+reject_path_overlap() {
+ local configured_path=$1 configured_description=$2 reserved_path=$3 reserved_description=$4
+
+ if paths_overlap "$configured_path" "$reserved_path"; then
+ fail "$configured_description must not overlap $reserved_description: $reserved_path"
+ fi
+}
+
+validate_configured_path_collisions() {
+ local service_binary="${INSTALL_ROOT}/bin/executor" reserved_path
+
+ reject_path_overlap "$DATA_DIR" "the Executor data directory" \
+ "$INSTALL_ROOT" "the managed service directory"
+ reject_path_overlap "$DATA_DIR" "the Executor data directory" \
+ "$PLIST" "the managed LaunchAgent plist"
+
+ for reserved_path in \
+ "$INSTALL_ROOT" \
+ "$service_binary" \
+ "$CONFIG_FILE" \
+ "$MANIFEST" \
+ "$RECOVERY_MARKER" \
+ "$wrapper" \
+ "$log_writer" \
+ "$PLIST"; do
+ reject_path_overlap "$TEMPLATES_FILE" "the MCP stdio template file" \
+ "$reserved_path" "a managed service path"
+ done
+
+ if path_is_same_or_ancestor "$TEMPLATES_FILE" "$DATA_DIR"; then
+ fail "the MCP stdio template file must not equal or contain the Executor data directory"
+ fi
+ for reserved_path in \
+ "${DATA_DIR}/master.key" \
+ "${DATA_DIR}/executor.db" \
+ "${DATA_DIR}/executor.db-journal" \
+ "${DATA_DIR}/executor.db-wal" \
+ "${DATA_DIR}/executor.db-shm" \
+ "${DATA_DIR}/executor.lock" \
+ "$private_log" \
+ "${private_log}.1" \
+ "${private_log}.2" \
+ "${private_log}.3"; do
+ reject_path_overlap "$TEMPLATES_FILE" "the MCP stdio template file" \
+ "$reserved_path" "a reserved Executor state path"
+ done
+}
+
+validate_encoded_config_value() {
+ local encoded=$1 description=$2 byte remainder
+
+ if (( ${#encoded} % 2 != 0 )); then
+ fail "$description has an invalid encoded length"
+ fi
+ case "$encoded" in
+ *[!0-9a-f]*) fail "$description contains invalid encoded bytes" ;;
+ esac
+ remainder=$encoded
+ while [[ -n "$remainder" ]]; do
+ byte=${remainder:0:2}
+ case "$byte" in
+ 0[0-9a-f]|1[0-9a-f]|7f)
+ fail "$description contains unsupported control bytes"
+ ;;
+ esac
+ remainder=${remainder:2}
+ done
+}
+
+decode_config_value() {
+ local encoded=$1 description=$2 byte decoded_byte
+
+ validate_encoded_config_value "$encoded" "$description"
+ decoded_config_value=""
+ while [[ -n "$encoded" ]]; do
+ byte=${encoded:0:2}
+ printf -v decoded_byte '%b' "\\x${byte}"
+ decoded_config_value="${decoded_config_value}${decoded_byte}"
+ encoded=${encoded:2}
+ done
+}
+
+encode_config_value() {
+ printf '%s' "$1" | od -An -v -tx1 | tr -d ' \n'
+}
+
+validate_safe_directory_path "$EXECUTOR_ROOT" "the Executor root directory"
+if [[ -d "$EXECUTOR_ROOT" ]]; then
+ require_safe_directory_metadata "$EXECUTOR_ROOT" "the Executor root directory" current
+fi
+validate_safe_directory_path "$INSTALL_ROOT" "the Executor service directory"
+if [[ -d "$INSTALL_ROOT" ]]; then
+ require_safe_directory_metadata "$INSTALL_ROOT" "the Executor service directory" current
+fi
+require_private_control_file "$CONFIG_FILE" "the persisted service configuration"
+require_private_control_file "$MANIFEST" "the service ownership manifest"
+require_private_control_file "$RECOVERY_MARKER" "the service installation recovery marker"
+
+config_existed=false
+DATA_DIR=$DEFAULT_DATA_DIR
+TEMPLATES_FILE="${DATA_DIR}/mcp-stdio-templates.json"
+PUBLIC_ORIGIN=""
+trusted_proxies=()
+if [[ -e "$CONFIG_FILE" ]]; then
+ config_existed=true
+ config_size="$(wc -c < "$CONFIG_FILE" | tr -d '[:space:]')"
+ case "$config_size" in
+ ''|*[!0-9]*) fail "the persisted service configuration size is invalid" ;;
+ esac
+ if (( config_size > 65536 )); then
+ fail "the persisted service configuration is too large"
+ fi
+
+ config_line_number=0
+ saw_data_dir=false
+ saw_templates_file=false
+ saw_public_origin=false
+ while IFS= read -r config_line || [[ -n "$config_line" ]]; do
+ config_line_number=$((config_line_number + 1))
+ if [[ "$config_line_number" -eq 1 ]]; then
+ [[ "$config_line" == executor-launchd-config-v1 ]] \
+ || fail "the persisted service configuration has an unsupported format"
+ continue
+ fi
+ case "$config_line" in
+ data_dir_hex=*)
+ [[ "$saw_data_dir" == false ]] \
+ || fail "the persisted service configuration repeats data_dir_hex"
+ decode_config_value "${config_line#data_dir_hex=}" "the persisted data directory"
+ DATA_DIR=$decoded_config_value
+ saw_data_dir=true
+ ;;
+ templates_file_hex=*)
+ [[ "$saw_templates_file" == false ]] \
+ || fail "the persisted service configuration repeats templates_file_hex"
+ decode_config_value "${config_line#templates_file_hex=}" \
+ "the persisted template file"
+ TEMPLATES_FILE=$decoded_config_value
+ saw_templates_file=true
+ ;;
+ public_origin_hex=*)
+ [[ "$saw_public_origin" == false ]] \
+ || fail "the persisted service configuration repeats public_origin_hex"
+ decode_config_value "${config_line#public_origin_hex=}" \
+ "the persisted public origin"
+ PUBLIC_ORIGIN=$decoded_config_value
+ saw_public_origin=true
+ ;;
+ trusted_proxy_hex=*)
+ decode_config_value "${config_line#trusted_proxy_hex=}" \
+ "a persisted trusted proxy"
+ [[ -n "$decoded_config_value" ]] \
+ || fail "persisted trusted proxy entries cannot be empty"
+ trusted_proxies+=("$decoded_config_value")
+ ;;
+ *) fail "the persisted service configuration contains an unknown field" ;;
+ esac
+ done < "$CONFIG_FILE"
+ [[ "$config_line_number" -gt 0 && "$saw_data_dir" == true \
+ && "$saw_templates_file" == true && "$saw_public_origin" == true ]] \
+ || fail "the persisted service configuration is incomplete"
+fi
+
+if [[ "$data_dir_explicit" == true ]]; then
+ DATA_DIR=$data_dir_override
+fi
+if [[ "$templates_explicit" == true ]]; then
+ TEMPLATES_FILE=$templates_override
+elif [[ "$config_existed" == false && "$data_dir_explicit" == true ]]; then
+ TEMPLATES_FILE="${DATA_DIR}/mcp-stdio-templates.json"
+fi
+if [[ "$public_origin_explicit" == true ]]; then
+ PUBLIC_ORIGIN=$public_origin_override
+fi
+if [[ "$trusted_proxies_explicit" == true ]]; then
+ trusted_proxies=()
+ if [[ -n "$trusted_proxies_override" ]]; then
+ case "$trusted_proxies_override" in
+ ,*|*,|*,,*) fail "trusted proxy entries cannot be empty" ;;
+ esac
+ IFS=',' read -r -a environment_proxies <<< "$trusted_proxies_override"
+ trusted_proxies+=("${environment_proxies[@]}")
+ fi
+ trusted_proxies+=("${trusted_proxy_options[@]}")
+fi
+
+[[ -n "$DATA_DIR" ]] || fail "the Executor data directory cannot be empty"
+[[ -n "$TEMPLATES_FILE" ]] || fail "the MCP stdio template file cannot be empty"
+wrapper="${INSTALL_ROOT}/run-launchd.sh"
+log_writer="${INSTALL_ROOT}/bounded-log.sh"
+for value in \
+ "$EXECUTOR_BINARY" \
+ "$DATA_DIR" \
+ "$TEMPLATES_FILE" \
+ "$PUBLIC_ORIGIN" \
+ "$wrapper"; do
+ encoded_value="$(encode_config_value "$value")"
+ validate_encoded_config_value "$encoded_value" "a service path or origin"
+ case "$value" in
+ *'&'*|*'<'*|*'>'*|*'|'*|*'\'*)
+ fail "service paths and origins cannot contain XML or template metacharacters"
+ ;;
+ esac
+done
+for trusted_proxy in "${trusted_proxies[@]}"; do
+ [[ -n "$trusted_proxy" ]] || fail "trusted proxy entries cannot be empty"
+ encoded_value="$(encode_config_value "$trusted_proxy")"
+ validate_encoded_config_value "$encoded_value" "a trusted proxy"
+done
+
+normalize_absolute_path "$DATA_DIR" "the Executor data directory"
+DATA_DIR=$normalized_path
+normalize_absolute_path "$TEMPLATES_FILE" "the MCP stdio template file"
+TEMPLATES_FILE=$normalized_path
+templates_directory="$(dirname "$TEMPLATES_FILE")"
+plist_directory="$(dirname "$PLIST")"
+validate_safe_directory_path "$DATA_DIR" "the Executor data directory"
+validate_safe_directory_path "$templates_directory" "the MCP template directory"
+validate_safe_directory_path "$plist_directory" "the LaunchAgents directory"
+canonicalize_path_without_creation "$DATA_DIR" "the Executor data directory"
+DATA_DIR=$canonical_path
+canonicalize_path_without_creation "$TEMPLATES_FILE" "the MCP stdio template file"
+TEMPLATES_FILE=$canonical_path
+templates_directory="$(dirname "$TEMPLATES_FILE")"
+private_log="${DATA_DIR}/executor.log"
+validate_configured_path_collisions
+require_recoverable_template_file false
+require_regular_file_or_absent "$wrapper" "the LaunchAgent wrapper path"
+require_regular_file_or_absent "$log_writer" "the bounded log helper path"
+require_regular_file_or_absent "$private_log" "the private log path"
+for generation in {1..3}; do
+ require_regular_file_or_absent "${private_log}.${generation}" \
+ "the rotated private log path"
+done
+require_regular_file_or_absent "$PLIST" "the LaunchAgent plist path"
+
+if [[ "$validate_only" == true ]]; then
+ printf 'LaunchAgent paths and configuration are valid.\n'
+ exit 0
+fi
+
+ensure_safe_directory_path "$EXECUTOR_ROOT" "the Executor root directory"
+require_safe_directory_metadata "$EXECUTOR_ROOT" "the Executor root directory" current
+chmod 0700 "$EXECUTOR_ROOT"
+ensure_safe_directory_path "$INSTALL_ROOT" "the Executor service directory"
+require_safe_directory_metadata "$INSTALL_ROOT" "the Executor service directory" current
+chmod 0700 "$INSTALL_ROOT"
+require_private_control_file "$CONFIG_FILE" "the persisted service configuration"
+require_private_control_file "$MANIFEST" "the service ownership manifest"
+require_private_control_file "$RECOVERY_MARKER" "the service installation recovery marker"
+
+ensure_safe_directory_path "$DATA_DIR" "the Executor data directory"
+require_safe_directory_metadata "$DATA_DIR" "the Executor data directory" current
+ensure_safe_directory_path "$templates_directory" "the MCP template directory"
+ensure_safe_directory_path "$plist_directory" "the LaunchAgents directory"
+chmod 0700 "$DATA_DIR"
+DATA_DIR="$(cd "$DATA_DIR" && pwd -P)"
+TEMPLATES_FILE="$(absolute_path "$TEMPLATES_FILE")"
+
+private_log="${DATA_DIR}/executor.log"
+validate_configured_path_collisions
+require_recoverable_template_file false
+require_regular_file_or_absent "$wrapper" "the LaunchAgent wrapper path"
+require_regular_file_or_absent "$log_writer" "the bounded log helper path"
+require_regular_file_or_absent "$private_log" "the private log path"
+for generation in {1..3}; do
+ require_regular_file_or_absent "${private_log}.${generation}" \
+ "the rotated private log path"
+done
+require_regular_file_or_absent "$PLIST" "the LaunchAgent plist path"
+
+if [[ -e "$RECOVERY_MARKER" ]]; then
+ recovery_contents="$(cat "$RECOVERY_MARKER")"
+ [[ "$recovery_contents" == executor-service-install-recovery-v1 ]] \
+ || fail "the service installation recovery marker is malformed"
+else
+ temporary_recovery="$(mktemp "${INSTALL_ROOT}/.service-recovery.XXXXXXXX")"
+ trap 'rm -f "$temporary_recovery"' EXIT
+ printf 'executor-service-install-recovery-v1\n' > "$temporary_recovery"
+ chmod 0600 "$temporary_recovery"
+ sync
+ mv "$temporary_recovery" "$RECOVERY_MARKER"
+ trap - EXIT
+ sync
+fi
+
+require_recoverable_template_file true
+
+temporary_config="$(mktemp "${INSTALL_ROOT}/.service-config.XXXXXXXX")"
+trap 'rm -f "$temporary_config"' EXIT
+{
+ printf 'executor-launchd-config-v1\n'
+ printf 'data_dir_hex=%s\n' "$(encode_config_value "$DATA_DIR")"
+ printf 'templates_file_hex=%s\n' "$(encode_config_value "$TEMPLATES_FILE")"
+ printf 'public_origin_hex=%s\n' "$(encode_config_value "$PUBLIC_ORIGIN")"
+ for trusted_proxy in "${trusted_proxies[@]}"; do
+ printf 'trusted_proxy_hex=%s\n' "$(encode_config_value "$trusted_proxy")"
+ done
+} > "$temporary_config"
+chmod 0600 "$temporary_config"
+sync
+mv -f "$temporary_config" "$CONFIG_FILE"
+trap - EXIT
+sync
+
+if [[ ! -e "$TEMPLATES_FILE" && ! -L "$TEMPLATES_FILE" ]]; then
+ temporary_templates="$(mktemp "${templates_directory}/.executor-templates.XXXXXXXX")"
+ trap 'rm -f "$temporary_templates"' EXIT
+ printf '{\n "templates": []\n}\n' > "$temporary_templates"
+ chmod 0600 "$temporary_templates"
+ sync
+ if ! ln "$temporary_templates" "$TEMPLATES_FILE"; then
+ fail "the MCP stdio template path appeared during installation"
+ fi
+ sync
+ if [[ ${EXECUTOR_INSTALL_TEST_CRASH_AFTER_TEMPLATE_LINK:-} == 1 ]]; then
+ kill -KILL "${BASHPID}"
+ fi
+ rm -f "$temporary_templates"
+ if [[ ${EXECUTOR_INSTALL_TEST_CRASH_AFTER_TEMPLATE_UNLINK:-} == 1 ]]; then
+ kill -KILL "${BASHPID}"
+ fi
+ sync
+ trap - EXIT
+fi
+[[ -f "$TEMPLATES_FILE" && ! -L "$TEMPLATES_FILE" ]] \
+ || fail "the MCP stdio template path must be a regular file"
+chmod 0600 "$TEMPLATES_FILE"
+require_private_control_file "$TEMPLATES_FILE" "the MCP stdio template path"
+
+temporary_log_writer="$(mktemp "${log_writer}.XXXXXXXX")"
+trap 'rm -f "$temporary_log_writer"' EXIT
+cp "$LOG_WRITER_SOURCE" "$temporary_log_writer"
+chmod 0700 "$temporary_log_writer"
+mv -f "$temporary_log_writer" "$log_writer"
+trap - EXIT
+
+temporary_wrapper="$(mktemp "${wrapper}.XXXXXXXX")"
+trap 'rm -f "$temporary_wrapper"' EXIT
+{
+ printf '#!/bin/bash\nset -euo pipefail\nexec '
+ printf '%q ' "$EXECUTOR_BINARY" server --data-dir "$DATA_DIR" \
+ --mcp-stdio-templates "$TEMPLATES_FILE"
+ if [[ -n "$PUBLIC_ORIGIN" ]]; then
+ printf '%q ' --public-origin "$PUBLIC_ORIGIN"
+ fi
+ if [[ ${#trusted_proxies[@]} -gt 0 ]]; then
+ for trusted_proxy in "${trusted_proxies[@]}"; do
+ [[ -n "$trusted_proxy" ]] || fail "trusted proxy entries cannot be empty"
+ printf '%q ' --trusted-proxy "$trusted_proxy"
+ done
+ fi
+ printf '> >(%q %q) 2>&1\n' "$log_writer" "$private_log"
+} > "$temporary_wrapper"
+chmod 0700 "$temporary_wrapper"
+mv -f "$temporary_wrapper" "$wrapper"
+trap - EXIT
+
+temporary_plist="$(mktemp "${PLIST}.XXXXXXXX")"
+trap 'rm -f "$temporary_plist"' EXIT
+sed -e "s|@EXECUTOR_WRAPPER@|${wrapper}|g" "$TEMPLATE" > "$temporary_plist"
+
+plutil -lint "$temporary_plist" >/dev/null
+chmod 0600 "$temporary_plist"
+mv -f "$temporary_plist" "$PLIST"
+trap - EXIT
+
+binary_hash="$(shasum -a 256 "$EXECUTOR_BINARY" | awk '{print $1}')"
+plist_hash="$(shasum -a 256 "$PLIST" | awk '{print $1}')"
+wrapper_hash="$(shasum -a 256 "$wrapper" | awk '{print $1}')"
+logger_hash="$(shasum -a 256 "$log_writer" | awk '{print $1}')"
+config_hash="$(shasum -a 256 "$CONFIG_FILE" | awk '{print $1}')"
+temporary_manifest="$(mktemp "${INSTALL_ROOT}/.service-install.XXXXXXXX")"
+trap 'rm -f "$temporary_manifest"' EXIT
+{
+ printf 'executor-service-install-v1\n'
+ printf 'binary %s\n' "$binary_hash"
+ printf 'plist %s\n' "$plist_hash"
+ printf 'wrapper %s\n' "$wrapper_hash"
+ printf 'logger %s\n' "$logger_hash"
+ printf 'config %s\n' "$config_hash"
+} > "$temporary_manifest"
+chmod 0600 "$temporary_manifest"
+sync
+mv -f "$temporary_manifest" "$MANIFEST"
+trap - EXIT
+sync
+rm -f "$RECOVERY_MARKER"
+sync
+
+domain="gui/$(id -u)"
+launchctl bootout "$domain/$LABEL" >/dev/null 2>&1 || true
+if [[ "$start_service" == "true" ]]; then
+ launchctl enable "$domain/$LABEL"
+ launchctl bootstrap "$domain" "$PLIST"
+ ready=false
+ previous_pid=""
+ consecutive_checks=0
+ for _ in {1..30}; do
+ current_pid="$(launchctl print "$domain/$LABEL" 2>/dev/null \
+ | awk '/^[[:space:]]*pid = [0-9]+$/ { print $3; exit }')"
+ if [[ -n "$current_pid" ]] \
+ && curl --fail --silent --show-error --noproxy '*' \
+ --connect-timeout 1 --max-time 1 \
+ http://127.0.0.1:4788/healthz >/dev/null 2>&1; then
+ if [[ "$current_pid" == "$previous_pid" ]]; then
+ consecutive_checks=$((consecutive_checks + 1))
+ else
+ consecutive_checks=1
+ previous_pid="$current_pid"
+ fi
+ if [[ "$consecutive_checks" -ge 2 ]]; then
+ ready=true
+ break
+ fi
+ else
+ consecutive_checks=0
+ previous_pid=""
+ fi
+ sleep 1
+ done
+ if [[ "$ready" != "true" ]]; then
+ launchctl print "$domain/$LABEL" >&2 || true
+ launchctl bootout "$domain/$LABEL" >/dev/null 2>&1 || true
+ fail "Executor did not become healthy within 30 seconds"
+ fi
+ printf 'Executor is running at http://127.0.0.1:4788\n'
+ printf 'Read first-boot setup and logs at %s\n' "$private_log"
+else
+ printf 'Installed %s\n' "$PLIST"
+ printf 'Load it with: launchctl bootstrap %s %q\n' "$domain" "$PLIST"
+fi
diff --git a/scripts/install-systemd.sh b/scripts/install-systemd.sh
new file mode 100755
index 000000000..e6d205655
--- /dev/null
+++ b/scripts/install-systemd.sh
@@ -0,0 +1,362 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly APP_USER="executor"
+readonly APP_GROUP="executor"
+readonly BINARY_TARGET="/usr/local/bin/executor"
+readonly CONFIG_DIR="/etc/executor"
+readonly DATA_DIR="/var/lib/executor"
+readonly MASTER_KEY_STAGING_PARENT="${DATA_DIR%/*}"
+readonly UNIT_TARGET="/etc/systemd/system/executor.service"
+readonly MANIFEST_TARGET="${CONFIG_DIR}/service-install.manifest"
+readonly RECOVERY_TARGET="${CONFIG_DIR}/service-install.recovery"
+readonly SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
+readonly REPO_DIR=$(cd -- "${SCRIPT_DIR}/.." && pwd -P)
+readonly UNIT_SOURCE="${REPO_DIR}/packaging/systemd/executor.service"
+readonly ENV_SOURCE="${REPO_DIR}/packaging/systemd/executor.env.example"
+source "${SCRIPT_DIR}/lib/install-systemd-master-key.sh"
+
+validate_systemd_path_layout() {
+ local persistent_path persistent_root managed_path
+
+ for persistent_path in \
+ "${DATA_DIR}/master.key" \
+ "${CONFIG_DIR}/executor.env" \
+ "${CONFIG_DIR}/mcp-stdio-templates.json"; do
+ for managed_path in \
+ "${BINARY_TARGET}" \
+ "${UNIT_TARGET}" \
+ "${MANIFEST_TARGET}" \
+ "${RECOVERY_TARGET}"; do
+ executor_require_disjoint_paths \
+ "${persistent_path}" "persistent Executor state" \
+ "${managed_path}" "a managed service path"
+ done
+ done
+ for persistent_root in "${DATA_DIR}" "${CONFIG_DIR}"; do
+ executor_require_disjoint_paths \
+ "${persistent_root}" "an Executor state directory" \
+ "${BINARY_TARGET}" "the managed service binary"
+ executor_require_disjoint_paths \
+ "${persistent_root}" "an Executor state directory" \
+ "${UNIT_TARGET}" "the managed systemd unit"
+ done
+}
+
+usage() {
+ cat <<'EOF'
+Install Executor as a hardened systemd service.
+
+Usage: sudo scripts/install-systemd.sh [--binary /path/to/executor] [--no-start]
+
+Options:
+ --binary PATH Install this binary. Defaults to executor from PATH.
+ --no-start Install and enable the unit without starting it.
+ -h, --help Show this help.
+
+The installer never replaces an existing master key, environment file, or MCP
+stdio template registry.
+EOF
+}
+
+binary_source=""
+start_service=true
+while (($# > 0)); do
+ case "$1" in
+ --binary)
+ if (($# < 2)); then
+ echo "--binary requires a path" >&2
+ exit 2
+ fi
+ binary_source=$2
+ shift 2
+ ;;
+ --no-start)
+ start_service=false
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "unknown option: $1" >&2
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+validate_systemd_path_layout
+
+if [[ ${EUID} -ne 0 ]]; then
+ echo "run this installer as root, for example with sudo" >&2
+ exit 1
+fi
+if ! command -v systemctl >/dev/null 2>&1; then
+ echo "systemctl is required" >&2
+ exit 1
+fi
+if ! command -v sha256sum >/dev/null 2>&1; then
+ echo "sha256sum is required" >&2
+ exit 1
+fi
+if [[ ${start_service} == true ]] && ! command -v curl >/dev/null 2>&1; then
+ echo "curl is required for the startup readiness check" >&2
+ exit 1
+fi
+
+service_was_active=false
+service_is_quiescent=false
+service_lifecycle_managed=false
+installation_finished=false
+manifest_temporary=""
+recovery_temporary=""
+
+on_installer_exit() {
+ local status=$?
+ trap - EXIT
+ if [[ -n ${manifest_temporary} ]]; then
+ rm -f -- "${manifest_temporary}"
+ fi
+ if [[ -n ${recovery_temporary} ]]; then
+ rm -f -- "${recovery_temporary}"
+ fi
+ if ((status != 0)) && [[ ${service_lifecycle_managed} == true \
+ && ${installation_finished} != true ]]; then
+ if [[ ${service_is_quiescent} != true ]]; then
+ if systemctl stop executor.service >/dev/null 2>&1; then
+ service_is_quiescent=true
+ else
+ echo "installation failed and executor.service could not be stopped" >&2
+ echo "inspect its status before retrying the installation" >&2
+ fi
+ fi
+ if [[ ${service_is_quiescent} == true ]]; then
+ echo "installation failed; executor.service was left stopped" >&2
+ echo "fix the reported problem, then rerun the installer or start the service manually" >&2
+ fi
+ fi
+ exit "${status}"
+}
+trap on_installer_exit EXIT
+
+service_state=$(systemctl is-active executor.service 2>/dev/null || true)
+case "${service_state}" in
+ active|activating|reloading|deactivating)
+ service_was_active=true
+ if ! systemctl stop executor.service; then
+ echo "could not stop executor.service before inspecting its state" >&2
+ exit 1
+ fi
+ service_is_quiescent=true
+ ;;
+ inactive|failed|unknown)
+ service_is_quiescent=true
+ ;;
+ *)
+ echo "could not determine whether executor.service is active" >&2
+ exit 1
+ ;;
+esac
+service_lifecycle_managed=true
+
+if [[ -z ${binary_source} ]]; then
+ binary_source=$(command -v executor || true)
+fi
+if [[ -z ${binary_source} || ! -f ${binary_source} || ! -x ${binary_source} ]]; then
+ echo "an executable Executor binary is required; pass --binary PATH" >&2
+ exit 1
+fi
+binary_source=$(cd -- "$(dirname -- "${binary_source}")" && pwd -P)/$(basename -- "${binary_source}")
+
+if ! getent group "${APP_GROUP}" >/dev/null; then
+ groupadd --system "${APP_GROUP}"
+fi
+if ! id -u "${APP_USER}" >/dev/null 2>&1; then
+ useradd \
+ --system \
+ --gid "${APP_GROUP}" \
+ --home-dir "${DATA_DIR}" \
+ --shell "$(command -v nologin || echo /usr/sbin/nologin)" \
+ "${APP_USER}"
+fi
+IFS=: read -r _ _ app_uid _ _ app_home app_shell \
+ <<< "$(getent passwd "${APP_USER}")"
+app_primary_group=$(id -gn "${APP_USER}")
+password_status=$(passwd -S "${APP_USER}" 2>/dev/null | awk '{print $2}')
+if [[ ${app_uid} -eq 0 \
+ || ${app_home} != "${DATA_DIR}" \
+ || ! ${app_shell} =~ /(nologin|false)$ \
+ || ${app_primary_group} != "${APP_GROUP}" \
+ || ! ${password_status} =~ ^(L|LK)$ ]]; then
+ echo "the executor account exists but is not the expected locked system account" >&2
+ echo "refusing to grant it access to Executor state" >&2
+ exit 1
+fi
+
+for managed_dir in "${CONFIG_DIR}" "${DATA_DIR}"; do
+ executor_require_safe_directory "${managed_dir}"
+done
+for managed_file in \
+ "${BINARY_TARGET}" \
+ "${UNIT_TARGET}" \
+ "${CONFIG_DIR}/executor.env" \
+ "${CONFIG_DIR}/mcp-stdio-templates.json" \
+ "${MANIFEST_TARGET}" \
+ "${RECOVERY_TARGET}" \
+ "${DATA_DIR}/master.key"; do
+ executor_require_regular_file_or_absent "${managed_file}"
+done
+if [[ -e ${MANIFEST_TARGET} ]]; then
+ manifest_uid=$(stat -c '%u' -- "${MANIFEST_TARGET}")
+ manifest_gid=$(stat -c '%g' -- "${MANIFEST_TARGET}")
+ manifest_mode=$(stat -c '%a' -- "${MANIFEST_TARGET}")
+ manifest_links=$(stat -c '%h' -- "${MANIFEST_TARGET}")
+ if [[ ${manifest_uid} -ne 0 || ${manifest_gid} -ne 0 \
+ || ${manifest_mode} != 600 || ${manifest_links} -ne 1 ]]; then
+ echo "the service ownership manifest has unsafe metadata" >&2
+ exit 1
+ fi
+fi
+if [[ -e ${RECOVERY_TARGET} ]]; then
+ recovery_uid=$(stat -c '%u' -- "${RECOVERY_TARGET}")
+ recovery_gid=$(stat -c '%g' -- "${RECOVERY_TARGET}")
+ recovery_mode=$(stat -c '%a' -- "${RECOVERY_TARGET}")
+ recovery_links=$(stat -c '%h' -- "${RECOVERY_TARGET}")
+ recovery_contents=$(cat -- "${RECOVERY_TARGET}")
+ if [[ ${recovery_uid} -ne 0 || ${recovery_gid} -ne 0 \
+ || ${recovery_mode} != 600 || ${recovery_links} -ne 1 \
+ || ${recovery_contents} != executor-service-install-recovery-v1 ]]; then
+ echo "the service installation recovery marker is unsafe or malformed" >&2
+ exit 1
+ fi
+fi
+
+install -d -o root -g "${APP_GROUP}" -m 0750 "${CONFIG_DIR}"
+install -d -o "${APP_USER}" -g "${APP_GROUP}" -m 0700 "${DATA_DIR}"
+
+if [[ ! -e ${CONFIG_DIR}/executor.env ]]; then
+ install -o root -g "${APP_GROUP}" -m 0640 \
+ "${ENV_SOURCE}" "${CONFIG_DIR}/executor.env"
+fi
+if [[ ! -e ${CONFIG_DIR}/mcp-stdio-templates.json ]]; then
+ umask 0027
+ printf '{\n "templates": []\n}\n' > "${CONFIG_DIR}/mcp-stdio-templates.json"
+ chown root:"${APP_GROUP}" "${CONFIG_DIR}/mcp-stdio-templates.json"
+ chmod 0640 "${CONFIG_DIR}/mcp-stdio-templates.json"
+fi
+if [[ ( -e ${DATA_DIR}/executor.db || -L ${DATA_DIR}/executor.db ) \
+ && ! -e ${DATA_DIR}/master.key \
+ && ! -L ${DATA_DIR}/master.key ]]; then
+ echo "an existing Executor database has no master key; restore the original key" >&2
+ exit 1
+fi
+app_gid=$(id -g "${APP_USER}")
+executor_ensure_master_key \
+ "${MASTER_KEY_STAGING_PARENT}" "${DATA_DIR}" \
+ "${DATA_DIR}/master.key" "${app_uid}" "${app_gid}"
+
+chown root:"${APP_GROUP}" \
+ "${CONFIG_DIR}/executor.env" \
+ "${CONFIG_DIR}/mcp-stdio-templates.json"
+chmod 0640 \
+ "${CONFIG_DIR}/executor.env" \
+ "${CONFIG_DIR}/mcp-stdio-templates.json"
+
+if [[ ! -e ${RECOVERY_TARGET} ]]; then
+ recovery_temporary=$(mktemp "${CONFIG_DIR}/.service-recovery.XXXXXXXX")
+ printf 'executor-service-install-recovery-v1\n' > "${recovery_temporary}"
+ chown root:root "${recovery_temporary}"
+ chmod 0600 "${recovery_temporary}"
+ sync -f "${recovery_temporary}"
+ mv -- "${recovery_temporary}" "${RECOVERY_TARGET}"
+ recovery_temporary=""
+ sync -f "${RECOVERY_TARGET}"
+fi
+
+if [[ ${binary_source} != "${BINARY_TARGET}" ]]; then
+ install -o root -g root -m 0755 "${binary_source}" "${BINARY_TARGET}"
+else
+ chown root:root "${BINARY_TARGET}"
+ chmod 0755 "${BINARY_TARGET}"
+fi
+install -o root -g root -m 0644 "${UNIT_SOURCE}" "${UNIT_TARGET}"
+executor_require_filesystem_sync \
+ "${BINARY_TARGET}" "the installed Executor binary"
+executor_require_filesystem_sync \
+ "${BINARY_TARGET%/*}" "the Executor binary directory"
+executor_require_filesystem_sync \
+ "${UNIT_TARGET}" "the installed systemd unit"
+executor_require_filesystem_sync \
+ "${UNIT_TARGET%/*}" "the systemd unit directory"
+
+binary_hash=$(sha256sum -- "${BINARY_TARGET}" | awk '{print $1}')
+unit_hash=$(sha256sum -- "${UNIT_TARGET}" | awk '{print $1}')
+manifest_temporary=$(mktemp "${CONFIG_DIR}/.service-install.XXXXXXXX")
+{
+ printf 'executor-service-install-v1\n'
+ printf 'binary %s\n' "${binary_hash}"
+ printf 'unit %s\n' "${unit_hash}"
+} > "${manifest_temporary}"
+chown root:root "${manifest_temporary}"
+chmod 0600 "${manifest_temporary}"
+sync -f "${manifest_temporary}"
+mv -f -- "${manifest_temporary}" "${MANIFEST_TARGET}"
+manifest_temporary=""
+sync -f "${MANIFEST_TARGET}"
+rm -f -- "${RECOVERY_TARGET}"
+sync -f "${CONFIG_DIR}"
+
+systemctl daemon-reload
+systemctl enable executor.service
+if [[ ${start_service} == true ]]; then
+ service_is_quiescent=false
+ if ! systemctl start executor.service; then
+ systemctl status --no-pager executor.service || true
+ exit 1
+ fi
+ ready=false
+ healthy_checks=0
+ last_main_pid=0
+ for _ in {1..30}; do
+ main_pid=$(systemctl show --property MainPID --value executor.service)
+ if systemctl is-active --quiet executor.service \
+ && [[ ${main_pid} =~ ^[1-9][0-9]*$ ]] \
+ && curl --noproxy '*' --fail --silent --max-time 1 \
+ http://127.0.0.1:4788/healthz >/dev/null; then
+ if [[ ${main_pid} == "${last_main_pid}" ]]; then
+ ((healthy_checks += 1))
+ else
+ healthy_checks=1
+ last_main_pid=${main_pid}
+ fi
+ if ((healthy_checks >= 2)); then
+ ready=true
+ break
+ fi
+ else
+ healthy_checks=0
+ last_main_pid=0
+ fi
+ if systemctl is-failed --quiet executor.service; then
+ break
+ fi
+ sleep 1
+ done
+ if [[ ${ready} != true ]]; then
+ echo "Executor did not become ready" >&2
+ systemctl status --no-pager executor.service || true
+ journalctl --unit executor.service --lines 30 --no-pager || true
+ exit 1
+ fi
+ installation_finished=true
+ echo "Executor is ready at http://127.0.0.1:4788"
+else
+ installation_finished=true
+ if [[ ${service_was_active} == true ]]; then
+ echo "Executor was stopped and remains stopped because --no-start was supplied."
+ fi
+ echo "Executor is installed. Start it with: systemctl start executor.service"
+fi
+echo "Follow logs with: journalctl -u executor.service -f"
diff --git a/scripts/install.sh b/scripts/install.sh
index 345743148..eda8da73a 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -1,12 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
-APP=executor
-REPO=RhysSullivan/executor
-MUTED='\033[0;2m'
-RED='\033[0;31m'
-ORANGE='\033[38;5;214m'
-NC='\033[0m'
+APP="executor"
+REPOSITORY="${EXECUTOR_REPOSITORY:-davis7dotsh/executor-fork-testing}"
+INSTALL_DIR="${EXECUTOR_INSTALL_DIR:-$HOME/.executor/bin}"
+MANIFEST_NAME=".executor-install-manifest"
+PATH_OWNERSHIP_NAME=".executor-path-ownership"
+RECOVERY_NAME=".executor-install-recovery"
+LOCK_NAME=".executor-install-lock"
+requested_version="${VERSION:-}"
+binary_path=""
+local_archive_path=""
+local_checksum_path=""
+no_modify_path=false
+uninstall=false
+version_option=false
+manifest_present=false
+manifest_digest=""
+manifest_executor_hash=""
+manifest_license_hash=""
+manifest_rust_notices_hash=""
+manifest_javascript_notices_hash=""
+manifest_path_ownership_hash=""
+install_source_executor=""
+install_source_license=""
+install_source_rust_notices=""
+install_source_javascript_notices=""
+record_path_config=false
+path_update_requested=false
+path_already_present=false
+recorded_config_file=""
+recorded_path_command=""
+path_edit_changed=false
+path_edit_error=""
+path_edit_lock_acquired=false
+path_edit_lock_digest=""
+path_edit_lock_identity=""
+path_edit_lock_path=""
+path_edit_parent=""
+path_edit_parent_identity=""
+path_edit_result=""
+trusted_temp_root=""
+trusted_temp_root_identity=""
+temporary_directory=""
+temporary_directory_identity=""
+test_swapped_temp_directory=""
+install_lock_acquired=false
+install_lock_digest=""
+install_lock_identity=""
+install_root_identity=""
+install_root_parent=""
+install_root_parent_identity=""
+recovery_executor_old=""
+recovery_executor_new=""
+recovery_license_old=""
+recovery_license_new=""
+recovery_rust_notices_old=""
+recovery_rust_notices_new=""
+recovery_javascript_notices_old=""
+recovery_javascript_notices_new=""
+recovery_path_ownership_old=""
+recovery_path_ownership_new=""
usage() {
cat < Install a specific version (e.g. 1.4.12)
+ -v, --version Install a specific version, such as 2.0.0
-b, --binary Install from a local binary instead of downloading
- --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
+ -a, --archive Install a release archive from disk
+ --checksum Checksum sidecar for --archive (default: .sha256)
+ --no-modify-path Do not modify shell configuration files
+ --uninstall Remove installer-owned files and PATH entry
+
+Environment:
+ EXECUTOR_INSTALL_DIR Binary directory (default: \$HOME/.executor/bin)
+ EXECUTOR_REPOSITORY GitHub owner/repository for downloads
+ (default: davis7dotsh/executor-fork-testing)
+ VERSION Version to install when --version is omitted
Examples:
- curl -fsSL https://raw.githubusercontent.com/${REPO}/main/scripts/install.sh | bash
- curl -fsSL https://raw.githubusercontent.com/${REPO}/main/scripts/install.sh | bash -s -- --version 1.4.12
- ./install.sh --binary /path/to/executor
+ curl -fsSL https://raw.githubusercontent.com/${REPOSITORY}/main/scripts/install.sh | bash
+ curl -fsSL https://raw.githubusercontent.com/${REPOSITORY}/main/scripts/install.sh | bash -s -- --version 2.0.0
+ curl -fsSL https://raw.githubusercontent.com/${REPOSITORY}/main/scripts/install.sh | bash -s -- --uninstall
+ ./scripts/install.sh --binary ./target/release/executor
EOF
}
-requested_version=${VERSION:-}
-no_modify_path=false
-binary_path=""
+fail() {
+ printf 'Error: %s\n' "$1" >&2
+ exit 1
+}
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -38,250 +103,1855 @@ while [[ $# -gt 0 ]]; do
exit 0
;;
-v|--version)
- if [[ -n "${2:-}" ]]; then
- requested_version="$2"
- shift 2
- else
- echo -e "${RED}Error: --version requires a version argument${NC}" >&2
- exit 1
- fi
+ [[ -n "${2:-}" ]] || fail "--version requires a version"
+ requested_version="$2"
+ version_option=true
+ shift 2
;;
-b|--binary)
- if [[ -n "${2:-}" ]]; then
- binary_path="$2"
- shift 2
- else
- echo -e "${RED}Error: --binary requires a path argument${NC}" >&2
- exit 1
- fi
+ [[ -n "${2:-}" ]] || fail "--binary requires a path"
+ binary_path="$2"
+ shift 2
+ ;;
+ -a|--archive)
+ [[ -n "${2:-}" ]] || fail "--archive requires a path"
+ local_archive_path="$2"
+ shift 2
+ ;;
+ --checksum)
+ [[ -n "${2:-}" ]] || fail "--checksum requires a path"
+ local_checksum_path="$2"
+ shift 2
;;
--no-modify-path)
no_modify_path=true
shift
;;
- *)
- echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
+ --uninstall)
+ uninstall=true
shift
;;
+ *)
+ fail "unknown option: $1"
+ ;;
esac
done
-INSTALL_DIR="${EXECUTOR_INSTALL_DIR:-$HOME/.executor/bin}"
-mkdir -p "$INSTALL_DIR"
-
-print_message() {
- local level=$1 message=$2 color=""
- case "$level" in
- info) color="${NC}" ;;
- warning) color="${ORANGE}" ;;
- error) color="${RED}" ;;
+if [[ -n "$binary_path" && -n "$local_archive_path" ]]; then
+ fail "--binary and --archive cannot be used together"
+fi
+if [[ -n "$local_checksum_path" && -z "$local_archive_path" ]]; then
+ fail "--checksum requires --archive"
+fi
+if [[ "$uninstall" == "true" ]] \
+ && [[ "$version_option" == "true" \
+ || -n "$binary_path" \
+ || -n "$local_archive_path" \
+ || -n "$local_checksum_path" ]]; then
+ fail "--uninstall cannot be combined with install source options"
+fi
+
+[[ "$INSTALL_DIR" == /* ]] || fail "EXECUTOR_INSTALL_DIR must be an absolute path"
+[[ "$INSTALL_DIR" != "/" ]] || fail "EXECUTOR_INSTALL_DIR cannot be the filesystem root"
+case "$INSTALL_DIR" in
+ *:*|*$'\n'*|*$'\r'*) fail "EXECUTOR_INSTALL_DIR cannot contain colons or line breaks" ;;
+esac
+
+resolve_path_config() {
+ local current_shell quoted_install_dir
+ current_shell="$(basename "${SHELL:-bash}")"
+ case "$current_shell" in
+ fish)
+ config_file="$HOME/.config/fish/config.fish"
+ quoted_install_dir=${INSTALL_DIR//\\/\\\\}
+ quoted_install_dir=${quoted_install_dir//\'/\\\'}
+ path_command="fish_add_path -- '$quoted_install_dir'"
+ ;;
+ zsh)
+ config_file="${ZDOTDIR:-$HOME}/.zshrc"
+ printf -v quoted_install_dir '%q' "$INSTALL_DIR"
+ path_command="export PATH=$quoted_install_dir:\$PATH"
+ ;;
+ *)
+ config_file="$HOME/.bashrc"
+ printf -v quoted_install_dir '%q' "$INSTALL_DIR"
+ path_command="export PATH=$quoted_install_dir:\$PATH"
+ ;;
esac
- echo -e "${color}${message}${NC}"
}
-if [[ -n "$binary_path" ]]; then
- if [[ ! -f "$binary_path" ]]; then
- print_message error "Error: binary not found at $binary_path"
- exit 1
+file_has_exact_line() {
+ local file=$1 expected=$2 line normalized
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ normalized="${line%$'\r'}"
+ if [[ "$normalized" == "$expected" ]]; then
+ return 0
+ fi
+ done < "$file"
+ return 1
+}
+
+prepare_path_config_tracking() {
+ if [[ "$no_modify_path" == "true" || ":${PATH}:" == *":${INSTALL_DIR}:"* ]]; then
+ return 0
fi
- specific_version="local"
-else
- raw_os=$(uname -s)
- case "$raw_os" in
- Darwin*) os="darwin" ;;
- Linux*) os="linux" ;;
- MINGW*|MSYS*|CYGWIN*) os="windows" ;;
- *)
- print_message error "Unsupported OS: $raw_os"
- exit 1
- ;;
+ path_update_requested=true
+ resolve_path_config
+ recorded_config_file="$config_file"
+ recorded_path_command="$path_command"
+ case "$recorded_config_file$recorded_path_command" in
+ *$'\t'*|*$'\n'*|*$'\r'*) return 0 ;;
esac
+ [[ "$recorded_config_file" == /* ]] || return 0
+ if [[ -f "$recorded_config_file" \
+ && ! -L "$recorded_config_file" \
+ && -O "$recorded_config_file" \
+ && -w "$recorded_config_file" ]]; then
+ if file_has_exact_line "$recorded_config_file" "$recorded_path_command"; then
+ path_already_present=true
+ return 0
+ fi
+ record_path_config=true
+ fi
+}
- arch=$(uname -m)
- case "$arch" in
- aarch64|arm64) arch="arm64" ;;
- x86_64|amd64) arch="x64" ;;
- *)
- print_message error "Unsupported architecture: $arch"
- exit 1
+print_manual_path_change() {
+ local operation=$1 config=$2 command=$3
+ if [[ "$operation" == "add" ]]; then
+ printf 'Add this to %s:\n %s\n' "$config" "$command"
+ else
+ printf 'Could not update %s. Remove these exact lines manually:\n' "$config"
+ printf ' # Executor\n %s\n' "$command"
+ fi
+}
+
+remove_path_entry() {
+ edit_path_config_portable remove "$1" "$2"
+}
+
+sha256_file() {
+ local path=$1
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$path" | awk '{ print $1 }'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$path" | awk '{ print $1 }'
+ else
+ fail "sha256sum or shasum is required"
+ fi
+}
+
+durability_checkpoint() {
+ local label=$1
+ if [[ -n "${EXECUTOR_INSTALL_TEST_DURABILITY_LOG:-}" ]]; then
+ printf '%s\n' "$label" >> "$EXECUTOR_INSTALL_TEST_DURABILITY_LOG" \
+ || fail "could not record durability checkpoint: $label"
+ fi
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_DURABILITY_POINT:-}" == "$label" ]]; then
+ fail "injected durability failure at $label"
+ fi
+}
+
+sync_with_label() {
+ local label=$1
+ command -v sync >/dev/null 2>&1 || return 1
+ EXECUTOR_INSTALL_SYNC_LABEL="$label" sync
+}
+
+durability_barrier() {
+ local label=$1
+ if [[ -n "$install_root_identity" ]]; then
+ assert_install_root
+ fi
+ sync_with_label "$label" || fail "could not complete durability barrier: $label"
+ durability_checkpoint "$label"
+}
+
+portable_file_identity() {
+ if stat -c '%d:%i:%u:%g:%f' -- "$1" >/dev/null 2>&1; then
+ stat -c '%d:%i:%u:%g:%f' -- "$1"
+ else
+ stat -f '%d:%i:%u:%g:%p' "$1"
+ fi
+}
+
+portable_link_count() {
+ if stat -c '%h' -- "$1" >/dev/null 2>&1; then
+ stat -c '%h' -- "$1"
+ else
+ stat -f '%l' "$1"
+ fi
+}
+
+process_identity() {
+ local pid=$1 stat_line remainder start_time boot_id snapshot
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_PROCESS_IDENTITY_PID:-}" == "$pid" ]]; then
+ return 1
+ fi
+ if [[ -r "/proc/${pid}/stat" && -r /proc/sys/kernel/random/boot_id ]]; then
+ stat_line="$(< "/proc/${pid}/stat")" || return 1
+ [[ "$stat_line" == *') '* ]] || return 1
+ remainder="${stat_line##*) }"
+ start_time="$(awk '{ print $20 }' <<< "$remainder")"
+ [[ "$start_time" =~ ^[0-9]+$ ]] || return 1
+ boot_id="$(< /proc/sys/kernel/random/boot_id)" || return 1
+ printf 'linux:%s:%s\n' "$boot_id" "$start_time"
+ return 0
+ fi
+ snapshot="$(LC_ALL=C ps -p "$pid" -o lstart= -o command= 2>/dev/null)" \
+ || return 1
+ [[ -n "$snapshot" ]] || return 1
+ printf '%s' "$snapshot" | cksum | awk '{ printf "ps:%s:%s\n", $1, $2 }'
+}
+
+validate_existing_secure_directory_chain() {
+ local path=$1 remainder component current
+ [[ "$path" == /* ]] || return 1
+ case "$path" in
+ */|*//*|*/./*|*/../*|*/.|*/..) return 1 ;;
+ esac
+ validate_install_path_component /
+ remainder="${path#/}"
+ current=""
+ while [[ -n "$remainder" ]]; do
+ component="${remainder%%/*}"
+ [[ -n "$component" && "$component" != "." && "$component" != ".." ]] \
+ || return 1
+ current="${current}/${component}"
+ validate_install_path_component "$current"
+ if [[ "$remainder" == "$component" ]]; then
+ remainder=""
+ else
+ remainder="${remainder#*/}"
+ fi
+ done
+}
+
+assert_path_edit_parent() {
+ [[ -d "$path_edit_parent" && ! -L "$path_edit_parent" ]] \
+ || return 1
+ [[ "$(portable_file_identity "$path_edit_parent")" == "$path_edit_parent_identity" ]]
+}
+
+read_path_edit_lock() {
+ local lock=$1 before after line key value extra line_number=0
+ observed_lock_pid=""
+ observed_lock_process_identity=""
+ [[ -f "$lock" && ! -L "$lock" && -O "$lock" ]] || return 1
+ before="$(sha256_file "$lock")" || return 1
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ [[ "$line" == "executor-path-edit-lock-v1" ]] || return 1
+ continue
+ fi
+ IFS=' ' read -r key value extra <<< "$line"
+ [[ -n "$key" && -n "$value" && -z "${extra:-}" ]] || return 1
+ case "$key" in
+ pid)
+ [[ -z "$observed_lock_pid" && "$value" =~ ^[1-9][0-9]*$ ]] \
+ || return 1
+ observed_lock_pid="$value"
+ ;;
+ identity)
+ [[ -z "$observed_lock_process_identity" \
+ && "$value" =~ ^[A-Za-z0-9:._-]+$ ]] \
+ || return 1
+ observed_lock_process_identity="$value"
+ ;;
+ *) return 1 ;;
+ esac
+ done < "$lock"
+ [[ "$line_number" -eq 3 \
+ && -n "$observed_lock_pid" \
+ && -n "$observed_lock_process_identity" ]] \
+ || return 1
+ after="$(sha256_file "$lock")" || return 1
+ [[ "$after" == "$before" ]] || return 1
+ observed_lock_digest="$after"
+ observed_lock_identity="$(portable_file_identity "$lock")" || return 1
+}
+
+reclaim_stale_path_edit_lock() {
+ local lock=$1 expected_identity=$2 expected_digest=$3 candidate snapshot
+ assert_install_root
+ assert_path_edit_parent || return 1
+ for candidate in "${lock}.owner."*; do
+ [[ -e "$candidate" || -L "$candidate" ]] || continue
+ if [[ -f "$candidate" && ! -L "$candidate" && -O "$candidate" ]] \
+ && [[ "$(portable_file_identity "$candidate")" == "$expected_identity" ]] \
+ && [[ "$(sha256_file "$candidate")" == "$expected_digest" ]]; then
+ rm -f -- "$candidate" || return 1
+ fi
+ done
+ sync_with_label path-edit-stale-owner-cleanup || return 1
+ snapshot="$(mktemp "${lock}.stale.XXXXXXXX")" || return 1
+ rm -f -- "$snapshot" || return 1
+ if ! ln "$lock" "$snapshot" 2>/dev/null; then
+ return 1
+ fi
+ if [[ ! -f "$lock" \
+ || -L "$lock" \
+ || "$(portable_file_identity "$lock")" != "$expected_identity" \
+ || "$(sha256_file "$lock")" != "$expected_digest" \
+ || "$(portable_file_identity "$snapshot")" != "$expected_identity" \
+ || "$(portable_link_count "$snapshot")" -ne 2 ]]; then
+ rm -f -- "$snapshot" || return 1
+ sync_with_label path-edit-stale-snapshot-cleanup || return 1
+ return 1
+ fi
+ if ! assert_path_edit_parent; then
+ rm -f -- "$snapshot" || return 1
+ return 1
+ fi
+ if ! rm -f -- "$lock"; then
+ if ! rm -f -- "$snapshot"; then
+ :
+ fi
+ return 1
+ fi
+ sync_with_label path-edit-stale-lock-reclaimed || return 1
+ rm -f -- "$snapshot" || return 1
+ sync_with_label path-edit-stale-lock-cleanup || return 1
+}
+
+cleanup_unacquired_path_edit_lock() {
+ local lock=$1 temporary=$2 status=0
+ rm -f -- "$lock" || status=1
+ rm -f -- "$temporary" || status=1
+ return "$status"
+}
+
+acquire_path_edit_lock() {
+ local config=$1 temporary owner_identity current_identity attempt
+ path_edit_parent="$(dirname "$config")"
+ validate_existing_secure_directory_chain "$path_edit_parent" || return 1
+ path_edit_parent_identity="$(portable_file_identity "$path_edit_parent")" || return 1
+ path_edit_lock_path="${config}.executor-edit-lock"
+ owner_identity="$(process_identity "$$")" || return 1
+
+ for attempt in {1..200}; do
+ assert_install_root
+ assert_path_edit_parent || return 1
+ if [[ -e "$path_edit_lock_path" || -L "$path_edit_lock_path" ]]; then
+ read_path_edit_lock "$path_edit_lock_path" || return 1
+ if kill -0 "$observed_lock_pid" 2>/dev/null; then
+ current_identity="$(process_identity "$observed_lock_pid" 2>/dev/null)" \
+ || return 1
+ if [[ "$current_identity" == "$observed_lock_process_identity" ]]; then
+ sleep 0.05 || return 1
+ continue
+ fi
+ elif current_identity="$(process_identity "$observed_lock_pid" 2>/dev/null)"; then
+ return 1
+ fi
+ if reclaim_stale_path_edit_lock \
+ "$path_edit_lock_path" \
+ "$observed_lock_identity" \
+ "$observed_lock_digest"; then
+ continue
+ fi
+ sleep 0.05 || return 1
+ continue
+ fi
+
+ temporary="$(mktemp "${path_edit_lock_path}.owner.XXXXXXXX")" || return 1
+ if ! {
+ printf 'executor-path-edit-lock-v1\n'
+ printf 'pid %s\n' "$$"
+ printf 'identity %s\n' "$owner_identity"
+ } > "$temporary"; then
+ rm -f -- "$temporary" || return 1
+ return 1
+ fi
+ if ! chmod 0600 "$temporary"; then
+ rm -f -- "$temporary" || return 1
+ return 1
+ fi
+ if ! sync_with_label path-edit-owner-record-durable; then
+ rm -f -- "$temporary" || return 1
+ return 1
+ fi
+ if ln "$temporary" "$path_edit_lock_path" 2>/dev/null; then
+ if ! sync_with_label path-edit-lock-acquired; then
+ rm -f -- "$path_edit_lock_path" || return 1
+ rm -f -- "$temporary" || return 1
+ sync_with_label path-edit-lock-acquire-rollback || return 1
+ return 1
+ fi
+ path_edit_lock_identity="$(portable_file_identity "$path_edit_lock_path")" \
+ || { cleanup_unacquired_path_edit_lock "$path_edit_lock_path" "$temporary"; return 1; }
+ path_edit_lock_digest="$(sha256_file "$path_edit_lock_path")" \
+ || { cleanup_unacquired_path_edit_lock "$path_edit_lock_path" "$temporary"; return 1; }
+ path_edit_lock_acquired=true
+ if ! rm -f -- "$temporary"; then
+ release_path_edit_lock || return 1
+ return 1
+ fi
+ sync_with_label path-edit-owner-temp-cleanup || return 1
+ return 0
+ fi
+ rm -f -- "$temporary" || return 1
+ sync_with_label path-edit-owner-temp-cleanup || return 1
+ done
+ return 1
+}
+
+release_path_edit_lock() {
+ if [[ "$path_edit_lock_acquired" != "true" ]]; then
+ return 0
+ fi
+ assert_path_edit_parent || return 1
+ [[ -f "$path_edit_lock_path" \
+ && ! -L "$path_edit_lock_path" \
+ && "$(portable_file_identity "$path_edit_lock_path")" == "$path_edit_lock_identity" \
+ && "$(sha256_file "$path_edit_lock_path")" == "$path_edit_lock_digest" ]] \
+ || return 1
+ rm -f -- "$path_edit_lock_path" || return 1
+ sync_with_label path-edit-lock-release || return 1
+ path_edit_lock_acquired=false
+}
+
+file_has_owned_path_block() {
+ local file=$1 expected=$2 previous="" line normalized
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ normalized="${line%$'\r'}"
+ if [[ "$previous" == "# Executor" && "$normalized" == "$expected" ]]; then
+ return 0
+ fi
+ previous="$normalized"
+ done < "$file"
+ return 1
+}
+
+perform_path_config_edit() {
+ local operation=$1 config=$2 command=$3 attempt temporary
+ local initial_identity initial_digest current_identity current_digest final_newline
+ path_edit_error=""
+ path_edit_result=""
+ for attempt in {1..4}; do
+ assert_install_root
+ assert_path_edit_parent \
+ || { path_edit_error="shell-config parent changed during update"; return 1; }
+ initial_identity="$(portable_file_identity "$config")" \
+ || { path_edit_error="could not inspect shell config"; return 1; }
+ initial_digest="$(sha256_file "$config")" \
+ || { path_edit_error="could not hash shell config"; return 1; }
+
+ if [[ "$operation" == "add" ]] && file_has_exact_line "$config" "$command"; then
+ path_edit_result=unchanged
+ return 0
+ fi
+ if [[ "$operation" == "remove" ]] \
+ && ! file_has_owned_path_block "$config" "$command"; then
+ path_edit_result=unchanged
+ return 0
+ fi
+
+ temporary="$(mktemp "${path_edit_parent}/.$(basename "$config").executor-XXXXXXXX")" \
+ || { path_edit_error="could not create shell-config staging file"; return 1; }
+ if ! cp -p "$config" "$temporary"; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove failed shell-config staging file"
+ path_edit_error="could not stage shell config"
+ return 1
+ fi
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_CONFIG_WRITE:-}" == "1" ]]; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove injected shell-config staging file"
+ path_edit_error="injected shell-config write failure"
+ return 1
+ fi
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_REAL_CONFIG_WRITE:-}" == "1" ]]; then
+ if ! chmod 0400 "$temporary"; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove failed shell-config staging file"
+ path_edit_error="could not make the shell-config staging file read-only"
+ return 1
+ fi
+ fi
+
+ if [[ "$operation" == "add" ]]; then
+ if [[ -s "$config" && "$(tail -c 1 "$config" | wc -l)" -eq 0 ]]; then
+ if ! printf '\n' >> "$temporary"; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove failed shell-config staging file"
+ path_edit_error="could not append a shell-config separator"
+ return 1
+ fi
+ fi
+ if ! printf '# Executor\n%s\n' "$command" >> "$temporary"; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove failed shell-config staging file"
+ path_edit_error="could not append the shell-config PATH block"
+ return 1
+ fi
+ elif [[ "$operation" == "remove" ]]; then
+ if [[ ! -s "$config" || "$(tail -c 1 "$config" | wc -l)" -eq 1 ]]; then
+ final_newline=true
+ else
+ final_newline=false
+ fi
+ if ! EXECUTOR_PATH_EDIT_COMMAND="$command" \
+ EXECUTOR_PATH_EDIT_FINAL_NEWLINE="$final_newline" \
+ awk '
+ function body(value, normalized) {
+ normalized = value
+ sub(/\r$/, "", normalized)
+ return normalized
+ }
+ { lines[++count] = $0 }
+ END {
+ for (cursor = 1; cursor <= count; cursor++) {
+ if (body(lines[cursor]) == "# Executor" \
+ && cursor < count \
+ && body(lines[cursor + 1]) == ENVIRON["EXECUTOR_PATH_EDIT_COMMAND"]) {
+ cursor++
+ continue
+ }
+ printf "%s", lines[cursor]
+ if (cursor < count \
+ || ENVIRON["EXECUTOR_PATH_EDIT_FINAL_NEWLINE"] == "true") {
+ printf "\n"
+ }
+ }
+ }
+ ' "$config" > "$temporary"; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove failed shell-config staging file"
+ path_edit_error="could not rewrite shell config"
+ return 1
+ fi
+ else
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove invalid shell-config staging file"
+ path_edit_error="invalid shell-config operation"
+ return 1
+ fi
+
+ durability_barrier "staged-path-config-durable:$operation" sync "$temporary"
+ if ! assert_path_edit_parent; then
+ if ! rm -f -- "$temporary"; then
+ path_edit_error="shell-config parent changed and staging cleanup failed"
+ else
+ path_edit_error="shell-config parent changed during update"
+ fi
+ return 1
+ fi
+ current_identity="$(portable_file_identity "$config")" || current_identity=""
+ current_digest="$(sha256_file "$config")" || current_digest=""
+ if [[ "$current_identity" != "$initial_identity" \
+ || "$current_digest" != "$initial_digest" ]]; then
+ rm -f -- "$temporary" \
+ || { path_edit_error="could not remove stale shell-config staging file"; return 1; }
+ continue
+ fi
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_CONFIG_RENAME:-}" == "1" ]]; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove injected shell-config staging file"
+ path_edit_error="injected shell-config rename failure"
+ return 1
+ fi
+ if ! mv -f "$temporary" "$config"; then
+ rm -f -- "$temporary" \
+ || path_edit_error="could not remove failed shell-config staging file"
+ path_edit_error="could not publish shell-config update"
+ return 1
+ fi
+ durability_barrier "path-config-durable:$operation" sync "$config" "$path_edit_parent"
+ path_edit_result=changed
+ return 0
+ done
+ path_edit_error="shell config changed during every update attempt"
+ return 1
+}
+
+edit_path_config_portable() {
+ local operation=$1 config=$2 command=$3
+ path_edit_changed=false
+ if [[ ! -e "$config" ]]; then
+ if [[ "$operation" == "add" ]]; then
+ print_manual_path_change "$operation" "$config" "$command"
+ fi
+ return 0
+ fi
+ if [[ ! -f "$config" || -L "$config" || ! -O "$config" || ! -w "$config" ]]; then
+ print_manual_path_change "$operation" "$config" "$command"
+ return 0
+ fi
+ if ! acquire_path_edit_lock "$config"; then
+ print_manual_path_change "$operation" "$config" "$command"
+ return 0
+ fi
+ if [[ -n "${EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_READY:-}" \
+ || -n "${EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_RELEASE:-}" ]]; then
+ if [[ -z "${EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_READY:-}" \
+ || -z "${EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_RELEASE:-}" ]]; then
+ release_path_edit_lock
+ fail "config pause injection is incomplete"
+ fi
+ : > "$EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_READY"
+ sync_with_label path-edit-test-pause \
+ || fail "could not publish config pause checkpoint"
+ while [[ ! -e "$EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_RELEASE" ]]; do
+ sleep 0.01
+ done
+ fi
+ if ! perform_path_config_edit "$operation" "$config" "$command"; then
+ if ! release_path_edit_lock; then
+ path_edit_error="$path_edit_error; could not release shell-config edit lock"
+ fi
+ fail "$path_edit_error"
+ fi
+ release_path_edit_lock || fail "could not release shell-config edit lock"
+ if [[ "$path_edit_result" == "changed" ]]; then
+ path_edit_changed=true
+ if [[ "$operation" == "add" ]]; then
+ printf 'Added Executor to PATH in %s\n' "$config"
+ else
+ printf 'Removed Executor PATH entry from %s\n' "$config"
+ fi
+ fi
+}
+
+install_path_owner_mode() {
+ if stat -c '%u:%a' -- "$1" >/dev/null 2>&1; then
+ stat -c '%u:%a' -- "$1"
+ else
+ stat -f '%u:%Lp' "$1"
+ fi
+}
+
+validate_install_path_component() {
+ local path=$1 metadata uid mode numeric_mode
+ [[ -d "$path" && ! -L "$path" ]] \
+ || fail "install path component is not a real directory: $path"
+ metadata="$(install_path_owner_mode "$path")" \
+ || fail "could not inspect install path component: $path"
+ IFS=: read -r uid mode <<< "$metadata"
+ [[ "$uid" =~ ^[0-9]+$ && "$mode" =~ ^[0-7]+$ ]] \
+ || fail "install path component metadata is malformed: $path"
+ [[ "$uid" -eq 0 || "$uid" -eq "$EUID" ]] \
+ || fail "install path components must be owned by root or the current user: $path"
+ numeric_mode=$((8#$mode))
+ (( (numeric_mode & 0022) == 0 )) \
+ || fail "install path components cannot be group or world writable: $path"
+}
+
+assert_install_root() {
+ [[ -d "$install_root_parent" && ! -L "$install_root_parent" ]] \
+ || fail "install root parent changed after validation: $install_root_parent"
+ [[ "$(portable_file_identity "$install_root_parent")" \
+ == "$install_root_parent_identity" ]] \
+ || fail "install root parent changed after validation: $install_root_parent"
+ [[ -d "$INSTALL_DIR" && ! -L "$INSTALL_DIR" && -O "$INSTALL_DIR" ]] \
+ || fail "install root changed after validation: $INSTALL_DIR"
+ [[ "$(portable_file_identity "$INSTALL_DIR")" == "$install_root_identity" ]] \
+ || fail "install root changed after validation: $INSTALL_DIR"
+}
+
+prepare_install_root() {
+ local remainder component current
+ case "$INSTALL_DIR" in
+ */|*//*|*/./*|*/../*|*/.|*/..)
+ fail "EXECUTOR_INSTALL_DIR must be a normalized absolute path"
;;
esac
- # Apple Silicon under Rosetta reports x64 — install the native arm64 build.
- if [[ "$os" == "darwin" && "$arch" == "x64" ]]; then
- if [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" == "1" ]]; then
- arch="arm64"
+ validate_install_path_component /
+ remainder="${INSTALL_DIR#/}"
+ current=""
+ while [[ -n "$remainder" ]]; do
+ component="${remainder%%/*}"
+ [[ -n "$component" && "$component" != "." && "$component" != ".." ]] \
+ || fail "EXECUTOR_INSTALL_DIR must be a normalized absolute path"
+ current="${current}/${component}"
+ if [[ ! -e "$current" && ! -L "$current" ]]; then
+ if ! mkdir -m 0700 -- "$current" 2>/dev/null; then
+ [[ -e "$current" || -L "$current" ]] \
+ || fail "could not create install path component: $current"
+ fi
+ fi
+ validate_install_path_component "$current"
+ if [[ "$remainder" == "$component" ]]; then
+ remainder=""
+ else
+ remainder="${remainder#*/}"
fi
+ done
+
+ [[ -O "$INSTALL_DIR" ]] \
+ || fail "install root must be owned by the current user: $INSTALL_DIR"
+ install_root_parent="$(dirname "$INSTALL_DIR")"
+ install_root_parent_identity="$(portable_file_identity "$install_root_parent")"
+ install_root_identity="$(portable_file_identity "$INSTALL_DIR")"
+ durability_barrier install-root-durable sync "$INSTALL_DIR"
+}
+
+read_install_lock() {
+ local lock=$1 before after line key value extra line_number=0
+ observed_lock_pid=""
+ observed_lock_process_identity=""
+ [[ -f "$lock" && ! -L "$lock" && -O "$lock" ]] \
+ || fail "install lock is not a regular current-user-owned file: $lock"
+ before="$(sha256_file "$lock")"
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ [[ "$line" == "executor-install-lock-v1" ]] \
+ || fail "install lock has an unsupported format"
+ continue
+ fi
+ IFS=' ' read -r key value extra <<< "$line"
+ [[ -n "$key" && -n "$value" && -z "${extra:-}" ]] \
+ || fail "install lock contains a malformed entry"
+ case "$key" in
+ pid)
+ [[ -z "$observed_lock_pid" && "$value" =~ ^[1-9][0-9]*$ ]] \
+ || fail "install lock contains an invalid pid"
+ observed_lock_pid="$value"
+ ;;
+ identity)
+ [[ -z "$observed_lock_process_identity" \
+ && "$value" =~ ^[A-Za-z0-9:._-]+$ ]] \
+ || fail "install lock contains an invalid process identity"
+ observed_lock_process_identity="$value"
+ ;;
+ *) fail "install lock contains an unsupported entry: $key" ;;
+ esac
+ done < "$lock"
+ [[ "$line_number" -eq 3 \
+ && -n "$observed_lock_pid" \
+ && -n "$observed_lock_process_identity" ]] \
+ || fail "install lock is incomplete"
+ after="$(sha256_file "$lock")"
+ [[ "$after" == "$before" ]] || fail "install lock changed while it was read"
+ observed_lock_digest="$after"
+ observed_lock_identity="$(portable_file_identity "$lock")"
+}
+
+reclaim_stale_install_lock() {
+ local lock=$1 expected_identity=$2 expected_digest=$3 candidate snapshot
+ assert_install_root
+ for candidate in "${lock}.owner."*; do
+ [[ -e "$candidate" || -L "$candidate" ]] || continue
+ if [[ -f "$candidate" && ! -L "$candidate" && -O "$candidate" ]] \
+ && [[ "$(portable_file_identity "$candidate")" == "$expected_identity" ]] \
+ && [[ "$(sha256_file "$candidate")" == "$expected_digest" ]]; then
+ rm -f -- "$candidate" || return 1
+ fi
+ done
+ durability_barrier stale-lock-owner-cleanup-durable sync "$INSTALL_DIR"
+ snapshot="$(mktemp "${INSTALL_DIR}/.${LOCK_NAME}.stale.XXXXXXXX")" || return 1
+ rm -f -- "$snapshot" || return 1
+ if ! ln "$lock" "$snapshot" 2>/dev/null; then
+ return 1
+ fi
+ if [[ ! -f "$lock" \
+ || -L "$lock" \
+ || "$(portable_file_identity "$lock")" != "$expected_identity" \
+ || "$(sha256_file "$lock")" != "$expected_digest" \
+ || "$(portable_file_identity "$snapshot")" != "$expected_identity" \
+ || "$(portable_link_count "$snapshot")" -ne 2 ]]; then
+ rm -f -- "$snapshot" || return 1
+ durability_barrier stale-lock-snapshot-cleanup sync "$INSTALL_DIR"
+ return 1
+ fi
+ assert_install_root
+ if ! rm -f -- "$lock"; then
+ rm -f -- "$snapshot" || return 1
+ return 1
+ fi
+ durability_barrier stale-lock-reclaimed sync "$INSTALL_DIR"
+ rm -f -- "$snapshot" || return 1
+ durability_barrier stale-lock-cleanup-durable sync "$INSTALL_DIR"
+}
+
+test_pause_if_requested() {
+ local point=$1
+ if [[ "${EXECUTOR_INSTALL_TEST_PAUSE_POINT:-}" != "$point" ]]; then
+ return 0
fi
+ [[ -n "${EXECUTOR_INSTALL_TEST_PAUSE_READY:-}" \
+ && -n "${EXECUTOR_INSTALL_TEST_PAUSE_RELEASE:-}" ]] \
+ || fail "pause injection requires ready and release paths"
+ : > "$EXECUTOR_INSTALL_TEST_PAUSE_READY"
+ durability_barrier "test-pause-ready:$point" sync "$EXECUTOR_INSTALL_TEST_PAUSE_READY"
+ while [[ ! -e "$EXECUTOR_INSTALL_TEST_PAUSE_RELEASE" ]]; do
+ sleep 0.1
+ done
+}
- is_musl=false
- if [[ "$os" == "linux" ]]; then
- if [[ -f /etc/alpine-release ]]; then
- is_musl=true
- elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
- is_musl=true
+acquire_install_lock() {
+ local lock="${INSTALL_DIR}/${LOCK_NAME}" temporary owner_identity
+ local current_identity attempt
+ assert_install_root
+ owner_identity="$(process_identity "$$")" \
+ || fail "could not determine installer process identity"
+ for attempt in {1..8}; do
+ temporary="$(mktemp "${INSTALL_DIR}/.${LOCK_NAME}.owner.XXXXXXXX")" \
+ || fail "could not create install-lock owner record"
+ if ! {
+ printf 'executor-install-lock-v1\n'
+ printf 'pid %s\n' "$$"
+ printf 'identity %s\n' "$owner_identity"
+ } > "$temporary"; then
+ fail "could not write install-lock owner record"
fi
+ chmod 0600 "$temporary" || fail "could not protect install-lock owner record"
+ durability_barrier lock-owner-record-durable sync "$temporary"
+ assert_install_root
+ if ln "$temporary" "$lock" 2>/dev/null; then
+ durability_barrier lock-acquired sync "$INSTALL_DIR"
+ install_lock_identity="$(portable_file_identity "$lock")"
+ install_lock_digest="$(sha256_file "$lock")"
+ install_lock_acquired=true
+ rm -f -- "$temporary" || fail "could not remove install-lock owner staging file"
+ durability_barrier lock-owner-temp-cleanup sync "$INSTALL_DIR"
+ test_pause_if_requested after-lock
+ return 0
+ fi
+ rm -f -- "$temporary" || fail "could not remove install-lock owner staging file"
+ durability_barrier lock-owner-temp-cleanup sync "$INSTALL_DIR"
+ if [[ ! -e "$lock" && ! -L "$lock" ]]; then
+ continue
+ fi
+ read_install_lock "$lock"
+ if kill -0 "$observed_lock_pid" 2>/dev/null; then
+ current_identity="$(process_identity "$observed_lock_pid" 2>/dev/null)" \
+ || fail "could not verify live install-lock owner pid $observed_lock_pid"
+ if [[ "$current_identity" == "$observed_lock_process_identity" ]]; then
+ fail "another Executor installer is active with pid $observed_lock_pid"
+ fi
+ elif current_identity="$(process_identity "$observed_lock_pid" 2>/dev/null)"; then
+ fail "install-lock owner pid $observed_lock_pid exists but cannot be signaled"
+ fi
+ if reclaim_stale_install_lock \
+ "$lock" \
+ "$observed_lock_identity" \
+ "$observed_lock_digest"; then
+ continue
+ fi
+ done
+ fail "could not acquire the Executor install lock after concurrent changes"
+}
+
+release_install_lock() {
+ local lock="${INSTALL_DIR}/${LOCK_NAME}"
+ if [[ "$install_lock_acquired" != "true" ]]; then
+ return 0
fi
+ if [[ ! -f "$lock" \
+ || -L "$lock" \
+ || "$(portable_file_identity "$lock")" != "$install_lock_identity" \
+ || "$(sha256_file "$lock")" != "$install_lock_digest" ]]; then
+ printf 'Error: install lock changed while held\n' >&2
+ return 1
+ fi
+ assert_install_root
+ if ! rm -f -- "$lock"; then
+ printf 'Error: could not release install lock\n' >&2
+ return 1
+ fi
+ if ! sync_with_label install-lock-release; then
+ printf 'Error: could not durably release install lock\n' >&2
+ return 1
+ fi
+ if [[ -n "${EXECUTOR_INSTALL_TEST_DURABILITY_LOG:-}" ]]; then
+ printf 'lock-released\n' >> "$EXECUTOR_INSTALL_TEST_DURABILITY_LOG" || return 1
+ fi
+ install_lock_acquired=false
+}
- target="${os}-${arch}"
- if [[ "$is_musl" == "true" ]]; then
- target="${target}-musl"
+installer_exit_cleanup() {
+ local status=$?
+ trap - EXIT
+ if declare -F cleanup_private_temp_directory >/dev/null 2>&1; then
+ cleanup_private_temp_directory || status=1
+ fi
+ if declare -F release_path_edit_lock >/dev/null 2>&1; then
+ release_path_edit_lock || status=1
fi
+ release_install_lock || status=1
+ exit "$status"
+}
+
+manifest_hash_for() {
+ case "$1" in
+ executor) printf '%s\n' "$manifest_executor_hash" ;;
+ LICENSE) printf '%s\n' "$manifest_license_hash" ;;
+ THIRD_PARTY_LICENSES.html) printf '%s\n' "$manifest_rust_notices_hash" ;;
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json)
+ printf '%s\n' "$manifest_javascript_notices_hash"
+ ;;
+ .executor-path-ownership) printf '%s\n' "$manifest_path_ownership_hash" ;;
+ *) fail "install manifest contains an unsupported file name: $1" ;;
+ esac
+}
- archive_ext=".zip"
- if [[ "$os" == "linux" ]]; then
- archive_ext=".tar.gz"
+set_manifest_hash() {
+ local name=$1 hash=$2
+ case "$name" in
+ executor) manifest_executor_hash="$hash" ;;
+ LICENSE) manifest_license_hash="$hash" ;;
+ THIRD_PARTY_LICENSES.html) manifest_rust_notices_hash="$hash" ;;
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json)
+ manifest_javascript_notices_hash="$hash"
+ ;;
+ .executor-path-ownership) manifest_path_ownership_hash="$hash" ;;
+ *) fail "install manifest contains an unsupported file name: $name" ;;
+ esac
+}
+
+verify_owned_file() {
+ local name=$1 expected=$2 path actual
+ path="${INSTALL_DIR}/${name}"
+ [[ -f "$path" && ! -L "$path" ]] \
+ || fail "installer-owned file is missing or not regular: $path"
+ actual="$(sha256_file "$path")"
+ [[ "$actual" == "$expected" ]] \
+ || fail "installer-owned file was replaced after installation: $path"
+}
+
+load_install_manifest() {
+ local manifest="${INSTALL_DIR}/${MANIFEST_NAME}"
+ local allow_missing=${1:-false}
+ local line hash name extra existing before_digest after_digest
+ local line_number=0 entry_count=0
+ manifest_present=false
+ manifest_digest=""
+ manifest_executor_hash=""
+ manifest_license_hash=""
+ manifest_rust_notices_hash=""
+ manifest_javascript_notices_hash=""
+ manifest_path_ownership_hash=""
+
+ if [[ ! -e "$manifest" && ! -L "$manifest" ]]; then
+ return 0
fi
+ [[ -f "$manifest" && ! -L "$manifest" && -O "$manifest" ]] \
+ || fail "install manifest is not a regular owned file: $manifest"
+ before_digest="$(sha256_file "$manifest")"
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ [[ "$line" == "executor-install-manifest-v1" ]] \
+ || fail "install manifest has an unsupported format"
+ continue
+ fi
+ IFS=' ' read -r hash name extra <<< "$line"
+ [[ -n "$hash" && -n "$name" && -z "${extra:-}" && "$line" == "$hash $name" ]] \
+ || fail "install manifest contains a malformed entry"
+ [[ "$hash" =~ ^[0-9a-f]{64}$ ]] \
+ || fail "install manifest contains a malformed SHA-256"
+ existing="$(manifest_hash_for "$name")"
+ [[ -z "$existing" ]] || fail "install manifest contains a duplicate entry: $name"
+ set_manifest_hash "$name" "$hash"
+ entry_count=$((entry_count + 1))
+ done < "$manifest"
+ [[ "$line_number" -gt 1 && "$entry_count" -gt 0 ]] \
+ || fail "install manifest contains no owned files"
+ after_digest="$(sha256_file "$manifest")"
+ [[ "$after_digest" == "$before_digest" ]] \
+ || fail "install manifest changed while it was being read"
+ manifest_present=true
+ manifest_digest="$after_digest"
- filename="${APP}-${target}${archive_ext}"
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ hash="$(manifest_hash_for "$name")"
+ if [[ -n "$hash" ]]; then
+ if [[ -e "${INSTALL_DIR}/${name}" || -L "${INSTALL_DIR}/${name}" ]]; then
+ verify_owned_file "$name" "$hash"
+ elif [[ "$allow_missing" != "true" ]]; then
+ fail "installer-owned file is missing: ${INSTALL_DIR}/${name}"
+ fi
+ fi
+ done
+}
- if [[ "$os" == "linux" ]]; then
- if ! command -v tar >/dev/null 2>&1; then
- print_message error "Error: 'tar' is required but not installed."
- exit 1
+prepare_install_ownership() {
+ local name expected path
+ load_install_manifest
+ for name in "$@"; do
+ path="${INSTALL_DIR}/${name}"
+ expected="$(manifest_hash_for "$name")"
+ if [[ -e "$path" || -L "$path" ]]; then
+ [[ -n "$expected" ]] \
+ || fail "refusing to overwrite a file not owned by this installer: $path"
fi
+ done
+}
+
+install_source_for() {
+ case "$1" in
+ executor) printf '%s\n' "$install_source_executor" ;;
+ LICENSE) printf '%s\n' "$install_source_license" ;;
+ THIRD_PARTY_LICENSES.html) printf '%s\n' "$install_source_rust_notices" ;;
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json)
+ printf '%s\n' "$install_source_javascript_notices"
+ ;;
+ *) fail "unsupported install transaction file: $1" ;;
+ esac
+}
+
+install_mode_for() {
+ if [[ "$1" == "executor" ]]; then
+ printf '0755\n'
+ elif [[ "$1" == ".executor-path-ownership" ]]; then
+ printf '0600\n'
else
- if ! command -v unzip >/dev/null 2>&1; then
- print_message error "Error: 'unzip' is required but not installed."
- exit 1
- fi
+ printf '0644\n'
fi
+}
- if [[ -z "$requested_version" ]]; then
- url="https://github.com/${REPO}/releases/latest/download/${filename}"
- specific_version=$(
- curl -s "https://api.github.com/repos/${REPO}/releases/latest" \
- | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p'
- )
- if [[ -z "$specific_version" ]]; then
- print_message error "Failed to fetch latest version metadata"
- exit 1
+set_recovery_hashes() {
+ local name=$1 old=$2 new=$3
+ case "$name" in
+ executor)
+ recovery_executor_old="$old"
+ recovery_executor_new="$new"
+ ;;
+ LICENSE)
+ recovery_license_old="$old"
+ recovery_license_new="$new"
+ ;;
+ THIRD_PARTY_LICENSES.html)
+ recovery_rust_notices_old="$old"
+ recovery_rust_notices_new="$new"
+ ;;
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json)
+ recovery_javascript_notices_old="$old"
+ recovery_javascript_notices_new="$new"
+ ;;
+ .executor-path-ownership)
+ recovery_path_ownership_old="$old"
+ recovery_path_ownership_new="$new"
+ ;;
+ *) fail "install recovery contains an unsupported file name: $name" ;;
+ esac
+}
+
+recovery_old_for() {
+ case "$1" in
+ executor) printf '%s\n' "$recovery_executor_old" ;;
+ LICENSE) printf '%s\n' "$recovery_license_old" ;;
+ THIRD_PARTY_LICENSES.html) printf '%s\n' "$recovery_rust_notices_old" ;;
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json)
+ printf '%s\n' "$recovery_javascript_notices_old"
+ ;;
+ .executor-path-ownership) printf '%s\n' "$recovery_path_ownership_old" ;;
+ *) fail "install recovery contains an unsupported file name: $1" ;;
+ esac
+}
+
+recovery_new_for() {
+ case "$1" in
+ executor) printf '%s\n' "$recovery_executor_new" ;;
+ LICENSE) printf '%s\n' "$recovery_license_new" ;;
+ THIRD_PARTY_LICENSES.html) printf '%s\n' "$recovery_rust_notices_new" ;;
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json)
+ printf '%s\n' "$recovery_javascript_notices_new"
+ ;;
+ .executor-path-ownership) printf '%s\n' "$recovery_path_ownership_new" ;;
+ *) fail "install recovery contains an unsupported file name: $1" ;;
+ esac
+}
+
+copy_file_atomically() {
+ local source=$1 destination=$2 label=$3 temporary parent
+ assert_install_root
+ parent="$(dirname "$destination")"
+ temporary="$(mktemp "${destination}.recovery.XXXXXXXX")" \
+ || fail "could not create rollback staging file"
+ trap 'rm -f "$temporary"' RETURN
+ cp -p "$source" "$temporary" || fail "could not stage rollback file"
+ durability_barrier "staged-restore-durable:$label" sync "$temporary"
+ assert_install_root
+ mv -f "$temporary" "$destination" || fail "could not publish rollback file"
+ durability_barrier "$label" sync "$destination" "$parent"
+ trap - RETURN
+}
+
+retire_install_recovery() {
+ local label=$1 recovery="${INSTALL_DIR}/${RECOVERY_NAME}" retired
+ assert_install_root
+ [[ -d "$recovery" && ! -L "$recovery" ]] \
+ || fail "install recovery directory disappeared"
+ retired="$(mktemp -d "${INSTALL_DIR}/.${RECOVERY_NAME}.retired.XXXXXXXX")" \
+ || fail "could not create recovery retirement directory"
+ assert_install_root
+ mv "$recovery" "${retired}/state" || fail "could not retire install recovery"
+ durability_barrier "recovery-retired:$label" sync "$retired" "$INSTALL_DIR"
+ rm -rf "$retired" || fail "could not remove retired install recovery"
+ durability_barrier "recovery-cleanup-durable:$label" sync "$INSTALL_DIR"
+}
+
+create_path_ownership_state() {
+ local output=$1 existing="${INSTALL_DIR}/${PATH_OWNERSHIP_NAME}"
+ local line tracked_config tracked_command extra line_number=0 entry_count=0
+ local already_recorded=false
+ printf 'executor-path-ownership-v1\n' > "$output"
+ if [[ -n "$manifest_path_ownership_hash" ]]; then
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ [[ "$line" == "executor-path-ownership-v1" ]] \
+ || fail "PATH ownership state has an unsupported format"
+ continue
+ fi
+ IFS=$'\t' read -r tracked_config tracked_command extra <<< "$line"
+ [[ -n "$tracked_config" \
+ && -n "$tracked_command" \
+ && -z "${extra:-}" \
+ && "$tracked_config" == /* \
+ && "$line" == "$tracked_config"$'\t'"$tracked_command" ]] \
+ || fail "PATH ownership state has a malformed entry"
+ printf '%s\t%s\n' "$tracked_config" "$tracked_command" >> "$output"
+ if [[ "$tracked_config" == "$recorded_config_file" \
+ && "$tracked_command" == "$recorded_path_command" ]]; then
+ already_recorded=true
+ fi
+ entry_count=$((entry_count + 1))
+ done < "$existing"
+ [[ "$line_number" -gt 1 && "$entry_count" -gt 0 ]] \
+ || fail "PATH ownership state contains no entries"
+ fi
+ if [[ "$already_recorded" != "true" ]]; then
+ printf '%s\t%s\n' \
+ "$recorded_config_file" \
+ "$recorded_path_command" \
+ >> "$output"
+ fi
+ chmod 0600 "$output"
+}
+
+remove_recorded_path_entries() {
+ local state="${INSTALL_DIR}/${PATH_OWNERSHIP_NAME}"
+ local line tracked_config tracked_command extra line_number=0 entry_count=0
+ [[ -e "$state" || -L "$state" ]] || return 0
+ [[ -f "$state" && ! -L "$state" && -O "$state" ]] \
+ || fail "PATH ownership state is not a regular owned file"
+
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ [[ "$line" == "executor-path-ownership-v1" ]] \
+ || fail "PATH ownership state has an unsupported format"
+ continue
+ fi
+ IFS=$'\t' read -r tracked_config tracked_command extra <<< "$line"
+ [[ -n "$tracked_config" \
+ && -n "$tracked_command" \
+ && -z "${extra:-}" \
+ && "$tracked_config" == /* \
+ && "$line" == "$tracked_config"$'\t'"$tracked_command" ]] \
+ || fail "PATH ownership state has a malformed entry"
+ entry_count=$((entry_count + 1))
+ done < "$state"
+ [[ "$line_number" -gt 1 && "$entry_count" -gt 0 ]] \
+ || fail "PATH ownership state contains no entries"
+
+ line_number=0
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ continue
fi
+ IFS=$'\t' read -r tracked_config tracked_command extra <<< "$line"
+ remove_path_entry "$tracked_config" "$tracked_command"
+ done < "$state"
+}
+
+begin_install_transaction() {
+ local recovery="${INSTALL_DIR}/${RECOVERY_NAME}" temporary plan_entries
+ local manifest="${INSTALL_DIR}/${MANIFEST_NAME}"
+ local transaction_label=$1
+ local old_manifest new_manifest name source mode old_hash new_hash hash
+ assert_install_root
+ prepare_install_ownership "$@"
+ mkdir -p "$INSTALL_DIR"
+ durability_barrier "install-directory-durable:$transaction_label" chain "$INSTALL_DIR"
+ [[ ! -e "$recovery" && ! -L "$recovery" ]] \
+ || fail "an install recovery transaction is already present"
+
+ temporary="$(mktemp -d "${INSTALL_DIR}/.${RECOVERY_NAME}.XXXXXXXX")" \
+ || fail "could not create install recovery staging directory"
+ trap 'rm -rf "$temporary"' RETURN
+ chmod 0700 "$temporary"
+ mkdir "${temporary}/backup" "${temporary}/new"
+ plan_entries="${temporary}/plan.entries"
+ : > "$plan_entries"
+ chmod 0600 "$plan_entries"
+
+ if [[ "$manifest_present" == "true" ]]; then
+ cp -p "$manifest" "${temporary}/backup/${MANIFEST_NAME}"
+ old_manifest="$manifest_digest"
else
- requested_version="${requested_version#v}"
- url="https://github.com/${REPO}/releases/download/v${requested_version}/${filename}"
- specific_version="$requested_version"
+ old_manifest=absent
+ fi
- http_status=$(curl -sI -o /dev/null -w "%{http_code}" \
- "https://github.com/${REPO}/releases/tag/v${requested_version}")
- if [[ "$http_status" == "404" ]]; then
- print_message error "Error: release v${requested_version} not found"
- print_message info "${MUTED}Available releases: https://github.com/${REPO}/releases${NC}"
- exit 1
+ for name in "$@"; do
+ mode="$(install_mode_for "$name")"
+ old_hash="$(manifest_hash_for "$name")"
+ if [[ -n "$old_hash" ]]; then
+ cp -p "${INSTALL_DIR}/${name}" "${temporary}/backup/${name}"
+ else
+ old_hash=absent
+ fi
+ if [[ "$name" == "$PATH_OWNERSHIP_NAME" ]]; then
+ create_path_ownership_state "${temporary}/new/${name}"
+ else
+ source="$(install_source_for "$name")"
+ [[ -f "$source" && ! -L "$source" ]] \
+ || fail "install source is missing or not regular: $source"
+ cp "$source" "${temporary}/new/${name}"
+ chmod "$mode" "${temporary}/new/${name}"
fi
+ new_hash="$(sha256_file "${temporary}/new/${name}")"
+ set_manifest_hash "$name" "$new_hash"
+ printf 'file %s %s %s\n' "$old_hash" "$new_hash" "$name" >> "$plan_entries"
+ done
+
+ new_manifest="${temporary}/new/${MANIFEST_NAME}"
+ printf 'executor-install-manifest-v1\n' > "$new_manifest"
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ hash="$(manifest_hash_for "$name")"
+ if [[ -n "$hash" ]]; then
+ printf '%s %s\n' "$hash" "$name" >> "$new_manifest"
+ fi
+ done
+ chmod 0600 "$new_manifest"
+ new_manifest="$(sha256_file "$new_manifest")"
+
+ {
+ printf 'executor-install-recovery-v1\n'
+ printf 'manifest %s %s\n' "$old_manifest" "$new_manifest"
+ cat "$plan_entries"
+ } > "${temporary}/plan"
+ chmod 0600 "${temporary}/plan"
+ rm -f "$plan_entries" || fail "could not finalize install recovery plan"
+ durability_barrier "recovery-tree-durable:$transaction_label" tree "$temporary"
+ assert_install_root
+ mv "$temporary" "$recovery" || fail "could not publish install recovery"
+ durability_barrier "recovery-durable:$transaction_label" sync "$recovery" "$INSTALL_DIR"
+ trap - RETURN
+}
+
+recover_install_transaction() {
+ local recovery="${INSTALL_DIR}/${RECOVERY_NAME}" plan manifest
+ local line kind old_hash new_hash name extra current_hash
+ local old_manifest="" new_manifest="" line_number=0 entry_count=0
+ assert_install_root
+ recovery_executor_old=""
+ recovery_executor_new=""
+ recovery_license_old=""
+ recovery_license_new=""
+ recovery_rust_notices_old=""
+ recovery_rust_notices_new=""
+ recovery_javascript_notices_old=""
+ recovery_javascript_notices_new=""
+ recovery_path_ownership_old=""
+ recovery_path_ownership_new=""
+
+ if [[ ! -e "$recovery" && ! -L "$recovery" ]]; then
+ return 0
fi
-fi
+ [[ -d "$recovery" && ! -L "$recovery" && -O "$recovery" ]] \
+ || fail "install recovery is not a private owned directory"
+ plan="${recovery}/plan"
+ [[ -f "$plan" && ! -L "$plan" && -O "$plan" ]] \
+ || fail "install recovery plan is not a regular owned file"
-check_existing_version() {
- if command -v executor >/dev/null 2>&1; then
- local installed
- installed=$(executor --version 2>/dev/null || echo "")
- if [[ "$installed" == "$specific_version" ]]; then
- print_message info "${MUTED}Version ${NC}${specific_version}${MUTED} already installed${NC}"
- exit 0
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ line_number=$((line_number + 1))
+ if [[ "$line_number" -eq 1 ]]; then
+ [[ "$line" == "executor-install-recovery-v1" ]] \
+ || fail "install recovery has an unsupported format"
+ continue
+ fi
+ if [[ "$line_number" -eq 2 ]]; then
+ IFS=' ' read -r kind old_manifest new_manifest extra <<< "$line"
+ [[ "$kind" == "manifest" && -z "${extra:-}" ]] \
+ || fail "install recovery has a malformed manifest entry"
+ [[ "$old_manifest" == "absent" || "$old_manifest" =~ ^[0-9a-f]{64}$ ]] \
+ || fail "install recovery has a malformed old manifest hash"
+ [[ "$new_manifest" =~ ^[0-9a-f]{64}$ ]] \
+ || fail "install recovery has a malformed new manifest hash"
+ continue
fi
- print_message info "${MUTED}Replacing installed version ${NC}${installed}"
+ IFS=' ' read -r kind old_hash new_hash name extra <<< "$line"
+ [[ "$kind" == "file" && -n "$name" && -z "${extra:-}" ]] \
+ || fail "install recovery has a malformed file entry"
+ [[ "$old_hash" == "absent" || "$old_hash" =~ ^[0-9a-f]{64}$ ]] \
+ || fail "install recovery has a malformed old file hash"
+ [[ "$new_hash" =~ ^[0-9a-f]{64}$ ]] \
+ || fail "install recovery has a malformed new file hash"
+ [[ -z "$(recovery_new_for "$name")" ]] \
+ || fail "install recovery has a duplicate file entry: $name"
+ set_recovery_hashes "$name" "$old_hash" "$new_hash"
+ entry_count=$((entry_count + 1))
+ done < "$plan"
+ [[ "$line_number" -gt 2 && "$entry_count" -gt 0 ]] \
+ || fail "install recovery contains no files"
+
+ manifest="${recovery}/new/${MANIFEST_NAME}"
+ [[ -f "$manifest" && ! -L "$manifest" ]] \
+ || fail "install recovery is missing the new manifest"
+ [[ "$(sha256_file "$manifest")" == "$new_manifest" ]] \
+ || fail "install recovery new manifest hash does not match"
+ if [[ "$old_manifest" != "absent" ]]; then
+ manifest="${recovery}/backup/${MANIFEST_NAME}"
+ [[ -f "$manifest" && ! -L "$manifest" ]] \
+ || fail "install recovery is missing the old manifest"
+ [[ "$(sha256_file "$manifest")" == "$old_manifest" ]] \
+ || fail "install recovery old manifest hash does not match"
fi
-}
-download_and_install() {
- print_message info "\n${MUTED}Installing ${NC}${APP} ${MUTED}version: ${NC}${specific_version}"
- local tmp_dir
- tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/${APP}_install_XXXXXXXXXX")
- trap 'rm -rf "$tmp_dir"' RETURN
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ new_hash="$(recovery_new_for "$name")"
+ if [[ -z "$new_hash" ]]; then
+ continue
+ fi
+ old_hash="$(recovery_old_for "$name")"
+ [[ -f "${recovery}/new/${name}" && ! -L "${recovery}/new/${name}" ]] \
+ || fail "install recovery is missing a staged file: $name"
+ [[ "$(sha256_file "${recovery}/new/${name}")" == "$new_hash" ]] \
+ || fail "install recovery staged file hash does not match: $name"
+ if [[ "$old_hash" != "absent" ]]; then
+ [[ -f "${recovery}/backup/${name}" && ! -L "${recovery}/backup/${name}" ]] \
+ || fail "install recovery is missing a backup file: $name"
+ [[ "$(sha256_file "${recovery}/backup/${name}")" == "$old_hash" ]] \
+ || fail "install recovery backup hash does not match: $name"
+ elif [[ -e "${recovery}/backup/${name}" || -L "${recovery}/backup/${name}" ]]; then
+ fail "install recovery has an unexpected backup file: $name"
+ fi
+
+ if [[ -e "${INSTALL_DIR}/${name}" || -L "${INSTALL_DIR}/${name}" ]]; then
+ [[ -f "${INSTALL_DIR}/${name}" && ! -L "${INSTALL_DIR}/${name}" ]] \
+ || fail "install destination was replaced during recovery: $name"
+ current_hash="$(sha256_file "${INSTALL_DIR}/${name}")"
+ [[ "$current_hash" == "$new_hash" \
+ || "$old_hash" != "absent" && "$current_hash" == "$old_hash" ]] \
+ || fail "install destination changed outside the recovery transaction: $name"
+ fi
+ done
- curl -# -L -o "${tmp_dir}/${filename}" "$url"
+ manifest="${INSTALL_DIR}/${MANIFEST_NAME}"
+ if [[ -e "$manifest" || -L "$manifest" ]]; then
+ [[ -f "$manifest" && ! -L "$manifest" && -O "$manifest" ]] \
+ || fail "install manifest was replaced during recovery"
+ current_hash="$(sha256_file "$manifest")"
+ [[ "$current_hash" == "$new_manifest" \
+ || "$old_manifest" != "absent" && "$current_hash" == "$old_manifest" ]] \
+ || fail "install manifest changed outside the recovery transaction"
+ fi
- if [[ "$os" == "linux" ]]; then
- tar -xzf "${tmp_dir}/${filename}" -C "$tmp_dir"
+ assert_install_root
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ new_hash="$(recovery_new_for "$name")"
+ if [[ -z "$new_hash" ]]; then
+ continue
+ fi
+ old_hash="$(recovery_old_for "$name")"
+ if [[ "$old_hash" == "absent" ]]; then
+ assert_install_root
+ rm -f -- "${INSTALL_DIR}/${name}" \
+ || fail "could not remove interrupted install file: $name"
+ durability_barrier "rollback-managed-durable:$name" sync "$INSTALL_DIR"
+ else
+ copy_file_atomically \
+ "${recovery}/backup/${name}" \
+ "${INSTALL_DIR}/${name}" \
+ "rollback-managed-durable:$name"
+ fi
+ done
+ if [[ "$old_manifest" == "absent" ]]; then
+ assert_install_root
+ rm -f -- "${INSTALL_DIR}/${MANIFEST_NAME}" \
+ || fail "could not remove interrupted install manifest"
+ durability_barrier "rollback-manifest-durable" sync "$INSTALL_DIR"
else
- unzip -q "${tmp_dir}/${filename}" -d "$tmp_dir"
+ copy_file_atomically \
+ "${recovery}/backup/${MANIFEST_NAME}" \
+ "${INSTALL_DIR}/${MANIFEST_NAME}" \
+ "rollback-manifest-durable"
fi
+ retire_install_recovery rollback
+ printf 'Recovered an interrupted Executor installation\n'
+}
- # The archive is flat — the binary plus sidecars (emscripten-module.wasm,
- # keyring.node) sit at the root. Copy them all into INSTALL_DIR so the
- # binary's relative-path lookups still resolve.
- rm -f "${tmp_dir}/${filename}"
- cp -R "${tmp_dir}/." "${INSTALL_DIR}/"
+commit_install_transaction() {
+ local recovery="${INSTALL_DIR}/${RECOVERY_NAME}" transaction_label=$1
+ local name mode count=0
+ assert_install_root
+ for name in "$@"; do
+ mode="$(install_mode_for "$name")"
+ install_file \
+ "${recovery}/new/${name}" \
+ "$name" \
+ "$mode" \
+ "managed-file-durable:$name"
+ count=$((count + 1))
+ test_pause_if_requested "after-install-file-$count"
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_AFTER_INSTALL_FILE:-}" == "$count" ]]; then
+ fail "injected failure after publishing install file $count"
+ fi
+ done
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_MANIFEST:-}" == "1" ]]; then
+ fail "injected install manifest publication failure"
+ fi
+ install_file \
+ "${recovery}/new/${MANIFEST_NAME}" \
+ "$MANIFEST_NAME" \
+ 0600 \
+ "manifest-durable:$transaction_label"
+ retire_install_recovery "$transaction_label"
+}
- chmod 755 "${INSTALL_DIR}/${APP}"
- rm -rf "$tmp_dir"
- trap - RETURN
+remove_owned_file() {
+ local name=$1 expected=$2
+ assert_install_root
+ if [[ -e "${INSTALL_DIR}/${name}" || -L "${INSTALL_DIR}/${name}" ]]; then
+ verify_owned_file "$name" "$expected"
+ assert_install_root
+ rm -f -- "${INSTALL_DIR}/${name}" \
+ || fail "could not remove installer-owned file: $name"
+ fi
+ durability_barrier "uninstall-file-durable:$name" sync "$INSTALL_DIR"
}
-install_from_binary() {
- print_message info "\n${MUTED}Installing ${NC}${APP} ${MUTED}from: ${NC}${binary_path}"
- cp "$binary_path" "${INSTALL_DIR}/${APP}"
- chmod 755 "${INSTALL_DIR}/${APP}"
+uninstall_executor() {
+ local name hash manifest="${INSTALL_DIR}/${MANIFEST_NAME}"
+ local removed_count=0 seen_existing_owned_file=false
+ load_install_manifest true
+ if [[ "$manifest_present" != "true" ]]; then
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ if [[ -e "${INSTALL_DIR}/${name}" || -L "${INSTALL_DIR}/${name}" ]]; then
+ fail "refusing to remove files without an installer ownership manifest"
+ fi
+ done
+ fi
+
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ hash="$(manifest_hash_for "$name")"
+ if [[ -z "$hash" ]]; then
+ continue
+ fi
+ if [[ -e "${INSTALL_DIR}/${name}" || -L "${INSTALL_DIR}/${name}" ]]; then
+ seen_existing_owned_file=true
+ elif [[ "$seen_existing_owned_file" == "true" ]]; then
+ fail "installer-owned files are missing outside resumable uninstall order"
+ fi
+ done
+
+ if [[ "$no_modify_path" != "true" && -n "$manifest_path_ownership_hash" ]]; then
+ remove_recorded_path_entries
+ fi
+
+ if [[ "$manifest_present" == "true" ]]; then
+ for name in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership; do
+ hash="$(manifest_hash_for "$name")"
+ if [[ -n "$hash" ]]; then
+ remove_owned_file "$name" "$hash"
+ removed_count=$((removed_count + 1))
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_AFTER_UNINSTALL_DELETE:-}" \
+ == "$removed_count" ]]; then
+ fail "injected failure after uninstall file $removed_count"
+ fi
+ fi
+ done
+ [[ "$(sha256_file "$manifest")" == "$manifest_digest" ]] \
+ || fail "install manifest changed during uninstall"
+ assert_install_root
+ rm -f -- "$manifest" || fail "could not remove install manifest"
+ durability_barrier "uninstall-manifest-durable" sync "$INSTALL_DIR"
+ fi
+ printf 'Removed Executor program files from %s\n' "$INSTALL_DIR"
+ printf 'Executor data, service definitions, and unrelated files were preserved.\n'
}
+prepare_install_root
+trap installer_exit_cleanup EXIT
+test_pause_if_requested after-install-root
+acquire_install_lock
+recover_install_transaction
+
+if [[ "$uninstall" == "true" ]]; then
+ uninstall_executor
+ exit 0
+fi
+
+case "$REPOSITORY" in
+ ''|/*|*/|*/*/*|*[!A-Za-z0-9._/-]*)
+ fail "EXECUTOR_REPOSITORY must be an owner/repository name"
+ ;;
+esac
+
+case "${OSTYPE:-}" in
+ darwin*) platform="apple-darwin" ;;
+ linux*) platform="unknown-linux-gnu" ;;
+ *) fail "Executor release binaries support only Linux and macOS" ;;
+esac
+
+machine="$(uname -m)"
+case "$machine" in
+ x86_64|amd64) architecture="x86_64" ;;
+ arm64|aarch64) architecture="aarch64" ;;
+ *) fail "unsupported architecture: $machine" ;;
+esac
+
+if [[ "$platform" == "apple-darwin" && "$architecture" == "x86_64" ]]; then
+ if [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || printf '0')" == "1" ]]; then
+ architecture="aarch64"
+ fi
+fi
+
+target="${architecture}-${platform}"
+archive="${APP}-${target}.tar.gz"
if [[ -n "$binary_path" ]]; then
- install_from_binary
+ planned_install_files=(executor)
else
- check_existing_version
- download_and_install
+ planned_install_files=(
+ executor
+ LICENSE
+ THIRD_PARTY_LICENSES.html
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json
+ )
fi
+prepare_path_config_tracking
-add_to_path() {
- local config_file=$1 command=$2
- if grep -Fxq "$command" "$config_file"; then
- print_message info "${MUTED}Already in ${NC}${config_file}"
- elif [[ -w "$config_file" ]]; then
- echo -e "\n# executor" >> "$config_file"
- echo "$command" >> "$config_file"
- print_message info "${MUTED}Added ${NC}${APP} ${MUTED}to \$PATH in ${NC}${config_file}"
+path_identity() {
+ if [[ "$platform" == "apple-darwin" ]]; then
+ stat -f '%u:%Lp:%d:%i' "$1"
else
- print_message warning "Manually add to ${config_file}:"
- print_message info " $command"
+ stat -c '%u:%a:%d:%i' -- "$1"
fi
}
-XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
-current_shell=$(basename "${SHELL:-bash}")
+validate_trusted_temp_component() {
+ local path=$1 identity uid mode device inode numeric_mode
+ [[ -d "$path" && ! -L "$path" ]] \
+ || fail "temporary-directory ancestor is not a real directory: $path"
+ identity="$(path_identity "$path")" \
+ || fail "could not inspect temporary-directory ancestor: $path"
+ IFS=: read -r uid mode device inode <<< "$identity"
+ [[ "$uid" =~ ^[0-9]+$ \
+ && "$mode" =~ ^[0-7]+$ \
+ && "$device" =~ ^[0-9]+$ \
+ && "$inode" =~ ^[0-9]+$ ]] \
+ || fail "temporary-directory ancestor metadata is malformed: $path"
+ numeric_mode=$((8#$mode))
+ if [[ "$uid" -eq 0 ]]; then
+ if (( (numeric_mode & 0022) != 0 && (numeric_mode & 01000) == 0 )); then
+ fail "writable root-owned temporary-directory ancestors must have the sticky bit: $path"
+ fi
+ elif [[ "$uid" -eq "$EUID" ]]; then
+ (( (numeric_mode & 0022) == 0 )) \
+ || fail "current-user temporary-directory ancestors cannot be group or world writable: $path"
+ else
+ fail "temporary-directory ancestors must be owned by root or the current user: $path"
+ fi
+}
-case "$current_shell" in
- fish)
- config_files="$HOME/.config/fish/config.fish"
- ;;
- zsh)
- config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
- ;;
- bash)
- config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
- ;;
- *)
- config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
- ;;
-esac
+prepare_trusted_temp_root() {
+ local raw_temp_root remainder component current
+ raw_temp_root="${TMPDIR:-/tmp}"
+ case "$raw_temp_root" in
+ ''|*$'\t'*|*$'\n'*|*$'\r'*) fail "TMPDIR must be a nonempty absolute path without control characters" ;;
+ esac
+ [[ "$raw_temp_root" == /* ]] || fail "TMPDIR must be an absolute path"
+ trusted_temp_root="$(cd -P -- "$raw_temp_root" 2>/dev/null && pwd -P)" \
+ || fail "TMPDIR does not resolve to an accessible directory"
+ [[ "$trusted_temp_root" != "/" ]] || fail "TMPDIR cannot be the filesystem root"
-if [[ "$no_modify_path" != "true" ]]; then
- config_file=""
- for file in $config_files; do
- if [[ -f "$file" ]]; then
- config_file=$file
- break
+ validate_trusted_temp_component /
+ remainder="${trusted_temp_root#/}"
+ current=""
+ while [[ -n "$remainder" ]]; do
+ component="${remainder%%/*}"
+ [[ -n "$component" && "$component" != "." && "$component" != ".." ]] \
+ || fail "TMPDIR must be a normalized path without empty, dot, or dot-dot components"
+ current="${current}/${component}"
+ validate_trusted_temp_component "$current"
+ if [[ "$remainder" == "$component" ]]; then
+ remainder=""
+ else
+ remainder="${remainder#*/}"
fi
done
+ trusted_temp_root_identity="$(path_identity "$trusted_temp_root")"
+}
- if [[ -z "$config_file" ]]; then
- print_message warning "No config file found for ${current_shell}. Add manually:"
- print_message info " export PATH=${INSTALL_DIR}:\$PATH"
- elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
- case "$current_shell" in
- fish) add_to_path "$config_file" "fish_add_path $INSTALL_DIR" ;;
- *) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;;
- esac
+verify_private_temp_directory() {
+ local root_identity directory_identity uid mode device inode
+ [[ -n "$temporary_directory" ]] || fail "private temporary directory is not initialized"
+ [[ -d "$trusted_temp_root" && ! -L "$trusted_temp_root" ]] \
+ || fail "trusted temporary root was replaced"
+ root_identity="$(path_identity "$trusted_temp_root")" \
+ || fail "could not recheck trusted temporary root"
+ [[ "$root_identity" == "$trusted_temp_root_identity" ]] \
+ || fail "trusted temporary root changed during installation"
+ [[ -d "$temporary_directory" && ! -L "$temporary_directory" ]] \
+ || fail "private temporary directory was replaced"
+ directory_identity="$(path_identity "$temporary_directory")" \
+ || fail "could not inspect private temporary directory"
+ [[ "$directory_identity" == "$temporary_directory_identity" ]] \
+ || fail "private temporary directory changed during installation"
+ IFS=: read -r uid mode device inode <<< "$directory_identity"
+ [[ "$uid" -eq "$EUID" && "$mode" == "700" ]] \
+ || fail "private temporary directory must remain current-user owned with mode 0700"
+}
+
+cleanup_private_temp_directory() {
+ local current_identity
+ if [[ -n "$temporary_directory" \
+ && -d "$temporary_directory" \
+ && ! -L "$temporary_directory" ]] \
+ && current_identity="$(path_identity "$temporary_directory" 2>/dev/null)" \
+ && [[ "$current_identity" == "$temporary_directory_identity" ]]; then
+ rm -rf -- "$temporary_directory" || return 1
+ fi
+ if [[ -n "$test_swapped_temp_directory" \
+ && -d "$test_swapped_temp_directory" \
+ && ! -L "$test_swapped_temp_directory" ]] \
+ && current_identity="$(path_identity "$test_swapped_temp_directory" 2>/dev/null)" \
+ && [[ "$current_identity" == "$temporary_directory_identity" ]]; then
+ rm -rf -- "$test_swapped_temp_directory" || return 1
+ fi
+}
+
+make_temp_dir() {
+ local identity uid mode device inode
+ prepare_trusted_temp_root
+ temporary_directory="$(mktemp -d "${trusted_temp_root}/${APP}-install.XXXXXXXX")"
+ chmod 0700 "$temporary_directory"
+ [[ -d "$temporary_directory" && ! -L "$temporary_directory" ]] \
+ || fail "mktemp did not create a private directory"
+ identity="$(path_identity "$temporary_directory")" \
+ || fail "could not inspect the private temporary directory"
+ IFS=: read -r uid mode device inode <<< "$identity"
+ [[ "$uid" -eq "$EUID" && "$mode" == "700" ]] \
+ || fail "mktemp directory is not current-user owned with mode 0700"
+ temporary_directory_identity="$identity"
+ verify_private_temp_directory
+}
+
+inject_temp_directory_swap_for_test() {
+ if [[ "${EXECUTOR_INSTALL_TEST_SWAP_TEMP_DIRECTORY:-}" != "1" ]]; then
+ return 0
+ fi
+ test_swapped_temp_directory="${temporary_directory}.original"
+ mv "$temporary_directory" "$test_swapped_temp_directory"
+ mkdir -m 0700 "$temporary_directory"
+}
+
+verify_checksum() {
+ local checksum_file=$1 archive_path=$2 expected actual
+ expected="$(awk 'NF { print $1; exit }' "$checksum_file")"
+ [[ "$expected" =~ ^[[:xdigit:]]{64}$ ]] || fail "release checksum is malformed"
+ actual="$(sha256_file "$archive_path")"
+ [[ "$actual" == "$expected" ]] || fail "release checksum verification failed"
+}
+
+install_file() {
+ local source=$1 destination=$2 mode=$3 label=$4 temporary
+ assert_install_root
+ mkdir -p "$INSTALL_DIR"
+ temporary="$(mktemp "${INSTALL_DIR}/.${destination}.XXXXXXXX")" \
+ || fail "could not create install staging file: $destination"
+ trap 'rm -f "$temporary"' RETURN
+ cp "$source" "$temporary" || fail "could not stage install file: $destination"
+ chmod "$mode" "$temporary" || fail "could not set install file mode: $destination"
+ durability_barrier "staged-file-durable:$destination" sync "$temporary"
+ assert_install_root
+ mv -f "$temporary" "${INSTALL_DIR}/${destination}" \
+ || fail "could not publish install file: $destination"
+ durability_barrier "$label" sync "${INSTALL_DIR}/${destination}" "$INSTALL_DIR"
+ trap - RETURN
+}
+
+if [[ -n "$binary_path" ]]; then
+ [[ -f "$binary_path" ]] || fail "binary not found at $binary_path"
+ install_source_executor="$binary_path"
+else
+ command -v tar >/dev/null 2>&1 || fail "tar is required"
+
+ make_temp_dir
+ trap installer_exit_cleanup EXIT
+ if [[ -n "$local_archive_path" ]]; then
+ local_archive_source="$local_archive_path"
+ local_checksum_source="${local_checksum_path:-${local_archive_path}.sha256}"
+ [[ -f "$local_archive_source" && ! -L "$local_archive_source" ]] \
+ || fail "archive is not a regular non-symlink file: $local_archive_source"
+ [[ -f "$local_checksum_source" && ! -L "$local_checksum_source" ]] \
+ || fail "checksum is not a regular non-symlink file: $local_checksum_source"
+ archive_path="${temporary_directory}/${archive}"
+ checksum_path="${archive_path}.sha256"
+ verify_private_temp_directory
+ (
+ ulimit -f 524288
+ cp "$local_archive_source" "$archive_path"
+ )
+ (
+ ulimit -f 2048
+ cp "$local_checksum_source" "$checksum_path"
+ )
+ chmod 0600 "$archive_path" "$checksum_path"
+ verify_private_temp_directory
+ if [[ "${EXECUTOR_INSTALL_TEST_REMOVE_SOURCE_AFTER_STAGE:-}" == "1" ]]; then
+ rm -f -- "$local_archive_source" "$local_checksum_source"
+ fi
+ else
+ command -v curl >/dev/null 2>&1 || fail "curl is required"
+ requested_version="${requested_version#v}"
+ if [[ -n "$requested_version" ]]; then
+ case "$requested_version" in
+ *[!A-Za-z0-9._-]*) fail "the requested version contains unsafe characters" ;;
+ esac
+ release_base="https://github.com/${REPOSITORY}/releases/download/v${requested_version}"
+ version_label="v${requested_version}"
+ else
+ release_base="https://github.com/${REPOSITORY}/releases/latest/download"
+ version_label="latest"
+ fi
+ archive_path="${temporary_directory}/${archive}"
+ checksum_path="${archive_path}.sha256"
+
+ printf 'Downloading Executor %s for %s\n' "$version_label" "$target"
+ curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
+ --max-filesize 268435456 \
+ --output "$archive_path" "${release_base}/${archive}"
+ curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
+ --max-filesize 1048576 \
+ --output "$checksum_path" "${release_base}/${archive}.sha256"
+ fi
+ inject_temp_directory_swap_for_test
+ verify_private_temp_directory
+ [[ "$(wc -c < "$archive_path")" -le 268435456 ]] \
+ || fail "release archive exceeds 256 MiB"
+ [[ "$(wc -c < "$checksum_path")" -le 1048576 ]] \
+ || fail "release checksum exceeds 1 MiB"
+ verify_checksum "$checksum_path" "$archive_path"
+
+ members_path="${temporary_directory}/archive-members.txt"
+ verify_private_temp_directory
+ (
+ ulimit -f 2048
+ ulimit -t 30
+ tar -tzf "$archive_path" > "$members_path"
+ )
+ archive_members="$(< "$members_path")"
+ expected_members=$'executor\nLICENSE\nTHIRD_PARTY_LICENSES.html\nTHIRD_PARTY_JAVASCRIPT_LICENSES.json'
+ [[ "$archive_members" == "$expected_members" ]] \
+ || fail "release archive has unexpected members"
+ verbose_members_path="${temporary_directory}/archive-members-verbose.txt"
+ verify_private_temp_directory
+ (
+ ulimit -f 2048
+ ulimit -t 30
+ tar -tvzf "$archive_path" > "$verbose_members_path"
+ )
+ while IFS= read -r member; do
+ [[ "${member:0:1}" == "-" ]] \
+ || fail "release archive members must be regular files"
+ done < "$verbose_members_path"
+
+ extracted_directory="${temporary_directory}/extracted"
+ mkdir "$extracted_directory"
+ extract_member() {
+ local member=$1 file_blocks=$2
+ verify_private_temp_directory
+ (
+ ulimit -f "$file_blocks"
+ ulimit -t 60
+ tar -xOzf "$archive_path" "$member" \
+ > "${extracted_directory}/${member}"
+ )
+ }
+ extract_member executor 524288
+ extract_member LICENSE 4096
+ extract_member THIRD_PARTY_LICENSES.html 131072
+ extract_member THIRD_PARTY_JAVASCRIPT_LICENSES.json 131072
+ verify_private_temp_directory
+
+ extracted_binary="${extracted_directory}/${APP}"
+ [[ -f "$extracted_binary" && -s "$extracted_binary" && ! -L "$extracted_binary" ]] \
+ || fail "release archive does not contain a regular executor binary"
+ for support_file in \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json; do
+ [[ -s "${extracted_directory}/${support_file}" ]] \
+ || fail "release archive contains an empty support file"
+ done
+ install_source_executor="$extracted_binary"
+ install_source_license="${extracted_directory}/LICENSE"
+ install_source_rust_notices="${extracted_directory}/THIRD_PARTY_LICENSES.html"
+ install_source_javascript_notices="${extracted_directory}/THIRD_PARTY_JAVASCRIPT_LICENSES.json"
+fi
+
+if [[ -n "$temporary_directory" ]]; then
+ verify_private_temp_directory
+fi
+begin_install_transaction "${planned_install_files[@]}"
+commit_install_transaction "${planned_install_files[@]}"
+
+add_to_path() {
+ edit_path_config_portable add "$1" "$2"
+}
+
+if [[ "$path_update_requested" == "true" ]]; then
+ if [[ "$path_already_present" == "true" ]]; then
+ :
+ elif [[ "$record_path_config" == "true" ]]; then
+ begin_install_transaction "$PATH_OWNERSHIP_NAME"
+ commit_install_transaction "$PATH_OWNERSHIP_NAME"
+ add_to_path "$recorded_config_file" "$recorded_path_command"
+ if [[ "${EXECUTOR_INSTALL_TEST_FAIL_AFTER_PATH_CONFIG:-}" == "1" ]]; then
+ fail "injected failure after PATH configuration update"
+ fi
+ else
+ print_manual_path_change add "$recorded_config_file" "$recorded_path_command"
fi
fi
-if [[ -n "${GITHUB_ACTIONS-}" && "${GITHUB_ACTIONS}" == "true" ]]; then
- echo "$INSTALL_DIR" >> "$GITHUB_PATH"
- print_message info "${MUTED}Added ${NC}${INSTALL_DIR}${MUTED} to \$GITHUB_PATH${NC}"
+if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then
+ printf '%s\n' "$INSTALL_DIR" >> "$GITHUB_PATH"
fi
-print_message info ""
-print_message info "${MUTED}Installed ${NC}${APP} ${MUTED}to ${NC}${INSTALL_DIR}/${APP}"
-print_message info ""
-print_message info "${MUTED}Get started:${NC}"
-print_message info " ${APP} web"
-print_message info ""
+printf '\nInstalled Executor at %s\n' "${INSTALL_DIR}/${APP}"
+printf 'Start it with: executor server\n'
diff --git a/scripts/lib/install-systemd-master-key.sh b/scripts/lib/install-systemd-master-key.sh
new file mode 100644
index 000000000..52f6157af
--- /dev/null
+++ b/scripts/lib/install-systemd-master-key.sh
@@ -0,0 +1,743 @@
+#!/usr/bin/env bash
+
+executor_installer_error() {
+ printf '%s\n' "$1" >&2
+ return 1
+}
+
+executor_paths_overlap() {
+ local left=${1%/}
+ local right=${2%/}
+
+ [[ -n ${left} ]] || left=/
+ [[ -n ${right} ]] || right=/
+ [[ ${left} == / || ${right} == / \
+ || ${left} == "${right}" \
+ || ${left} == "${right}/"* \
+ || ${right} == "${left}/"* ]]
+}
+
+executor_require_disjoint_paths() {
+ local left=$1
+ local left_description=$2
+ local right=$3
+ local right_description=$4
+
+ if executor_paths_overlap "${left}" "${right}"; then
+ executor_installer_error \
+ "${left_description} must not overlap ${right_description}: ${right}"
+ return
+ fi
+}
+
+executor_write_and_sync_master_key() {
+ local path=$1
+
+ dd if=/dev/urandom of="${path}" bs=32 count=1 \
+ iflag=fullblock oflag=nofollow conv=excl,fsync status=none
+}
+
+executor_sync_filesystem_path() {
+ sync -f -- "$1"
+}
+
+executor_require_filesystem_sync() {
+ local path=$1
+ local description=$2
+ local sync_status
+
+ if executor_sync_filesystem_path "${path}"; then
+ return 0
+ else
+ sync_status=$?
+ fi
+ executor_installer_error "could not sync ${description}: ${path}"
+ return "${sync_status}"
+}
+
+executor_master_key_path_digest() {
+ printf '%s' "$1" | sha256sum | awk '{print $1}'
+}
+
+executor_publish_master_key_recovery() {
+ local recovery_path=$1
+ local state=$2
+ local destination_digest=$3
+ local staging_name=$4
+ local expected_uid=$5
+ local expected_gid=$6
+ local device=${7:--}
+ local inode=${8:--}
+ local key_digest=${9:--}
+ local recovery_parent=${recovery_path%/*}
+ local temporary
+
+ if ! temporary=$(mktemp "${recovery_path}.XXXXXXXX"); then
+ executor_installer_error "could not create the master key recovery record"
+ return
+ fi
+ if ! chmod 0600 "${temporary}"; then
+ if ! rm -f -- "${temporary}"; then
+ executor_installer_error \
+ "could not clean the unsecured master key recovery record"
+ fi
+ executor_installer_error "could not secure the master key recovery record"
+ return
+ fi
+ if ! {
+ printf 'executor-master-key-recovery-v1\n'
+ printf 'state %s\n' "${state}"
+ printf 'destination %s\n' "${destination_digest}"
+ printf 'staging %s\n' "${staging_name}"
+ printf 'uid %s\n' "${expected_uid}"
+ printf 'gid %s\n' "${expected_gid}"
+ printf 'device %s\n' "${device}"
+ printf 'inode %s\n' "${inode}"
+ printf 'digest %s\n' "${key_digest}"
+ } > "${temporary}"; then
+ if ! rm -f -- "${temporary}"; then
+ executor_installer_error \
+ "could not clean the unwritten master key recovery record"
+ fi
+ executor_installer_error "could not write the master key recovery record"
+ return
+ fi
+ executor_require_filesystem_sync \
+ "${temporary}" "the master key recovery record" || {
+ local status=$?
+ if ! rm -f -- "${temporary}"; then
+ executor_installer_error \
+ "could not clean the unsynced master key recovery record"
+ fi
+ return "${status}"
+ }
+ if ! mv -f -- "${temporary}" "${recovery_path}"; then
+ if ! rm -f -- "${temporary}"; then
+ executor_installer_error \
+ "could not clean the unpublished master key recovery record"
+ fi
+ executor_installer_error "could not publish the master key recovery record"
+ return
+ fi
+ executor_require_filesystem_sync \
+ "${recovery_parent}" "the master key recovery directory"
+}
+
+executor_retire_master_key_recovery() {
+ local recovery_path=$1
+ local recovery_parent=${recovery_path%/*}
+
+ if ! rm -f -- "${recovery_path}"; then
+ executor_installer_error "could not retire the master key recovery record"
+ return
+ fi
+ executor_require_filesystem_sync \
+ "${recovery_parent}" "the retired master key recovery record"
+}
+
+executor_validate_master_key_recovery_file() {
+ local path=$1
+ local metadata file_type owner mode links size
+
+ if ! metadata=$(LC_ALL=C stat \
+ --format='%F|%u|%a|%h|%s' -- "${path}"); then
+ executor_installer_error \
+ "could not inspect the master key recovery record: ${path}"
+ return
+ fi
+ IFS='|' read -r file_type owner mode links size <<< "${metadata}"
+ if [[ ${file_type} != "regular file" \
+ || ${owner} != "${EUID}" \
+ || ${mode} != 600 \
+ || ${links} != 1 \
+ || ${size} -gt 4096 ]]; then
+ executor_installer_error \
+ "the master key recovery record has unsafe metadata: ${path}"
+ return
+ fi
+}
+
+executor_validate_recorded_master_key() {
+ local path=$1
+ local expected_device=$2
+ local expected_inode=$3
+ local expected_uid=$4
+ local expected_gid=$5
+ local expected_digest=$6
+ local expected_links=$7
+ local metadata file_type size links owner group mode device inode digest
+
+ if ! metadata=$(LC_ALL=C stat \
+ --format='%F|%s|%h|%u|%g|%a|%d|%i' -- "${path}"); then
+ executor_installer_error "could not inspect recorded master key: ${path}"
+ return
+ fi
+ IFS='|' read -r file_type size links owner group mode device inode <<< "${metadata}"
+ if [[ ${file_type} != "regular file" \
+ || ${size} != 32 \
+ || ${links} != "${expected_links}" \
+ || ${owner} != "${expected_uid}" \
+ || ${group} != "${expected_gid}" \
+ || ${mode} != 600 \
+ || ${device} != "${expected_device}" \
+ || ${inode} != "${expected_inode}" ]]; then
+ executor_installer_error \
+ "recorded master key metadata does not match recovery state: ${path}"
+ return
+ fi
+ if ! digest=$(sha256sum -- "${path}" | awk '{print $1}'); then
+ executor_installer_error "could not digest recorded master key: ${path}"
+ return
+ fi
+ if [[ ${digest} != "${expected_digest}" ]]; then
+ executor_installer_error \
+ "recorded master key digest does not match recovery state: ${path}"
+ return
+ fi
+}
+
+executor_validate_prepared_master_key() {
+ local path=$1
+ local expected_device=$2
+ local expected_uid=$3
+ local expected_gid=$4
+ local metadata file_type size links owner group mode device current_gid
+
+ if ! metadata=$(LC_ALL=C stat \
+ --format='%F|%s|%h|%u|%g|%a|%d' -- "${path}"); then
+ executor_installer_error "could not inspect prepared master key: ${path}"
+ return
+ fi
+ IFS='|' read -r file_type size links owner group mode device <<< "${metadata}"
+ if ! current_gid=$(id -g); then
+ executor_installer_error "could not inspect the installer group"
+ return
+ fi
+ if [[ ${file_type} != "regular file" \
+ || ${size} -gt 32 \
+ || ${links} != 1 \
+ || ${owner} != "${EUID}" && ${owner} != "${expected_uid}" \
+ || ${group} != "${current_gid}" && ${group} != "${expected_gid}" \
+ || ${mode} != 600 \
+ || ${device} != "${expected_device}" ]]; then
+ executor_installer_error \
+ "prepared master key metadata does not match recovery state: ${path}"
+ return
+ fi
+}
+
+executor_recover_master_key() {
+ local staging_parent=$1
+ local data_dir=$2
+ local path=$3
+ local expected_uid=$4
+ local expected_gid=$5
+ local recovery_path=$6
+ local destination_digest=$7
+ local staging_device=$8
+ local -a lines=()
+ local state recorded_destination staging_name recorded_uid recorded_gid
+ local recorded_device recorded_inode recorded_digest staging_dir key_tmp
+ local destination_exists=false alias_exists=false destination_metadata
+ local destination_device destination_inode
+
+ if [[ ! -e ${recovery_path} && ! -L ${recovery_path} ]]; then
+ return 0
+ fi
+ executor_validate_master_key_recovery_file "${recovery_path}" || return $?
+ if ! mapfile -t lines < "${recovery_path}"; then
+ executor_installer_error \
+ "could not read the master key recovery record: ${recovery_path}"
+ return
+ fi
+ if [[ ${#lines[@]} -ne 9 \
+ || ${lines[0]} != executor-master-key-recovery-v1 \
+ || ${lines[1]} != "state "* \
+ || ${lines[2]} != "destination "* \
+ || ${lines[3]} != "staging "* \
+ || ${lines[4]} != "uid "* \
+ || ${lines[5]} != "gid "* \
+ || ${lines[6]} != "device "* \
+ || ${lines[7]} != "inode "* \
+ || ${lines[8]} != "digest "* ]]; then
+ executor_installer_error \
+ "the master key recovery record is malformed: ${recovery_path}"
+ return
+ fi
+ state=${lines[1]#state }
+ recorded_destination=${lines[2]#destination }
+ staging_name=${lines[3]#staging }
+ recorded_uid=${lines[4]#uid }
+ recorded_gid=${lines[5]#gid }
+ recorded_device=${lines[6]#device }
+ recorded_inode=${lines[7]#inode }
+ recorded_digest=${lines[8]#digest }
+ if [[ ${recorded_destination} != "${destination_digest}" \
+ || ${recorded_uid} != "${expected_uid}" \
+ || ${recorded_gid} != "${expected_gid}" \
+ || ! ${staging_name} =~ ^\.executor-master-key\.[A-Za-z0-9]{8}$ \
+ || ! ${recorded_uid} =~ ^[0-9]+$ \
+ || ! ${recorded_gid} =~ ^[0-9]+$ ]]; then
+ executor_installer_error \
+ "the master key recovery record does not match this installation"
+ return
+ fi
+ if [[ ${state} == prepared ]]; then
+ if [[ ${recorded_device} != - \
+ || ${recorded_inode} != - \
+ || ${recorded_digest} != - ]]; then
+ executor_installer_error "the prepared master key recovery record is malformed"
+ return
+ fi
+ elif [[ ${state} == staged ]]; then
+ if [[ ! ${recorded_device} =~ ^[0-9]+$ \
+ || ! ${recorded_inode} =~ ^[0-9]+$ \
+ || ! ${recorded_digest} =~ ^[0-9a-f]{64}$ ]]; then
+ executor_installer_error "the staged master key recovery record is malformed"
+ return
+ fi
+ else
+ executor_installer_error "the master key recovery state is unsupported"
+ return
+ fi
+
+ staging_dir="${staging_parent}/${staging_name}"
+ key_tmp="${staging_dir}/master.key"
+ if [[ -e ${staging_dir} || -L ${staging_dir} ]]; then
+ local directory_metadata directory_type directory_owner directory_mode directory_device
+ if ! directory_metadata=$(LC_ALL=C stat \
+ --format='%F|%u|%a|%d' -- "${staging_dir}"); then
+ executor_installer_error \
+ "could not inspect the recorded master key staging directory"
+ return
+ fi
+ IFS='|' read -r directory_type directory_owner directory_mode directory_device \
+ <<< "${directory_metadata}"
+ if [[ ${directory_type} != directory \
+ || ${directory_owner} != "${EUID}" \
+ || ${directory_mode} != 700 \
+ || ${directory_device} != "${staging_device}" ]]; then
+ executor_installer_error \
+ "the recorded master key staging directory is unsafe: ${staging_dir}"
+ return
+ fi
+ fi
+ [[ -e ${path} || -L ${path} ]] && destination_exists=true
+ [[ -e ${key_tmp} || -L ${key_tmp} ]] && alias_exists=true
+
+ if [[ ${state} == prepared ]]; then
+ if [[ ${destination_exists} == true ]]; then
+ executor_installer_error \
+ "a destination key appeared before master key staging completed"
+ return
+ fi
+ if [[ ${alias_exists} == true ]]; then
+ executor_validate_prepared_master_key \
+ "${key_tmp}" "${staging_device}" \
+ "${expected_uid}" "${expected_gid}" || return $?
+ if ! rm -f -- "${key_tmp}"; then
+ executor_installer_error "could not remove the prepared master key"
+ return
+ fi
+ fi
+ elif [[ ${destination_exists} == true ]]; then
+ if ! destination_metadata=$(LC_ALL=C stat \
+ --format='%d|%i' -- "${path}"); then
+ executor_installer_error "could not inspect the recovered master key"
+ return
+ fi
+ IFS='|' read -r destination_device destination_inode \
+ <<< "${destination_metadata}"
+ if [[ ${destination_device} != "${recorded_device}" \
+ || ${destination_inode} != "${recorded_inode}" ]]; then
+ executor_installer_error \
+ "the destination master key does not match the recovery record"
+ return
+ fi
+ if [[ ${alias_exists} == true ]]; then
+ executor_validate_recorded_master_key \
+ "${path}" "${recorded_device}" "${recorded_inode}" \
+ "${expected_uid}" "${expected_gid}" \
+ "${recorded_digest}" 2 || return $?
+ executor_validate_recorded_master_key \
+ "${key_tmp}" "${recorded_device}" "${recorded_inode}" \
+ "${expected_uid}" "${expected_gid}" \
+ "${recorded_digest}" 2 || return $?
+ if ! rm -f -- "${key_tmp}"; then
+ executor_installer_error \
+ "could not remove the recovered master key alias"
+ return
+ fi
+ else
+ executor_validate_recorded_master_key \
+ "${path}" "${recorded_device}" "${recorded_inode}" \
+ "${expected_uid}" "${expected_gid}" \
+ "${recorded_digest}" 1 || return $?
+ fi
+ elif [[ ${alias_exists} == true ]]; then
+ executor_validate_recorded_master_key \
+ "${key_tmp}" "${recorded_device}" "${recorded_inode}" \
+ "${expected_uid}" "${expected_gid}" \
+ "${recorded_digest}" 1 || return $?
+ if ! ln -T -- "${key_tmp}" "${path}"; then
+ executor_installer_error \
+ "the master key destination appeared during recovery"
+ return
+ fi
+ executor_require_filesystem_sync \
+ "${data_dir}" "the recovered published master key" || return $?
+ if ! rm -f -- "${key_tmp}"; then
+ executor_installer_error "could not remove the recovered master key alias"
+ return
+ fi
+ fi
+
+ if [[ -d ${staging_dir} && ! -L ${staging_dir} ]]; then
+ if ! rmdir -- "${staging_dir}"; then
+ executor_installer_error \
+ "could not remove the recovered master key staging directory"
+ return
+ fi
+ fi
+ executor_require_filesystem_sync \
+ "${staging_parent}" "the recovered master key staging parent" || return $?
+ executor_require_filesystem_sync \
+ "${data_dir}" "the recovered master key directory" || return $?
+ executor_retire_master_key_recovery "${recovery_path}"
+}
+
+executor_require_safe_directory() {
+ local path=$1
+
+ if [[ -L ${path} ]]; then
+ executor_installer_error "refusing symbolic link at managed directory: ${path}"
+ return
+ fi
+ if [[ -e ${path} && ! -d ${path} ]]; then
+ executor_installer_error "managed directory path is not a directory: ${path}"
+ return
+ fi
+}
+
+executor_require_regular_file_or_absent() {
+ local path=$1
+
+ if [[ -L ${path} ]]; then
+ executor_installer_error "refusing symbolic link at managed path: ${path}"
+ return
+ fi
+ if [[ -e ${path} && ! -f ${path} ]]; then
+ executor_installer_error "managed path is not a regular file: ${path}"
+ return
+ fi
+}
+
+executor_validate_master_key_staging_parent() {
+ local path=$1
+ local metadata file_type owner mode device
+
+ if [[ ! -e ${path} && ! -L ${path} ]]; then
+ executor_installer_error "master key staging parent does not exist: ${path}"
+ return
+ fi
+ if ! metadata=$(LC_ALL=C stat \
+ --format='%F|%u|%a|%d' -- "${path}"); then
+ executor_installer_error \
+ "could not inspect master key staging parent: ${path}"
+ return
+ fi
+ IFS='|' read -r file_type owner mode device <<< "${metadata}"
+
+ if [[ ${file_type} != "directory" ]]; then
+ executor_installer_error \
+ "master key staging parent must be a non-symlinked directory: ${path}"
+ return
+ fi
+ if [[ ${owner} != "${EUID}" ]]; then
+ executor_installer_error \
+ "master key staging parent must be owned by uid ${EUID}: ${path}"
+ return
+ fi
+ if (((8#${mode} & 0022) != 0)); then
+ executor_installer_error \
+ "master key staging parent must not be group or other writable: ${path}"
+ return
+ fi
+
+ printf '%s\n' "${device}"
+}
+
+executor_validate_master_key() {
+ local path=$1
+ local expected_uid=$2
+ local expected_gid=$3
+ local metadata file_type size links owner group mode
+
+ if [[ ! -e ${path} && ! -L ${path} ]]; then
+ executor_installer_error "master key does not exist: ${path}"
+ return
+ fi
+
+ # GNU stat inspects the directory entry itself unless -L is supplied.
+ # Keeping this non-dereferencing prevents validation from following links.
+ if ! metadata=$(LC_ALL=C stat \
+ --format='%F|%s|%h|%u|%g|%a' -- "${path}"); then
+ executor_installer_error "could not inspect master key metadata: ${path}"
+ return
+ fi
+ IFS='|' read -r file_type size links owner group mode <<< "${metadata}"
+
+ if [[ ${file_type} != "regular file" ]]; then
+ executor_installer_error "master key must be a regular file: ${path}"
+ return
+ fi
+ if [[ ${size} != 32 ]]; then
+ executor_installer_error "${path} must contain exactly 32 bytes"
+ return
+ fi
+ if [[ ${links} != 1 ]]; then
+ executor_installer_error "${path} must not have additional hard links"
+ return
+ fi
+ if [[ ${owner} != "${expected_uid}" || ${group} != "${expected_gid}" ]]; then
+ executor_installer_error \
+ "${path} must already be owned by uid ${expected_uid} and gid ${expected_gid}"
+ return
+ fi
+ if [[ ${mode} != 600 ]]; then
+ executor_installer_error "${path} must already have mode 0600"
+ return
+ fi
+}
+
+executor_create_master_key() (
+ local staging_parent=$1
+ local data_dir=$2
+ local path=$3
+ local expected_uid=$4
+ local expected_gid=$5
+ local staging_device data_device validation_status write_status
+ local recovery_path destination_digest staging_name key_metadata
+ local key_device key_inode key_digest
+ local staging_dir=""
+ local key_tmp=""
+
+ cleanup_master_key_temp() {
+ if [[ -n ${key_tmp} ]]; then
+ rm -f -- "${key_tmp}"
+ fi
+ if [[ -n ${staging_dir} ]]; then
+ rmdir -- "${staging_dir}" 2>/dev/null || true
+ fi
+ }
+ trap cleanup_master_key_temp EXIT
+
+ umask 0077
+ if ! staging_device=$(executor_validate_master_key_staging_parent \
+ "${staging_parent}"); then
+ return 1
+ fi
+ if ! data_device=$(LC_ALL=C stat --format='%d' -- "${data_dir}"); then
+ executor_installer_error "could not inspect master key data directory"
+ return
+ fi
+ if [[ ${staging_device} != "${data_device}" ]]; then
+ executor_installer_error \
+ "master key staging parent and data directory must share a filesystem"
+ return
+ fi
+ if ! staging_dir=$(mktemp -d \
+ "${staging_parent}/.executor-master-key.XXXXXXXX"); then
+ executor_installer_error "could not create the master key staging directory"
+ return
+ fi
+ if ! chmod 0700 "${staging_dir}"; then
+ executor_installer_error "could not secure the master key staging directory"
+ return
+ fi
+ staging_name=${staging_dir##*/}
+ key_tmp="${staging_dir}/master.key"
+ recovery_path="${staging_parent}/.executor-master-key.recovery"
+ if ! destination_digest=$(executor_master_key_path_digest "${path}"); then
+ executor_installer_error "could not identify the master key destination"
+ return
+ fi
+ executor_publish_master_key_recovery \
+ "${recovery_path}" prepared "${destination_digest}" \
+ "${staging_name}" "${expected_uid}" "${expected_gid}" || return $?
+ if executor_write_and_sync_master_key "${key_tmp}"; then
+ :
+ else
+ write_status=$?
+ executor_installer_error "could not generate the master key"
+ return "${write_status}"
+ fi
+ if ! chown "${expected_uid}:${expected_gid}" "${key_tmp}"; then
+ executor_installer_error "could not set temporary master key ownership"
+ return
+ fi
+ if ! chmod 0600 "${key_tmp}"; then
+ executor_installer_error "could not set temporary master key permissions"
+ return
+ fi
+ if executor_validate_master_key \
+ "${key_tmp}" "${expected_uid}" "${expected_gid}"; then
+ :
+ else
+ validation_status=$?
+ return "${validation_status}"
+ fi
+ executor_require_filesystem_sync \
+ "${key_tmp}" "the final master key metadata" || return $?
+ if ! key_metadata=$(LC_ALL=C stat --format='%d|%i' -- "${key_tmp}"); then
+ executor_installer_error "could not inspect the staged master key identity"
+ return
+ fi
+ IFS='|' read -r key_device key_inode <<< "${key_metadata}"
+ if ! key_digest=$(sha256sum -- "${key_tmp}" | awk '{print $1}'); then
+ executor_installer_error "could not digest the staged master key"
+ return
+ fi
+ executor_publish_master_key_recovery \
+ "${recovery_path}" staged "${destination_digest}" \
+ "${staging_name}" "${expected_uid}" "${expected_gid}" \
+ "${key_device}" "${key_inode}" "${key_digest}" || return $?
+ if [[ ${EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_METADATA_SYNC:-} == 1 ]]; then
+ kill -KILL "${BASHPID}"
+ fi
+
+ # A hard-link publication is atomic and fails if any directory entry has
+ # appeared at the destination. The published path is never overwritten.
+ if ln -T -- "${key_tmp}" "${path}"; then
+ :
+ else
+ local publication_status=$?
+ if ! rm -f -- "${key_tmp}"; then
+ executor_installer_error \
+ "could not remove the failed master key staging file"
+ return
+ fi
+ key_tmp=""
+ if ! rmdir -- "${staging_dir}"; then
+ executor_installer_error \
+ "could not remove the failed master key staging directory"
+ return
+ fi
+ staging_dir=""
+ executor_require_filesystem_sync \
+ "${staging_parent}" "the failed master key staging cleanup" || return $?
+ executor_retire_master_key_recovery "${recovery_path}" || return $?
+ executor_installer_error \
+ "master key path appeared during installation; refusing to replace it"
+ return "${publication_status}"
+ fi
+ executor_require_filesystem_sync \
+ "${data_dir}" "the published master key directory" || return $?
+ if [[ ${EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_DESTINATION_SYNC:-} == 1 ]]; then
+ kill -KILL "${BASHPID}"
+ fi
+ if ! rm -f -- "${key_tmp}"; then
+ executor_installer_error "could not remove the temporary master key link"
+ return
+ fi
+ key_tmp=""
+ if [[ ${EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_ALIAS_UNLINK:-} == 1 ]]; then
+ kill -KILL "${BASHPID}"
+ fi
+ if ! rmdir -- "${staging_dir}"; then
+ executor_installer_error "could not remove the master key staging directory"
+ return
+ fi
+ staging_dir=""
+ executor_require_filesystem_sync \
+ "${staging_parent}" "the master key staging parent" || return $?
+ executor_require_filesystem_sync \
+ "${data_dir}" "the master key directory after publication" || return $?
+
+ executor_validate_master_key "${path}" "${expected_uid}" "${expected_gid}" || return $?
+ executor_retire_master_key_recovery "${recovery_path}"
+)
+
+executor_ensure_master_key() (
+ local staging_parent=$1
+ local data_dir=$2
+ local path=$3
+ local expected_uid=$4
+ local expected_gid=$5
+ local recovery_path="${staging_parent}/.executor-master-key.recovery"
+ local lock_path="${staging_parent}/.executor-master-key.lock"
+ local destination_digest staging_device lock_fd lock_metadata fd_identity
+ local lock_type lock_owner lock_mode lock_links lock_size lock_device lock_inode
+
+ if ! command -v flock >/dev/null 2>&1; then
+ executor_installer_error "flock is required for master key installation"
+ return
+ fi
+ if ! staging_device=$(executor_validate_master_key_staging_parent \
+ "${staging_parent}"); then
+ return 1
+ fi
+ umask 0077
+ if [[ ! -e ${lock_path} && ! -L ${lock_path} ]]; then
+ if ! (set -o noclobber; : > "${lock_path}") 2>/dev/null \
+ && [[ ! -e ${lock_path} || -L ${lock_path} ]]; then
+ executor_installer_error "could not create the master key installation lock"
+ return
+ fi
+ fi
+ if ! lock_metadata=$(LC_ALL=C stat \
+ --format='%F|%u|%a|%h|%s|%d|%i' -- "${lock_path}"); then
+ executor_installer_error "could not inspect the master key installation lock"
+ return
+ fi
+ IFS='|' read -r lock_type lock_owner lock_mode lock_links lock_size \
+ lock_device lock_inode <<< "${lock_metadata}"
+ if [[ ${lock_type} != "regular empty file" \
+ || ${lock_owner} != "${EUID}" \
+ || ${lock_mode} != 600 \
+ || ${lock_links} != 1 \
+ || ${lock_size} != 0 \
+ || ${lock_device} != "${staging_device}" ]]; then
+ executor_installer_error \
+ "the master key installation lock has unsafe metadata"
+ return
+ fi
+ if ! exec {lock_fd}<>"${lock_path}"; then
+ executor_installer_error "could not open the master key installation lock"
+ return
+ fi
+ if ! fd_identity=$(LC_ALL=C stat -L \
+ --format='%d|%i' -- "/proc/self/fd/${lock_fd}") \
+ || [[ ${fd_identity} != "${lock_device}|${lock_inode}" ]]; then
+ executor_installer_error "the master key installation lock changed while opening"
+ return
+ fi
+ executor_require_filesystem_sync \
+ "${lock_path}" "the master key installation lock" || return $?
+ executor_require_filesystem_sync \
+ "${staging_parent}" "the master key lock directory" || return $?
+ if ! flock -n -x "${lock_fd}"; then
+ executor_installer_error "another master key installation is already active"
+ return
+ fi
+ if ! destination_digest=$(executor_master_key_path_digest "${path}"); then
+ executor_installer_error "could not identify the master key destination"
+ return
+ fi
+ executor_recover_master_key \
+ "${staging_parent}" "${data_dir}" "${path}" \
+ "${expected_uid}" "${expected_gid}" "${recovery_path}" \
+ "${destination_digest}" "${staging_device}" || return $?
+
+ if [[ -e ${path} || -L ${path} ]]; then
+ executor_validate_master_key \
+ "${path}" "${expected_uid}" "${expected_gid}" || return $?
+ executor_require_filesystem_sync \
+ "${data_dir}" "the existing master key directory" || return $?
+ return 0
+ fi
+
+ executor_create_master_key \
+ "${staging_parent}" "${data_dir}" "${path}" \
+ "${expected_uid}" "${expected_gid}"
+)
diff --git a/scripts/migrate-schema-class.ts b/scripts/migrate-schema-class.ts
index 6eb10c46b..e98ccc574 100644
--- a/scripts/migrate-schema-class.ts
+++ b/scripts/migrate-schema-class.ts
@@ -29,7 +29,6 @@ const IGNORE_DIRS = new Set([
"dist",
"build",
".local",
- ".changeset",
]);
// Files we intentionally leave alone — they contain `Schema.Class` strings as
diff --git a/scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js b/scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js
index 0c34fe6be..78e250ffe 100644
--- a/scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js
+++ b/scripts/oxlint-plugin-executor/rules/no-cross-package-relative-imports.js
@@ -50,7 +50,7 @@ function findPackageRoot(absolutePath) {
function collectPackageRoots() {
const roots = [];
- for (const root of ["packages", "apps", "examples"]) {
+ for (const root of ["packages", "apps", "legacy", "examples"]) {
collectPackageRootsFrom(path.join(repoRoot, root), roots);
}
return roots;
diff --git a/scripts/oxlint-plugin-executor/rules/no-direct-cloud-executor-schema-import.js b/scripts/oxlint-plugin-executor/rules/no-direct-cloud-executor-schema-import.js
index 75ca1db1e..bcbf88555 100644
--- a/scripts/oxlint-plugin-executor/rules/no-direct-cloud-executor-schema-import.js
+++ b/scripts/oxlint-plugin-executor/rules/no-direct-cloud-executor-schema-import.js
@@ -3,9 +3,12 @@ import { getPropertyName, isIdentifier, toRepoRelative, unwrapExpression } from
const message =
"Do not access cloud executor tables directly outside DB schema wiring. Executor-domain table access must go through the scoped SDK adapter so scope_id filtering cannot be skipped.";
-const allowedFiles = new Set(["apps/cloud/src/db/db.ts", "apps/cloud/src/db/db.schema.test.ts"]);
+const allowedFiles = new Set([
+ "legacy/cloud/src/db/db.ts",
+ "legacy/cloud/src/db/db.schema.test.ts",
+]);
-const isCloudSource = (filename) => toRepoRelative(filename).startsWith("apps/cloud/src/");
+const isCloudSource = (filename) => toRepoRelative(filename).startsWith("legacy/cloud/src/");
const isDirectExecutorSchemaImport = (specifier) =>
specifier === "./executor-schema" ||
diff --git a/scripts/package-release-archive.py b/scripts/package-release-archive.py
new file mode 100755
index 000000000..97b4bf919
--- /dev/null
+++ b/scripts/package-release-archive.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+import argparse
+import gzip
+import pathlib
+import tarfile
+
+
+ARCHIVE_MEMBERS = (
+ ("executor", 0o755),
+ ("LICENSE", 0o644),
+ ("THIRD_PARTY_LICENSES.html", 0o644),
+ ("THIRD_PARTY_JAVASCRIPT_LICENSES.json", 0o644),
+)
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="Create a deterministic Executor release archive")
+ parser.add_argument("--source", type=pathlib.Path, required=True)
+ parser.add_argument("--output", type=pathlib.Path, required=True)
+ parser.add_argument("--epoch", type=int, required=True)
+ return parser.parse_args()
+
+
+def main() -> None:
+ args = parse_args()
+ if args.epoch < 0:
+ raise SystemExit("--epoch must be a non-negative Unix timestamp")
+
+ expected = {name for name, _ in ARCHIVE_MEMBERS}
+ actual = {path.name for path in args.source.iterdir()}
+ if actual != expected:
+ raise SystemExit(
+ f"release archive staging members differ: expected {sorted(expected)}, got {sorted(actual)}"
+ )
+
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ with args.output.open("wb") as raw_output:
+ with gzip.GzipFile(filename="", mode="wb", fileobj=raw_output, mtime=args.epoch) as compressed:
+ with tarfile.open(fileobj=compressed, mode="w", format=tarfile.USTAR_FORMAT) as archive:
+ for name, mode in ARCHIVE_MEMBERS:
+ source = args.source / name
+ if not source.is_file() or source.is_symlink():
+ raise SystemExit(f"release archive member must be a regular file: {source}")
+ info = tarfile.TarInfo(name)
+ info.size = source.stat().st_size
+ info.mtime = args.epoch
+ info.mode = mode
+ info.uid = 0
+ info.gid = 0
+ info.uname = ""
+ info.gname = ""
+ with source.open("rb") as contents:
+ archive.addfile(info, contents)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/publish-packages.ts b/scripts/publish-packages.ts
deleted file mode 100644
index 1fa543439..000000000
--- a/scripts/publish-packages.ts
+++ /dev/null
@@ -1,394 +0,0 @@
-#!/usr/bin/env bun
-/**
- * Publishes the public @executor-js/* workspace packages to npm.
- *
- * Walks a hard-coded list of publishable package directories, determines the
- * dist-tag from the version string (anything containing `-` is treated as beta),
- * and packs + publishes each package whose current version is not already on npm.
- *
- * Invoked from `.github/workflows/release.yml` via the `publish:` input on
- * changesets/action after the Version Packages PR has been merged, and locally
- * via `bun run release:publish:packages` (or `--dry-run`).
- */
-import { $ } from "bun";
-import { existsSync } from "node:fs";
-import { readdir, readFile, rm, writeFile } from "node:fs/promises";
-import { dirname, join, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-type Channel = "latest" | "beta";
-
-const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
-
-const PACKAGE_SCOPE = "@executor-js";
-
-/**
- * Workspace-relative paths of the public packages. Kept explicit so a new
- * directory under `packages/plugins/` does not accidentally ship to npm.
- */
-const PUBLIC_PACKAGE_DIRS = [
- "packages/core/fumadb",
- "packages/kernel/core",
- "packages/kernel/runtime-quickjs",
- "packages/core/sdk",
- "packages/core/config",
- "packages/core/execution",
- "packages/core/cli",
- "packages/plugins/example",
- "packages/plugins/file-secrets",
- "packages/plugins/graphql",
- "packages/plugins/keychain",
- "packages/plugins/mcp",
- "packages/plugins/onepassword",
- "packages/plugins/openapi",
-] as const;
-
-const parseArgs = (argv: ReadonlyArray): { dryRun: boolean; prepareOnly: boolean } => {
- let dryRun = false;
- let prepareOnly = false;
- for (const arg of argv) {
- if (arg === "--dry-run") {
- dryRun = true;
- continue;
- }
- if (arg === "--prepare-only") {
- prepareOnly = true;
- continue;
- }
- throw new Error(`Unknown argument: ${arg}`);
- }
- return { dryRun, prepareOnly };
-};
-
-const resolveChannel = (version: string): Channel => (version.includes("-") ? "beta" : "latest");
-
-const readPackageMeta = async (pkgDir: string) => {
- const pkgJsonPath = join(pkgDir, "package.json");
- const pkg = (await Bun.file(pkgJsonPath).json()) as {
- name?: string;
- version?: string;
- private?: boolean;
- };
-
- if (!pkg.name || !pkg.version) {
- throw new Error(`Missing name/version in ${pkgJsonPath}`);
- }
- if (pkg.private === true) {
- throw new Error(`${pkg.name} is marked private and cannot be published`);
- }
-
- return { name: pkg.name, version: pkg.version };
-};
-
-const packageAlreadyPublished = async (name: string, version: string): Promise => {
- const proc = Bun.spawn(["npm", "view", `${name}@${version}`, "version"], {
- stdio: ["ignore", "ignore", "ignore"],
- });
- return (await proc.exited) === 0;
-};
-
-type DependencyBlock = Record;
-type PeerDependenciesMeta = Record;
-type MutablePackageJson = {
- name?: string;
- dependencies?: DependencyBlock;
- devDependencies?: DependencyBlock;
- peerDependencies?: DependencyBlock;
- peerDependenciesMeta?: PeerDependenciesMeta;
- optionalDependencies?: DependencyBlock;
- [key: string]: unknown;
-};
-
-/**
- * Resolves `workspace:*` dependencies between public packages to concrete
- * versions before packing. Returns a restore function that reverts package.json.
- *
- * Workspace-only `@executor-js/*` peer deps (e.g. `@executor-js/api`,
- * `@executor-js/react`) that aren't in `publishable` are stripped from
- * `peerDependencies` (and `peerDependenciesMeta`) entirely — they don't
- * exist on npm, so leaving them in the packed manifest would emit
- * install-time warnings for unresolvable packages.
- */
-const applyWorkspaceVersions = async (
- pkgDir: string,
- publishable: ReadonlySet,
- publishableVersions: ReadonlyMap,
-): Promise<() => Promise> => {
- const isInternalScope = (key: string): boolean => key.startsWith(`${PACKAGE_SCOPE}/`);
-
- const renameDepBlock = (block: DependencyBlock | undefined): DependencyBlock | undefined => {
- if (!block) return block;
- const next: DependencyBlock = {};
- let mutated = false;
- for (const [key, value] of Object.entries(block)) {
- if (publishable.has(key) && value.startsWith("workspace:")) {
- next[key] = publishableVersions.get(key) ?? value;
- mutated = true;
- } else if (isInternalScope(key) && !publishable.has(key)) {
- // Workspace-only `@executor-js/*` regular dep that we don't
- // publish (e.g. `@executor-js/api`). Strip it: it's not in the
- // shipped runtime entries (those imports live in
- // `src/api/*` / `src/react/*` which don't make it into the
- // packed dist), and leaving it in would 404 at install time.
- mutated = true;
- } else {
- next[key] = value;
- }
- }
- return mutated ? next : block;
- };
-
- /**
- * Peer-deps variant of `renameDepBlock`: resolve workspace specifiers for
- * publishable peers, but DROP non-publishable `@executor-js/*` peers.
- * They reference workspace-only packages (`@executor-js/api`,
- * `@executor-js/react`) that don't exist on npm, so leaving them in
- * the packed manifest emits install-time warnings for unresolvable
- * packages. Non-`@executor-js` peers (`react`, `@tanstack/*`,
- * `@effect-atom/*`, etc.) are real npm packages and pass through
- * unchanged.
- */
- const renamePeerDepBlock = (block: DependencyBlock | undefined): DependencyBlock | undefined => {
- if (!block) return block;
- const next: DependencyBlock = {};
- let mutated = false;
- for (const [key, value] of Object.entries(block)) {
- if (publishable.has(key)) {
- next[key] = value.startsWith("workspace:")
- ? (publishableVersions.get(key) ?? value)
- : value;
- if (next[key] !== value) mutated = true;
- } else if (isInternalScope(key)) {
- // Workspace-only `@executor-js/*` peer that we don't publish —
- // strip it so the packed tarball doesn't reference an
- // npm package that doesn't exist.
- mutated = true;
- } else {
- next[key] = value;
- }
- }
- return mutated ? next : block;
- };
-
- /**
- * Strips `peerDependenciesMeta` entries that target an
- * `@executor-js/*` peer we don't publish, mirroring `renamePeerDepBlock`
- * so the meta block can't drift out of sync with the deps block.
- */
- const renamePeerMetaBlock = (
- block: PeerDependenciesMeta | undefined,
- ): PeerDependenciesMeta | undefined => {
- if (!block) return block;
- const next: PeerDependenciesMeta = {};
- let mutated = false;
- for (const [key, value] of Object.entries(block)) {
- if (publishable.has(key)) {
- next[key] = value;
- } else if (isInternalScope(key)) {
- mutated = true;
- } else {
- next[key] = value;
- }
- }
- return mutated ? next : block;
- };
-
- const pkgJsonPath = join(pkgDir, "package.json");
- const original = await readFile(pkgJsonPath, "utf8");
- const pkg = JSON.parse(original) as MutablePackageJson;
- pkg.dependencies = renameDepBlock(pkg.dependencies);
- pkg.devDependencies = renameDepBlock(pkg.devDependencies);
- pkg.peerDependencies = renamePeerDepBlock(pkg.peerDependencies);
- pkg.peerDependenciesMeta = renamePeerMetaBlock(pkg.peerDependenciesMeta);
- pkg.optionalDependencies = renameDepBlock(pkg.optionalDependencies);
- const pkgNext = `${JSON.stringify(pkg, null, 2)}\n`;
- if (pkgNext !== original) {
- await writeFile(pkgJsonPath, pkgNext);
- return async () => {
- await writeFile(pkgJsonPath, original);
- };
- }
- return async () => {};
-};
-
-/**
- * Applies `publishConfig` field overrides to package.json in place, returning a
- * function that restores the original file. `bun pm pack` does not substitute
- * `publishConfig.exports` / `publishConfig.main` etc at pack time (npm does,
- * but only for a subset of fields and only for `npm pack`), so we rewrite the
- * file ourselves so the packed tarball has the correct `exports` pointing at
- * `dist/` instead of the dev-time `src/index.ts`.
- */
-const applyPublishConfig = async (pkgDir: string): Promise<() => Promise> => {
- const pkgJsonPath = join(pkgDir, "package.json");
- const original = await readFile(pkgJsonPath, "utf8");
- const parsed = JSON.parse(original) as {
- publishConfig?: Record;
- [key: string]: unknown;
- };
-
- const publishConfig = parsed.publishConfig;
- if (!publishConfig || typeof publishConfig !== "object") {
- return async () => {};
- }
-
- // Fields we allow publishConfig to override. `access`/`tag`/`registry` are
- // real npm publish-time config keys — they must NOT be hoisted into the
- // top-level manifest.
- const overridable = new Set([
- "exports",
- "main",
- "module",
- "types",
- "typings",
- "bin",
- "browser",
- "files",
- ]);
-
- const nextPublishConfig: Record = {};
- let mutated = false;
- for (const [key, value] of Object.entries(publishConfig)) {
- if (overridable.has(key)) {
- parsed[key] = value;
- mutated = true;
- } else {
- nextPublishConfig[key] = value;
- }
- }
-
- if (!mutated) {
- return async () => {};
- }
-
- if (Object.keys(nextPublishConfig).length === 0) {
- delete parsed.publishConfig;
- } else {
- parsed.publishConfig = nextPublishConfig;
- }
-
- await writeFile(pkgJsonPath, `${JSON.stringify(parsed, null, 2)}\n`);
- return async () => {
- await writeFile(pkgJsonPath, original);
- };
-};
-
-const publishPackage = async (
- pkgDir: string,
- dryRun: boolean,
- publishable: ReadonlySet,
- publishableVersions: ReadonlyMap,
-) => {
- const { name, version } = await readPackageMeta(pkgDir);
- const channel = resolveChannel(version);
-
- if (!existsSync(join(pkgDir, "dist"))) {
- throw new Error(`Missing dist/ in ${pkgDir}. Did you run 'bun run build:packages'?`);
- }
-
- console.log(`[pack] ${name}@${version} (${channel})${dryRun ? " [dry-run]" : ""}`);
-
- // Clean any stale tarballs from previous runs so our readdir finds exactly
- // the archive produced by the pack below.
- const stale = (await readdir(pkgDir)).filter((entry) => entry.endsWith(".tgz"));
- for (const entry of stale) {
- await rm(join(pkgDir, entry), { force: true });
- }
-
- const restoreWorkspaceVersions = await applyWorkspaceVersions(
- pkgDir,
- publishable,
- publishableVersions,
- );
- const restorePublishConfig = await applyPublishConfig(pkgDir);
- try {
- await $`bun pm pack`.cwd(pkgDir);
- } finally {
- await restorePublishConfig();
- await restoreWorkspaceVersions();
- }
-
- const produced = (await readdir(pkgDir)).filter((entry) => entry.endsWith(".tgz"));
- if (produced.length !== 1) {
- throw new Error(
- `Expected exactly 1 .tgz in ${pkgDir}, found ${produced.length}: ${produced.join(", ")}`,
- );
- }
- const tarball = produced[0]!;
-
- if (dryRun) {
- return;
- }
-
- // Skip publishing already-shipped versions. The pack still ran above so
- // smoke tests / pkg-pr-new previews always have a fresh tarball.
- if (await packageAlreadyPublished(name, version)) {
- console.log(`[skip] ${name}@${version} already on npm`);
- return;
- }
-
- console.log(`[publish] ${name}@${version} (${channel})`);
-
- const args = ["publish", tarball, "--access", "public", "--tag", channel];
- if (process.env.GITHUB_ACTIONS === "true") {
- args.push("--provenance");
- }
- await $`npm ${args}`.cwd(pkgDir);
-};
-
-/**
- * Rewrite-in-place mode for the pkg.pr.new preview workflow. pkg-pr-new runs
- * `bun pm pack` against each workspace package directly, which can't see our
- * `publishConfig.exports` overrides or resolve `workspace:*` references. This
- * walks every public package, applies the same rewrites `publishPackage`
- * does, and intentionally leaves them mutated — the CI job is ephemeral and
- * the workflow tears down right after pkg-pr-new finishes.
- */
-const prepareOnePackage = async (
- pkgDir: string,
- publishable: ReadonlySet,
- publishableVersions: ReadonlyMap,
-) => {
- const { name, version } = await readPackageMeta(pkgDir);
- if (!existsSync(join(pkgDir, "dist"))) {
- throw new Error(`Missing dist/ in ${pkgDir}. Did you run 'bun run build:packages'?`);
- }
- console.log(`[prepare] ${name}@${version}`);
- await applyWorkspaceVersions(pkgDir, publishable, publishableVersions);
- await applyPublishConfig(pkgDir);
-};
-
-const main = async () => {
- const { dryRun, prepareOnly } = parseArgs(process.argv.slice(2));
-
- // Each package's own version determines its dist-tag (pre-release versions
- // with `-` publish to `beta`, everything else to `latest`). Packages are
- // only skipped when their current version is already on npm.
- const mode = prepareOnly ? " [prepare-only]" : dryRun ? " [dry-run]" : "";
- console.log(`Publishing ${PACKAGE_SCOPE} packages${mode}`);
-
- await $`bun run build:packages`.cwd(repoRoot);
-
- // Snapshot the public package names and versions up front so public
- // workspace dependencies can be written as exact versions in packed tarballs.
- const publishable = new Set();
- const publishableVersions = new Map();
- for (const relDir of PUBLIC_PACKAGE_DIRS) {
- const pkg = await readPackageMeta(join(repoRoot, relDir));
- publishable.add(pkg.name);
- publishableVersions.set(pkg.name, pkg.version);
- }
-
- if (prepareOnly) {
- for (const relDir of PUBLIC_PACKAGE_DIRS) {
- await prepareOnePackage(join(repoRoot, relDir), publishable, publishableVersions);
- }
- return;
- }
-
- for (const relDir of PUBLIC_PACKAGE_DIRS) {
- await publishPackage(join(repoRoot, relDir), dryRun, publishable, publishableVersions);
- }
-};
-
-await main();
diff --git a/scripts/reap-dev-servers.ts b/scripts/reap-dev-servers.ts
index 5a9aa9c62..1fd312bae 100644
--- a/scripts/reap-dev-servers.ts
+++ b/scripts/reap-dev-servers.ts
@@ -31,7 +31,7 @@ for (const line of ps.split("\n")) {
if (Number(pidText) === process.pid) continue;
// The checkout root is whatever absolute path prefixes node_modules/ or a
// workspace dir in the command line.
- const pathMatch = command!.match(/(\/[^ ]*?)\/(?:node_modules|apps|packages|e2e)\//);
+ const pathMatch = command!.match(/(\/[^ ]*?)\/(?:node_modules|apps|legacy|packages|e2e)\//);
const checkout = pathMatch?.[1];
const orphan = checkout !== undefined && !existsSync(checkout);
candidates.push({ pid: Number(pidText), command: command!.slice(0, 160), checkout, orphan });
diff --git a/scripts/run-local-e2e.ts b/scripts/run-local-e2e.ts
new file mode 100644
index 000000000..ec453202b
--- /dev/null
+++ b/scripts/run-local-e2e.ts
@@ -0,0 +1,22 @@
+import { execFileSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+const root = fileURLToPath(new URL("..", import.meta.url));
+
+execFileSync("bun", ["run", "build"], {
+ cwd: fileURLToPath(new URL("../web", import.meta.url)),
+ stdio: "inherit",
+});
+execFileSync("cargo", ["build", "--bin", "executor"], {
+ cwd: root,
+ env: { ...process.env, EXECUTOR_WEB_ASSETS_DIR: "web/build" },
+ stdio: "inherit",
+});
+execFileSync("bun", ["run", "test:unit"], {
+ cwd: fileURLToPath(new URL("../e2e", import.meta.url)),
+ stdio: "inherit",
+});
+execFileSync("bun", ["run", "test"], {
+ cwd: fileURLToPath(new URL("../e2e", import.meta.url)),
+ stdio: "inherit",
+});
diff --git a/scripts/smoke-docs-install.ts b/scripts/smoke-docs-install.ts
deleted file mode 100644
index 0a74fcd9a..000000000
--- a/scripts/smoke-docs-install.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/env bun
-/**
- * Verifies the SDK install command shown in docs resolves to a package we can
- * actually publish and consume.
- *
- * This intentionally installs the packed tarball under the documented package
- * name (`@executor-js/sdk`) instead of relying on workspace resolution.
- */
-import { $ } from "bun";
-import { existsSync, readdirSync } from "node:fs";
-import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
-import { tmpdir } from "node:os";
-import { dirname, join, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
-const documentedPackages = ["@executor-js/sdk", "@executor-js/plugin-openapi"] as const;
-const publicPackageDirs = [
- "packages/core/fumadb",
- "packages/core/sdk",
- "packages/core/config",
- "packages/plugins/openapi",
-] as const;
-
-const readPackageName = async (pkgDir: string): Promise => {
- const raw = await readFile(join(pkgDir, "package.json"), "utf8");
- return (JSON.parse(raw) as { name: string }).name;
-};
-
-const findTarball = (pkgDir: string, packageName: string): string => {
- const tarball = readdirSync(pkgDir).find((entry) => entry.endsWith(".tgz"));
- if (!tarball) {
- throw new Error(`No packed tarball found for ${packageName}`);
- }
- return join(pkgDir, tarball);
-};
-
-console.log(`[docs-smoke] packing documented SDK packages`);
-await $`bun run scripts/publish-packages.ts --dry-run`.cwd(repoRoot);
-
-const tarballs = new Map();
-for (const relDir of publicPackageDirs) {
- const pkgDir = join(repoRoot, relDir);
- const name = await readPackageName(pkgDir);
- tarballs.set(name, findTarball(pkgDir, name));
-}
-
-const tmp = await mkdtemp(join(tmpdir(), "executor-docs-install-"));
-
-try {
- const dependencies: Record = {};
- const overrides: Record = {};
- for (const [name, tarball] of tarballs) {
- overrides[name] = `file:${tarball}`;
- }
- for (const name of documentedPackages) {
- const tarball = tarballs.get(name);
- if (!tarball) {
- throw new Error(`No packed tarball found for documented package ${name}`);
- }
- dependencies[name] = `file:${tarball}`;
- }
-
- const fixture = {
- name: "executor-docs-install-smoke",
- version: "0.0.0",
- private: true,
- type: "module",
- dependencies,
- overrides,
- };
-
- await writeFile(join(tmp, "package.json"), `${JSON.stringify(fixture, null, 2)}\n`);
-
- console.log(`[docs-smoke] npm install ${documentedPackages.join(" ")}`);
- await $`npm install --no-audit --no-fund --legacy-peer-deps`.cwd(tmp);
-
- for (const packageName of documentedPackages) {
- const installedManifest = join(tmp, "node_modules", ...packageName.split("/"), "package.json");
- if (!existsSync(installedManifest)) {
- throw new Error(`Expected ${packageName} to be installed at ${installedManifest}`);
- }
- const manifest = await import(installedManifest, { with: { type: "json" } });
- if (manifest.default.name !== packageName) {
- throw new Error(
- `Expected installed package name to be ${packageName}, got ${manifest.default.name}`,
- );
- }
- }
-
- console.log(`[docs-smoke] import documented SDK packages`);
- await $`node --input-type=module --eval ${`const sdk = await import("@executor-js/sdk"); const openapi = await import("@executor-js/plugin-openapi"); if (typeof sdk.createExecutor !== "function") throw new Error("missing createExecutor"); if (typeof openapi.openApiPlugin !== "function") throw new Error("missing openApiPlugin");`}`.cwd(
- tmp,
- );
-} finally {
- await rm(tmp, { recursive: true, force: true });
-}
diff --git a/scripts/smoke-release-container.sh b/scripts/smoke-release-container.sh
new file mode 100755
index 000000000..f8f057359
--- /dev/null
+++ b/scripts/smoke-release-container.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+image="${1:?container image is required}"
+canonical_web_dir="${2:?canonical web directory is required}"
+container_name="${3:?container name is required}"
+volume_name="${4:?volume name is required}"
+log_file=""
+
+cleanup() {
+ local status=$1
+ trap - EXIT
+ if docker inspect "$container_name" >/dev/null 2>&1; then
+ if [[ "$status" -ne 0 ]]; then
+ docker logs "$container_name" >&2 || true
+ fi
+ docker rm --force "$container_name" >/dev/null 2>&1 || true
+ fi
+ docker volume rm --force "$volume_name" >/dev/null 2>&1 || true
+ if [[ -n "$log_file" ]]; then
+ rm -f "$log_file"
+ fi
+ exit "$status"
+}
+trap 'cleanup "$?"' EXIT
+
+docker volume create "$volume_name" >/dev/null
+docker run --detach \
+ --name "$container_name" \
+ --pull=never \
+ --init \
+ --read-only \
+ --cap-drop ALL \
+ --security-opt no-new-privileges \
+ --memory 2g \
+ --pids-limit 512 \
+ --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
+ --mount "source=${volume_name},target=/var/lib/executor" \
+ --publish 127.0.0.1::4788 \
+ "$image" >/dev/null
+
+endpoint="$(docker port "$container_name" 4788/tcp)"
+port="${endpoint##*:}"
+[[ "$port" =~ ^[0-9]+$ ]] \
+ || { printf 'could not resolve mapped container port from %s\n' "$endpoint" >&2; exit 1; }
+base_url="http://127.0.0.1:${port}"
+
+ready=false
+for _ in {1..120}; do
+ if curl --fail --silent --show-error "${base_url}/healthz" >/dev/null 2>&1; then
+ ready=true
+ break
+ fi
+ if [[ "$(docker inspect --format '{{.State.Status}}' "$container_name")" == "exited" ]]; then
+ break
+ fi
+ sleep 1
+done
+[[ "$ready" == true ]] \
+ || { printf 'release container did not become healthy\n' >&2; exit 1; }
+
+log_file="$(mktemp "${TMPDIR:-/tmp}/executor-container-smoke.XXXXXXXX")"
+docker logs "$container_name" > "$log_file" 2>&1
+grep -F 'Complete first-boot setup at:' "$log_file" >/dev/null
+grep -E 'http://127\.0\.0\.1:4788/setup#token=.+' "$log_file" >/dev/null
+rm -f "$log_file"
+log_file=""
+
+"$(dirname "${BASH_SOURCE[0]}")/smoke-release-server.sh" \
+ "$base_url" \
+ "$canonical_web_dir"
+
+docker stop --time 120 "$container_name" >/dev/null
+[[ "$(docker inspect --format '{{.State.ExitCode}}' "$container_name")" == "0" ]]
diff --git a/scripts/smoke-release-server.sh b/scripts/smoke-release-server.sh
new file mode 100755
index 000000000..f0a7225e5
--- /dev/null
+++ b/scripts/smoke-release-server.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+base_url="${1:?base URL is required}"
+canonical_web_dir="${2:?canonical web directory is required}"
+
+[[ -f "${canonical_web_dir}/index.html" ]] \
+ || { printf 'missing canonical index.html in %s\n' "$canonical_web_dir" >&2; exit 1; }
+
+response_dir="$(mktemp -d "${TMPDIR:-/tmp}/executor-web-smoke.XXXXXXXX")"
+trap 'rm -rf "$response_dir"' EXIT
+
+curl --fail --globoff --silent --show-error "${base_url}/healthz" >/dev/null
+curl --fail --globoff --silent --show-error --output "${response_dir}/root" "${base_url}/"
+cmp "${canonical_web_dir}/index.html" "${response_dir}/root"
+
+asset_list="${response_dir}/assets"
+find "$canonical_web_dir" -type f | LC_ALL=C sort > "$asset_list"
+asset_index=0
+while IFS= read -r asset; do
+ relative="${asset#${canonical_web_dir}/}"
+ case "$relative" in
+ *$'\n'*|*$'\r'*|*' '*|*'?'*|*'#'*|*'%'*)
+ printf 'canonical web asset has an unsafe URL path: %s\n' "$relative" >&2
+ exit 1
+ ;;
+ esac
+ asset_index=$((asset_index + 1))
+ served="${response_dir}/served-${asset_index}"
+ curl --fail --globoff --silent --show-error \
+ --output "$served" \
+ "${base_url}/${relative}"
+ cmp "$asset" "$served"
+done < "$asset_list"
+
+[[ "$asset_index" -gt 1 ]] \
+ || { printf 'canonical web payload must contain more than index.html\n' >&2; exit 1; }
+
+printf 'verified %s canonical web assets from %s\n' "$asset_index" "$base_url"
diff --git a/scripts/smoke-test-packed.ts b/scripts/smoke-test-packed.ts
deleted file mode 100644
index 4b87c603e..000000000
--- a/scripts/smoke-test-packed.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-#!/usr/bin/env bun
-/**
- * Pack-and-import smoke test for the public `@executor-js/*` packages.
- *
- * Reproduces what an external npm consumer experiences:
- *
- * 1. Pack each publishable workspace via `publish-packages.ts
- * --dry-run`, so `publishConfig.exports` and `workspace:*` rewrites
- * are applied to the tarball.
- * 2. For each package, install the tarball into a fresh temp dir
- * with npm `overrides` pointing every other `@executor-js/*`
- * transitive dep at its local tarball — otherwise npm pulls the
- * currently-published version from the registry, which masks any
- * internal-API mismatch this branch introduced.
- * 3. Read the installed package.json (the post-`publishConfig` view)
- * and dynamically `import()` every subpath in its `exports` map.
- *
- * Failures for `@executor-js/*` package not-found are hard failures —
- * that's the regression class where private workspace packages leak
- * into a public bundle. Failures for other peers (`react`, `effect`,
- * `@tanstack/*`, etc.) are downgraded to warnings, since they reflect
- * a missing peer in the smoke environment, not a bug in the bundle.
- *
- * Invoke via `bun run release:smoke:packages`.
- */
-import { $ } from "bun";
-import { existsSync, readdirSync } from "node:fs";
-import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
-import { tmpdir } from "node:os";
-import { dirname, join, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
-
-const PUBLIC_PACKAGE_DIRS = [
- "packages/core/fumadb",
- "packages/kernel/core",
- "packages/kernel/runtime-quickjs",
- "packages/core/sdk",
- "packages/core/config",
- "packages/core/execution",
- "packages/core/cli",
- "packages/plugins/example",
- "packages/plugins/file-secrets",
- "packages/plugins/graphql",
- "packages/plugins/keychain",
- "packages/plugins/mcp",
- "packages/plugins/onepassword",
- "packages/plugins/openapi",
-] as const;
-
-type PackageJson = {
- name: string;
- version: string;
- exports?: Record;
-};
-
-const readPackageJson = async (pkgDir: string): Promise => {
- const raw = await readFile(join(pkgDir, "package.json"), "utf8");
- return JSON.parse(raw) as PackageJson;
-};
-
-const findTarball = (pkgDir: string): string | null => {
- const tgz = readdirSync(pkgDir).find((entry) => entry.endsWith(".tgz"));
- return tgz ? join(pkgDir, tgz) : null;
-};
-
-const subpathsToTest = (pkg: PackageJson): readonly string[] => Object.keys(pkg.exports ?? {});
-
-const importSpecifier = (pkgName: string, subpath: string): string =>
- subpath === "." ? pkgName : `${pkgName}${subpath.slice(1)}`;
-
-type SmokeFailure = {
- readonly pkg: string;
- readonly subpath: string;
- readonly reason: string;
-};
-
-const PRIVATE_PACKAGE_RE = /Cannot find package '(@executor-js\/[^']+)'/;
-
-const firstMeaningfulLine = (stderr: string): string => {
- const lines = stderr
- .split("\n")
- .map((line) => line.trim())
- .filter(Boolean);
- const errorLine = lines.find((line) => /^(\w*Error|Cannot find)/i.test(line));
- if (errorLine) return errorLine;
- for (const line of lines) {
- if (line.startsWith("node:")) continue;
- if (line.startsWith("file://")) continue;
- if (line.startsWith("at ")) continue;
- if (line.startsWith("import ")) continue;
- if (line.startsWith("throw ")) continue;
- if (line === "^" || /^\^+$/.test(line)) continue;
- return line;
- }
- return lines[0] ?? "(no stderr)";
-};
-
-type Tarballs = ReadonlyMap;
-
-const smokeTestPackage = async (
- pkgDir: string,
- tarballs: Tarballs,
- failures: SmokeFailure[],
-): Promise => {
- const pkg = await readPackageJson(pkgDir);
- const tarballPath = tarballs.get(pkg.name);
- if (!tarballPath) {
- failures.push({ pkg: pkg.name, subpath: "", reason: "no tarball produced" });
- return;
- }
-
- const tmp = await mkdtemp(join(tmpdir(), "executor-smoke-"));
- try {
- // npm `overrides` forces transitive `@executor-js/*` deps to resolve
- // to their local tarball instead of whatever's published on npm.
- // Without this, a plugin built against an unreleased symbol in
- // `@executor-js/sdk` would silently install the published `sdk`
- // and fail at import — a real bug, but not the bundle bug we're
- // here to catch.
- const overrides: Record = {};
- for (const [name, path] of tarballs) {
- overrides[name] = `file:${path}`;
- }
- const fixture = {
- name: "executor-smoke-fixture",
- version: "0.0.0",
- private: true,
- type: "module",
- dependencies: { [pkg.name]: `file:${tarballPath}` },
- overrides,
- };
- await writeFile(join(tmp, "package.json"), `${JSON.stringify(fixture, null, 2)}\n`);
-
- const install = await $`npm install --no-audit --no-fund --legacy-peer-deps`
- .cwd(tmp)
- .quiet()
- .nothrow();
- if (install.exitCode !== 0) {
- failures.push({
- pkg: pkg.name,
- subpath: "",
- reason: install.stderr.toString().trim().split("\n").slice(-3).join("\n"),
- });
- return;
- }
-
- // Read the installed manifest — that's the real published view
- // (publishConfig.exports applied, workspace specifiers resolved).
- const installedPkg = await readPackageJson(join(tmp, "node_modules", ...pkg.name.split("/")));
- const subpaths = subpathsToTest(installedPkg);
- if (subpaths.length === 0) {
- failures.push({
- pkg: pkg.name,
- subpath: "",
- reason: "no exports declared in published manifest",
- });
- return;
- }
-
- for (const subpath of subpaths) {
- const spec = importSpecifier(pkg.name, subpath);
- const probe =
- await $`node --input-type=module --eval ${`await import(${JSON.stringify(spec)});`}`
- .cwd(tmp)
- .quiet()
- .nothrow();
- if (probe.exitCode === 0) {
- console.log(` ok ${spec}`);
- continue;
- }
- const stderr = probe.stderr.toString();
- const privateMatch = stderr.match(PRIVATE_PACKAGE_RE);
- if (privateMatch) {
- const offending = privateMatch[1];
- failures.push({
- pkg: pkg.name,
- subpath,
- reason: `published bundle imports private workspace package '${offending}'`,
- });
- console.log(` FAIL ${spec} — references private '${offending}'`);
- continue;
- }
- const peerMatch =
- stderr.match(/Cannot find package '([^']+)'/) ??
- stderr.match(/Cannot find module '([^']+)'/);
- const detail = peerMatch ? `missing peer '${peerMatch[1]}'` : firstMeaningfulLine(stderr);
- console.log(` skip ${spec} — ${detail}`);
- }
- } finally {
- await rm(tmp, { recursive: true, force: true });
- }
-};
-
-const main = async () => {
- console.log("[smoke] packing public packages via publish-packages.ts --dry-run");
- await $`bun run scripts/publish-packages.ts --dry-run`.cwd(repoRoot);
-
- const tarballs = new Map();
- for (const relDir of PUBLIC_PACKAGE_DIRS) {
- const pkgDir = join(repoRoot, relDir);
- if (!existsSync(pkgDir)) continue;
- const pkg = await readPackageJson(pkgDir);
- const tarball = findTarball(pkgDir);
- if (tarball) tarballs.set(pkg.name, tarball);
- }
-
- const failures: SmokeFailure[] = [];
- for (const relDir of PUBLIC_PACKAGE_DIRS) {
- const pkgDir = join(repoRoot, relDir);
- if (!existsSync(pkgDir)) {
- failures.push({ pkg: relDir, subpath: "", reason: "missing dir" });
- continue;
- }
- const pkg = await readPackageJson(pkgDir);
- console.log(`[smoke] ${pkg.name}`);
- await smokeTestPackage(pkgDir, tarballs, failures);
- }
-
- if (failures.length === 0) {
- console.log("[smoke] all packages OK");
- return;
- }
-
- console.error(`\n[smoke] ${failures.length} failure(s):`);
- for (const f of failures) {
- console.error(` - ${f.pkg}${f.subpath ? ` (${f.subpath})` : ""}: ${f.reason}`);
- }
- process.exit(1);
-};
-
-await main();
diff --git a/scripts/test-bounded-log.sh b/scripts/test-bounded-log.sh
new file mode 100755
index 000000000..61a933060
--- /dev/null
+++ b/scripts/test-bounded-log.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
+readonly REPOSITORY_ROOT=$(cd -- "${SCRIPT_DIR}/.." && pwd -P)
+readonly LOGGER="${REPOSITORY_ROOT}/packaging/launchd/bounded-log.sh"
+readonly MIB=$((1024 * 1024))
+
+test_root=$(mktemp -d)
+trap 'rm -rf -- "${test_root}"' EXIT
+test_number=0
+
+pass() {
+ test_number=$((test_number + 1))
+ printf 'ok %s - %s\n' "${test_number}" "$1"
+}
+
+fail() {
+ printf 'not ok %s - %s\n' "$((test_number + 1))" "$1" >&2
+ exit 1
+}
+
+file_mode() {
+ if stat -f '%Lp' "$1" >/dev/null 2>&1; then
+ stat -f '%Lp' "$1"
+ else
+ stat --format='%a' -- "$1"
+ fi
+}
+
+normal_log="${test_root}/normal.log"
+normal_expected="${test_root}/normal.expected"
+printf 'first line\nsecond line\nthird line without newline' > "$normal_expected"
+"$LOGGER" "$normal_log" < "$normal_expected"
+if ! cmp -s "$normal_expected" "$normal_log"; then
+ fail "normal line input was not preserved exactly"
+fi
+if [[ "$(file_mode "$normal_log")" != 600 ]]; then
+ fail "normal log does not have mode 0600"
+fi
+pass "normal lines and a final partial line are preserved"
+
+symlink_target="${test_root}/symlink-target"
+printf 'do not touch' > "$symlink_target"
+symlink_log="${test_root}/symlink.log"
+ln -s "$symlink_target" "$symlink_log"
+if "$LOGGER" "$symlink_log" < "$normal_expected" 2>/dev/null; then
+ fail "symbolic-link log path was accepted"
+fi
+if [[ "$(cat "$symlink_target")" != 'do not touch' ]]; then
+ fail "symbolic-link log target was changed"
+fi
+pass "symbolic-link log path is rejected without touching its target"
+
+large_input="${test_root}/large.input"
+segment_c="${test_root}/segment-C"
+segment_d="${test_root}/segment-D"
+segment_e="${test_root}/segment-E"
+segment_f="${test_root}/segment-F"
+: > "$large_input"
+for letter in A B C D E; do
+ segment="${test_root}/segment-${letter}"
+ dd if=/dev/zero bs="$MIB" count=1 2>/dev/null \
+ | tr '\000' "$letter" > "$segment"
+ cat "$segment" >> "$large_input"
+done
+dd if=/dev/zero bs=123 count=1 2>/dev/null \
+ | tr '\000' F > "$segment_f"
+cat "$segment_f" >> "$large_input"
+
+large_log="${test_root}/large.log"
+"$LOGGER" "$large_log" < "$large_input"
+if ! cmp -s "$segment_f" "$large_log"; then
+ fail "current log does not contain the final partial generation"
+fi
+if ! cmp -s "$segment_e" "${large_log}.1"; then
+ fail "first rotated generation is incorrect"
+fi
+if ! cmp -s "$segment_d" "${large_log}.2"; then
+ fail "second rotated generation is incorrect"
+fi
+if ! cmp -s "$segment_c" "${large_log}.3"; then
+ fail "third rotated generation is incorrect"
+fi
+if [[ -e "${large_log}.4" ]]; then
+ fail "logger retained more than three rotated generations"
+fi
+for retained_log in \
+ "$large_log" \
+ "${large_log}.1" \
+ "${large_log}.2" \
+ "${large_log}.3"; do
+ if [[ "$(wc -c < "$retained_log")" -gt "$MIB" ]]; then
+ fail "retained log exceeds 1 MiB: ${retained_log}"
+ fi
+ if [[ "$(file_mode "$retained_log")" != 600 ]]; then
+ fail "retained log does not have mode 0600: ${retained_log}"
+ fi
+done
+pass "large newline-free input stays within four secure bounded files"
+
+printf '1..%s\n' "$test_number"
diff --git a/scripts/test-install-launchd-safety.sh b/scripts/test-install-launchd-safety.sh
new file mode 100755
index 000000000..7aaa6af55
--- /dev/null
+++ b/scripts/test-install-launchd-safety.sh
@@ -0,0 +1,673 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+readonly INSTALLER="${REPOSITORY_ROOT}/scripts/install-launchd.sh"
+readonly TEST_ROOT="$(mktemp -d)"
+
+cleanup() {
+ rm -rf "$TEST_ROOT"
+}
+trap cleanup EXIT
+
+test_number=0
+run_case() {
+ local name=$1
+
+ CASE_ROOT="${TEST_ROOT}/${name}"
+ HOME_DIR="${CASE_ROOT}/home"
+ DATA_DIR="${HOME_DIR}/Library/Application Support/Executor"
+ PLIST_DIR="${HOME_DIR}/Library/LaunchAgents"
+ SERVICE_DIR="${HOME_DIR}/.executor/service"
+ STUB_DIR="${CASE_ROOT}/bin"
+ EXECUTOR_BINARY="${CASE_ROOT}/executor"
+ OUTPUT="${CASE_ROOT}/output"
+ mkdir -p "$DATA_DIR" "$PLIST_DIR" "$STUB_DIR"
+ printf '#!/bin/sh\nprintf "Darwin\\n"\n' > "${STUB_DIR}/uname"
+ chmod 0700 "${STUB_DIR}/uname"
+ printf '#!/bin/sh\nexit 0\n' > "$EXECUTOR_BINARY"
+ chmod 0700 "$EXECUTOR_BINARY"
+}
+
+expect_rejected() {
+ if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - unsafe launchd destination was accepted\n' "$test_number"
+ cat "$OUTPUT"
+ exit 1
+ fi
+}
+
+private_file_metadata() {
+ if stat --version >/dev/null 2>&1; then
+ stat -c '%u|%a|%h' "$1"
+ else
+ stat -f '%u|%Lp|%l' "$1"
+ fi
+}
+
+test_number=$((test_number + 1))
+run_case dangling-template
+victim="${CASE_ROOT}/victim"
+ln -s "$victim" "${DATA_DIR}/mcp-stdio-templates.json"
+expect_rejected
+if [[ ! -e "$victim" && -L "${DATA_DIR}/mcp-stdio-templates.json" ]]; then
+ printf 'ok %s - dangling template symlink is rejected without creating its target\n' \
+ "$test_number"
+else
+ printf 'not ok %s - dangling template symlink changed its target\n' "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case wrapper-symlink
+victim="${CASE_ROOT}/victim-directory"
+mkdir "$victim"
+mkdir -p "$SERVICE_DIR"
+ln -s "$victim" "${SERVICE_DIR}/run-launchd.sh"
+expect_rejected
+if [[ -L "${SERVICE_DIR}/run-launchd.sh" \
+ && ! -e "${DATA_DIR}/mcp-stdio-templates.json" \
+ && -z "$(find "$victim" -mindepth 1 -print -quit)" ]]; then
+ printf 'ok %s - wrapper symlink-to-directory fails before managed writes\n' "$test_number"
+else
+ printf 'not ok %s - wrapper symlink redirected or allowed managed writes\n' "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case logger-directory
+mkdir -p "$SERVICE_DIR"
+mkdir "${SERVICE_DIR}/bounded-log.sh"
+expect_rejected
+if [[ -d "${SERVICE_DIR}/bounded-log.sh" \
+ && ! -e "${DATA_DIR}/mcp-stdio-templates.json" ]]; then
+ printf 'ok %s - nonregular logger destination fails before managed writes\n' "$test_number"
+else
+ printf 'not ok %s - nonregular logger destination was modified\n' "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case plist-symlink
+victim="${CASE_ROOT}/victim-directory"
+mkdir "$victim"
+ln -s "$victim" "${PLIST_DIR}/dev.executor.gateway.plist"
+expect_rejected
+if [[ -L "${PLIST_DIR}/dev.executor.gateway.plist" \
+ && ! -e "${DATA_DIR}/mcp-stdio-templates.json" \
+ && -z "$(find "$victim" -mindepth 1 -print -quit)" ]]; then
+ printf 'ok %s - plist symlink-to-directory fails before managed writes\n' "$test_number"
+else
+ printf 'not ok %s - plist symlink redirected or allowed managed writes\n' "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case manifest-publication-failure
+ledger="${CASE_ROOT}/launchctl-ledger"
+real_mktemp="$(command -v mktemp)"
+printf '#!/bin/sh\ncase "$1" in *\/.service-install.*) exit 1 ;; esac\nexec %s "$@"\n' \
+ "$real_mktemp" > "${STUB_DIR}/mktemp"
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> %q\nexit 0\n' \
+ "$ledger" > "${STUB_DIR}/launchctl"
+chmod 0700 "${STUB_DIR}/mktemp" "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - manifest publication failure was accepted\n' "$test_number"
+ exit 1
+fi
+if [[ -f "${PLIST_DIR}/dev.executor.gateway.plist" \
+ && ! -e "${HOME_DIR}/.executor/service/service-install.manifest" \
+ && -f "${HOME_DIR}/.executor/service/service-install.recovery" \
+ && ! -s "$ledger" ]]; then
+ rm -f "${STUB_DIR}/mktemp"
+ HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+ if [[ -f "${HOME_DIR}/.executor/service/service-install.manifest" \
+ && ! -e "${HOME_DIR}/.executor/service/service-install.recovery" ]]; then
+ printf 'ok %s - manifest failure stays stopped and a clean rerun recovers\n' \
+ "$test_number"
+ else
+ printf 'not ok %s - rerun did not recover manifest publication\n' "$test_number"
+ cat "$OUTPUT"
+ exit 1
+ fi
+else
+ printf 'not ok %s - launchctl ran before the ownership manifest committed\n' "$test_number"
+ cat "$OUTPUT"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case template-link-crash
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/launchctl"
+chmod 0700 "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+template_path="${DATA_DIR}/.executor-templates.ABCDEFGH"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_INSTALL_TEST_CRASH_AFTER_TEMPLATE_LINK=1 \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="$template_path" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - template link crash injection reported success\n' "$test_number"
+ exit 1
+fi
+template_copy="${CASE_ROOT}/template.copy"
+cp "$template_path" "$template_copy"
+if [[ "$(private_file_metadata "$template_path")" != "$(id -u)|600|2" \
+ || ! -f "${SERVICE_DIR}/service-install.recovery" ]]; then
+ printf 'not ok %s - template link crash did not leave recoverable state\n' \
+ "$test_number"
+ exit 1
+fi
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="$template_path" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+if cmp -s "$template_copy" "$template_path" \
+ && [[ "$(private_file_metadata "$template_path")" == "$(id -u)|600|1" \
+ && ! -e "${SERVICE_DIR}/service-install.recovery" ]] \
+ && [[ "$(find "$DATA_DIR" -maxdepth 1 -type f \
+ -name '.executor-templates.*' ! -path "$template_path" | wc -l)" -eq 0 ]]; then
+ printf 'ok %s - template basename crash recovers only the same-inode alias\n' \
+ "$test_number"
+else
+ printf 'not ok %s - template link crash recovery changed or stranded state\n' \
+ "$test_number"
+ cat "$OUTPUT"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case template-unlink-crash
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/launchctl"
+chmod 0700 "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+template_path="${DATA_DIR}/mcp-stdio-templates.json"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_INSTALL_TEST_CRASH_AFTER_TEMPLATE_UNLINK=1 \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - template unlink crash injection reported success\n' "$test_number"
+ exit 1
+fi
+template_copy="${CASE_ROOT}/template.copy"
+cp "$template_path" "$template_copy"
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+if cmp -s "$template_copy" "$template_path" \
+ && [[ "$(private_file_metadata "$template_path")" == "$(id -u)|600|1" \
+ && ! -e "${SERVICE_DIR}/service-install.recovery" ]]; then
+ printf 'ok %s - template unlink crash resumes with the original file\n' \
+ "$test_number"
+else
+ printf 'not ok %s - template unlink crash did not resume safely\n' "$test_number"
+ cat "$OUTPUT"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case template-alias-substitution
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/launchctl"
+chmod 0700 "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+template_path="${DATA_DIR}/mcp-stdio-templates.json"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_INSTALL_TEST_CRASH_AFTER_TEMPLATE_LINK=1 \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - template substitution crash injection reported success\n' \
+ "$test_number"
+ exit 1
+fi
+template_alias="$(compgen -G "${DATA_DIR}/.executor-templates.*")"
+unknown_alias="${CASE_ROOT}/unknown-template-alias"
+rm -f "$template_alias"
+ln "$template_path" "$unknown_alias"
+printf '{\n "templates": []\n}\n' > "$template_alias"
+chmod 0600 "$template_alias"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1; then
+ printf 'not ok %s - substituted template recovery alias was accepted\n' \
+ "$test_number"
+ exit 1
+fi
+if [[ -f "$template_alias" && -f "$unknown_alias" \
+ && "$(private_file_metadata "$template_path")" == "$(id -u)|600|2" ]]; then
+ rm -f "$template_alias" "$unknown_alias"
+ HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+ printf 'ok %s - substituted and unknown template aliases remain rejected\n' \
+ "$test_number"
+else
+ printf 'not ok %s - failed template recovery mutated attacker state\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case template-destination-basename
+template_path="${DATA_DIR}/.executor-templates.ABCDEFGH"
+unknown_alias="${CASE_ROOT}/unknown-template-alias"
+printf '{\n "templates": []\n}\n' > "$template_path"
+chmod 0600 "$template_path"
+ln "$template_path" "$unknown_alias"
+template_copy="${CASE_ROOT}/template.copy"
+cp "$template_path" "$template_copy"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="$template_path" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - destination basename was mistaken for a recovery alias\n' \
+ "$test_number"
+ exit 1
+fi
+if cmp -s "$template_copy" "$template_path" \
+ && [[ -f "$unknown_alias" \
+ && "$(private_file_metadata "$template_path")" == "$(id -u)|600|2" \
+ && ! -e "$SERVICE_DIR" ]]; then
+ printf 'ok %s - template destination basename never identifies itself as an alias\n' \
+ "$test_number"
+else
+ printf 'not ok %s - basename recovery changed the destination or unknown alias\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+for corruption in zero truncated; do
+ run_case "template-${corruption}-crash"
+ printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+ printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/launchctl"
+ chmod 0700 "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+ template_path="${DATA_DIR}/mcp-stdio-templates.json"
+ if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_INSTALL_TEST_CRASH_AFTER_TEMPLATE_LINK=1 \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - %s template crash injection reported success\n' \
+ "$test_number" "$corruption"
+ exit 1
+ fi
+ if [[ "$corruption" == zero ]]; then
+ : > "$template_path"
+ else
+ printf '{}' > "$template_path"
+ fi
+ template_copy="${CASE_ROOT}/template.copy"
+ cp "$template_path" "$template_copy"
+ if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1; then
+ printf 'not ok %s - %s crash template was accepted during recovery\n' \
+ "$test_number" "$corruption"
+ exit 1
+ fi
+ if ! cmp -s "$template_copy" "$template_path" \
+ || [[ "$(private_file_metadata "$template_path")" != "$(id -u)|600|2" \
+ || ! -e "${SERVICE_DIR}/service-install.recovery" ]]; then
+ printf 'not ok %s - %s crash recovery mutated invalid template state\n' \
+ "$test_number" "$corruption"
+ exit 1
+ fi
+done
+printf 'ok %s - zero and truncated template crash states fail closed\n' "$test_number"
+
+test_number=$((test_number + 1))
+run_case persisted-configuration
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/launchctl"
+chmod 0700 "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+custom_data="${HOME_DIR}/Custom Executor Data"
+custom_templates="${HOME_DIR}/Custom Templates/stdio templates.json"
+service_root="${HOME_DIR}/.executor/service"
+config="${service_root}/service-config"
+wrapper="${service_root}/run-launchd.sh"
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_DATA_DIR="$custom_data" \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="$custom_templates" \
+ EXECUTOR_PUBLIC_ORIGIN="https://executor.example.test" \
+ EXECUTOR_TRUSTED_PROXIES="127.0.0.1/32,::1/128" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1
+config_metadata="$(private_file_metadata "$config")"
+escaped_custom_data="$(printf '%q' "$custom_data")"
+escaped_custom_templates="$(printf '%q' "$custom_templates")"
+if [[ "$config_metadata" != "$(id -u)|600|1" \
+ || ! -f "${service_root}/bounded-log.sh" \
+ || -e "${custom_data}/bounded-log.sh" \
+ || -e "${custom_data}/run-launchd.sh" ]] \
+ || ! grep -F -- "--data-dir ${escaped_custom_data}" "$wrapper" >/dev/null \
+ || ! grep -F -- "--mcp-stdio-templates ${escaped_custom_templates}" \
+ "$wrapper" >/dev/null; then
+ printf 'not ok %s - persisted configuration metadata is not private\n' \
+ "$test_number"
+ exit 1
+fi
+cp "$config" "${CASE_ROOT}/initial-config"
+cp "$wrapper" "${CASE_ROOT}/initial-wrapper"
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+if ! cmp -s "$config" "${CASE_ROOT}/initial-config" \
+ || ! cmp -s "$wrapper" "${CASE_ROOT}/initial-wrapper"; then
+ printf 'not ok %s - reinstall without overrides reset persisted settings\n' \
+ "$test_number"
+ cat "$OUTPUT"
+ exit 1
+fi
+initial_data_line="$(grep '^data_dir_hex=' "$config")"
+initial_templates_line="$(grep '^templates_file_hex=' "$config")"
+initial_proxies="$(grep '^trusted_proxy_hex=' "$config")"
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_PUBLIC_ORIGIN="https://new.example.test" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+if [[ "$(grep '^data_dir_hex=' "$config")" != "$initial_data_line" \
+ || "$(grep '^templates_file_hex=' "$config")" != "$initial_templates_line" \
+ || "$(grep '^trusted_proxy_hex=' "$config")" != "$initial_proxies" ]] \
+ || ! grep -F -- '--public-origin https://new.example.test' "$wrapper" >/dev/null; then
+ printf 'not ok %s - one-field override reset unrelated persisted settings\n' \
+ "$test_number"
+ cat "$OUTPUT"
+ exit 1
+fi
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_PUBLIC_ORIGIN="" EXECUTOR_TRUSTED_PROXIES="" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1
+if [[ "$(grep '^data_dir_hex=' "$config")" == "$initial_data_line" \
+ && "$(grep '^templates_file_hex=' "$config")" == "$initial_templates_line" \
+ && -z "$(grep '^trusted_proxy_hex=' "$config" || true)" ]] \
+ && ! grep -F -- '--public-origin' "$wrapper" >/dev/null \
+ && ! grep -F -- '--trusted-proxy' "$wrapper" >/dev/null; then
+ printf 'ok %s - reinstall preserves settings and explicit empty overrides clear them\n' \
+ "$test_number"
+else
+ printf 'not ok %s - explicit empty overrides did not clear persisted settings\n' \
+ "$test_number"
+ cat "$OUTPUT"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case unsafe-config-symlink
+victim="${CASE_ROOT}/config-victim"
+mkdir -p "${HOME_DIR}/.executor/service"
+printf 'keep me\n' > "$victim"
+ln -s "$victim" "${HOME_DIR}/.executor/service/service-config"
+expect_rejected
+if [[ "$(cat "$victim")" == "keep me" \
+ && -L "${HOME_DIR}/.executor/service/service-config" \
+ && ! -e "${DATA_DIR}/mcp-stdio-templates.json" ]]; then
+ printf 'ok %s - persisted configuration symlinks are rejected before managed writes\n' \
+ "$test_number"
+else
+ printf 'not ok %s - unsafe persisted configuration was modified\n' "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case unsafe-custom-ancestor
+unsafe_parent="${HOME_DIR}/unsafe-parent"
+mkdir "$unsafe_parent"
+chmod 0777 "$unsafe_parent"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_DATA_DIR="${unsafe_parent}/data" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - world-writable custom data ancestry was accepted\n' \
+ "$test_number"
+ exit 1
+fi
+if [[ ! -e "${unsafe_parent}/data" \
+ && ! -e "${HOME_DIR}/.executor/service/service-config" \
+ && ! -e "${HOME_DIR}/.executor/service/service-install.recovery" ]]; then
+ printf 'ok %s - unsafe custom data ancestry fails before managed writes\n' \
+ "$test_number"
+else
+ printf 'not ok %s - unsafe custom data ancestry allowed managed writes\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case symlinked-custom-ancestor
+victim="${CASE_ROOT}/custom-data-victim"
+mkdir "$victim"
+ln -s "$victim" "${HOME_DIR}/custom-data-link"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_DATA_DIR="${HOME_DIR}/custom-data-link/data" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - symlinked custom data ancestry was accepted\n' \
+ "$test_number"
+ exit 1
+fi
+if [[ -L "${HOME_DIR}/custom-data-link" \
+ && -z "$(find "$victim" -mindepth 1 -print -quit)" \
+ && ! -e "${HOME_DIR}/.executor/service/service-config" ]]; then
+ printf 'ok %s - symlinked custom data ancestry cannot redirect writes\n' \
+ "$test_number"
+else
+ printf 'not ok %s - symlinked custom data ancestry redirected writes\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case unsafe-template-ancestor
+unsafe_parent="${HOME_DIR}/unsafe-templates"
+mkdir "$unsafe_parent"
+chmod 0777 "$unsafe_parent"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="${unsafe_parent}/templates.json" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - world-writable template ancestry was accepted\n' \
+ "$test_number"
+ exit 1
+fi
+if [[ ! -e "${unsafe_parent}/templates.json" \
+ && ! -e "${HOME_DIR}/.executor/service/service-config" \
+ && ! -e "${HOME_DIR}/.executor/service/service-install.recovery" ]]; then
+ printf 'ok %s - unsafe template ancestry fails before managed writes\n' \
+ "$test_number"
+else
+ printf 'not ok %s - unsafe template ancestry allowed managed writes\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case relative-custom-path
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" EXECUTOR_DATA_DIR="relative-data" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - relative custom data path was accepted\n' "$test_number"
+ exit 1
+fi
+if [[ ! -e "${HOME_DIR}/.executor/service/service-config" \
+ && ! -e "${HOME_DIR}/.executor/service/service-install.recovery" ]]; then
+ printf 'ok %s - relative custom paths fail before managed writes\n' "$test_number"
+else
+ printf 'not ok %s - relative custom path allowed managed writes\n' "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+collision_case_number=0
+for collision_name in \
+ service-binary \
+ service-config \
+ manifest \
+ recovery \
+ wrapper \
+ logger \
+ plist \
+ data-directory \
+ master-key \
+ database \
+ database-journal \
+ private-log \
+ rotated-log \
+ case-folded-master-key \
+ nested-service-path; do
+ collision_case_number=$((collision_case_number + 1))
+ run_case "reserved-collision-${collision_case_number}"
+ case "$collision_name" in
+ service-binary) collision_path="${SERVICE_DIR}/bin/executor" ;;
+ service-config) collision_path="${SERVICE_DIR}/service-config" ;;
+ manifest) collision_path="${SERVICE_DIR}/service-install.manifest" ;;
+ recovery) collision_path="${SERVICE_DIR}/service-install.recovery" ;;
+ wrapper) collision_path="${SERVICE_DIR}/run-launchd.sh" ;;
+ logger) collision_path="${SERVICE_DIR}/bounded-log.sh" ;;
+ plist) collision_path="${PLIST_DIR}/dev.executor.gateway.plist" ;;
+ data-directory) collision_path="$DATA_DIR" ;;
+ master-key)
+ collision_path="${DATA_DIR}/master.key"
+ printf 'existing master key sentinel\n' > "$collision_path"
+ chmod 0600 "$collision_path"
+ ;;
+ database) collision_path="${DATA_DIR}/executor.db" ;;
+ database-journal) collision_path="${DATA_DIR}/executor.db-journal" ;;
+ private-log) collision_path="${DATA_DIR}/executor.log" ;;
+ rotated-log) collision_path="${DATA_DIR}/executor.log.2" ;;
+ case-folded-master-key) collision_path="${DATA_DIR}/MASTER.KEY" ;;
+ nested-service-path) collision_path="${SERVICE_DIR}/custom/templates.json" ;;
+ *) exit 1 ;;
+ esac
+ if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="$collision_path" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - reserved template collision %s was accepted\n' \
+ "$test_number" "$collision_name"
+ exit 1
+ fi
+ if [[ -e "$SERVICE_DIR" \
+ || -e "${PLIST_DIR}/dev.executor.gateway.plist" \
+ || -e "${DATA_DIR}/mcp-stdio-templates.json" ]]; then
+ printf 'not ok %s - collision %s wrote managed files before rejection\n' \
+ "$test_number" "$collision_name"
+ cat "$OUTPUT"
+ exit 1
+ fi
+ if [[ "$collision_name" == master-key \
+ && "$(cat "$collision_path")" != "existing master key sentinel" ]]; then
+ printf 'not ok %s - master key collision changed the existing sentinel\n' \
+ "$test_number"
+ exit 1
+ fi
+done
+printf 'ok %s - template collisions with every reserved path fail before writes\n' \
+ "$test_number"
+
+test_number=$((test_number + 1))
+for collision_name in case-folded-service-root plist-parent; do
+ run_case "data-collision-${collision_name}"
+ case "$collision_name" in
+ case-folded-service-root) collision_path="${HOME_DIR}/.EXECUTOR" ;;
+ plist-parent) collision_path="$PLIST_DIR" ;;
+ *) exit 1 ;;
+ esac
+ if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_DATA_DIR="$collision_path" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1; then
+ printf 'not ok %s - data collision %s was accepted\n' \
+ "$test_number" "$collision_name"
+ exit 1
+ fi
+ if [[ -e "$SERVICE_DIR" \
+ || -e "${PLIST_DIR}/dev.executor.gateway.plist" ]]; then
+ printf 'not ok %s - data collision %s wrote managed files\n' \
+ "$test_number" "$collision_name"
+ exit 1
+ fi
+done
+printf 'ok %s - data paths cannot contain service-owned paths\n' "$test_number"
+
+test_number=$((test_number + 1))
+run_case collision-reinstall-preservation
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/plutil"
+printf '#!/bin/sh\nexit 0\n' > "${STUB_DIR}/launchctl"
+chmod 0700 "${STUB_DIR}/plutil" "${STUB_DIR}/launchctl"
+HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >"$OUTPUT" 2>&1
+snapshot="${CASE_ROOT}/snapshot"
+mkdir "$snapshot"
+cp "${SERVICE_DIR}/service-config" "${snapshot}/service-config"
+cp "${SERVICE_DIR}/service-install.manifest" "${snapshot}/service-install.manifest"
+cp "${SERVICE_DIR}/run-launchd.sh" "${snapshot}/run-launchd.sh"
+cp "${SERVICE_DIR}/bounded-log.sh" "${snapshot}/bounded-log.sh"
+cp "${PLIST_DIR}/dev.executor.gateway.plist" "${snapshot}/launchd.plist"
+cp "${DATA_DIR}/mcp-stdio-templates.json" "${snapshot}/templates.json"
+if HOME="$HOME_DIR" PATH="${STUB_DIR}:$PATH" \
+ EXECUTOR_MCP_STDIO_TEMPLATES_FILE="${SERVICE_DIR}/service-config" \
+ bash "$INSTALLER" --binary "$EXECUTOR_BINARY" --no-start \
+ >>"$OUTPUT" 2>&1; then
+ printf 'not ok %s - colliding reinstall was accepted\n' "$test_number"
+ exit 1
+fi
+if cmp -s "${SERVICE_DIR}/service-config" "${snapshot}/service-config" \
+ && cmp -s "${SERVICE_DIR}/service-install.manifest" \
+ "${snapshot}/service-install.manifest" \
+ && cmp -s "${SERVICE_DIR}/run-launchd.sh" "${snapshot}/run-launchd.sh" \
+ && cmp -s "${SERVICE_DIR}/bounded-log.sh" "${snapshot}/bounded-log.sh" \
+ && cmp -s "${PLIST_DIR}/dev.executor.gateway.plist" "${snapshot}/launchd.plist" \
+ && cmp -s "${DATA_DIR}/mcp-stdio-templates.json" "${snapshot}/templates.json" \
+ && [[ ! -e "${SERVICE_DIR}/service-install.recovery" ]]; then
+ printf 'ok %s - rejected collision preserves an existing installation byte-for-byte\n' \
+ "$test_number"
+else
+ printf 'not ok %s - rejected collision changed an existing installation\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case unsafe-config-hardlink
+victim="${CASE_ROOT}/config-victim"
+mkdir -p "${HOME_DIR}/.executor/service"
+printf 'keep me\n' > "$victim"
+chmod 0600 "$victim"
+ln "$victim" "${HOME_DIR}/.executor/service/service-config"
+expect_rejected
+if [[ "$(cat "$victim")" == "keep me" \
+ && "$(private_file_metadata "$victim")" == "$(id -u)|600|2" \
+ && ! -e "${DATA_DIR}/mcp-stdio-templates.json" ]]; then
+ printf 'ok %s - hard-linked persisted configuration is rejected without mutation\n' \
+ "$test_number"
+else
+ printf 'not ok %s - hard-linked persisted configuration was modified\n' \
+ "$test_number"
+ exit 1
+fi
+
+test_number=$((test_number + 1))
+run_case malformed-config
+mkdir -p "${HOME_DIR}/.executor/service"
+printf 'executor-launchd-config-v1\nunknown_hex=00\n' \
+ > "${HOME_DIR}/.executor/service/service-config"
+chmod 0600 "${HOME_DIR}/.executor/service/service-config"
+expect_rejected
+if [[ ! -e "${DATA_DIR}/mcp-stdio-templates.json" \
+ && ! -e "${HOME_DIR}/.executor/service/service-install.recovery" ]]; then
+ printf 'ok %s - malformed persisted configuration fails before managed writes\n' \
+ "$test_number"
+else
+ printf 'not ok %s - malformed persisted configuration allowed managed writes\n' \
+ "$test_number"
+ exit 1
+fi
+
+printf '1..%s\n' "$test_number"
diff --git a/scripts/test-install-release-archive.sh b/scripts/test-install-release-archive.sh
new file mode 100755
index 000000000..5835acf50
--- /dev/null
+++ b/scripts/test-install-release-archive.sh
@@ -0,0 +1,1142 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+fixture="$(mktemp -d "${HOME}/.executor-release-fixture.XXXXXXXX")"
+trap 'rm -rf "$fixture"' EXIT
+
+event_line() {
+ local log=$1 event=$2
+ awk -v expected="$event" '
+ $0 == expected { line = NR }
+ END { if (line == 0) exit 1; print line }
+ ' "$log"
+}
+
+assert_event_before() {
+ local log=$1 first=$2 second=$3 first_line second_line
+ first_line="$(event_line "$log" "$first")" \
+ || { printf 'missing durability event: %s\n' "$first" >&2; exit 1; }
+ second_line="$(event_line "$log" "$second")" \
+ || { printf 'missing durability event: %s\n' "$second" >&2; exit 1; }
+ if [[ "$first_line" -ge "$second_line" ]]; then
+ printf 'durability event %s did not precede %s\n' "$first" "$second" >&2
+ exit 1
+ fi
+}
+
+wait_for_file() {
+ local path=$1 process=$2 attempt
+ for attempt in {1..100}; do
+ [[ -e "$path" ]] && return 0
+ if ! kill -0 "$process" 2>/dev/null; then
+ printf 'process %s exited before creating %s\n' "$process" "$path" >&2
+ return 1
+ fi
+ sleep 0.1
+ done
+ printf 'timed out waiting for %s\n' "$path" >&2
+ return 1
+}
+
+default_help="$(env -u EXECUTOR_REPOSITORY "${repo_root}/scripts/install.sh" --help)"
+[[ "$default_help" == *"raw.githubusercontent.com/davis7dotsh/executor-fork-testing/"* ]]
+override_help="$(EXECUTOR_REPOSITORY=owner/repository "${repo_root}/scripts/install.sh" --help)"
+[[ "$override_help" == *"raw.githubusercontent.com/owner/repository/"* ]]
+
+stage="${fixture}/stage"
+mkdir "$stage"
+printf '#!/bin/sh\nexit 0\n' > "${stage}/executor"
+chmod 0755 "${stage}/executor"
+printf 'MIT fixture\n' > "${stage}/LICENSE"
+printf 'Rust license fixture\n' > "${stage}/THIRD_PARTY_LICENSES.html"
+printf '[{"name":"Svelte","identifier":"MIT","text":"fixture"}]\n' \
+ > "${stage}/THIRD_PARTY_JAVASCRIPT_LICENSES.json"
+
+archive="${fixture}/executor-fixture.tar.gz"
+archive_retry="${fixture}/executor-fixture-retry.tar.gz"
+"${repo_root}/scripts/package-release-archive.py" \
+ --source "$stage" \
+ --output "$archive" \
+ --epoch 1700000000
+touch "${stage}"/*
+"${repo_root}/scripts/package-release-archive.py" \
+ --source "$stage" \
+ --output "$archive_retry" \
+ --epoch 1700000000
+cmp "$archive" "$archive_retry"
+if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$archive" > "${archive}.sha256"
+else
+ shasum -a 256 "$archive" > "${archive}.sha256"
+fi
+if command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 --check "${archive}.sha256" >/dev/null
+fi
+
+failure_tools="${fixture}/failure-tools"
+mkdir "$failure_tools"
+printf '%s\n' \
+ '#!/bin/sh' \
+ 'last_argument=' \
+ 'for argument in "$@"; do last_argument=$argument; done' \
+ 'if [ -n "${EXECUTOR_INSTALL_TEST_FAIL_REAL_MV_PATH:-}" ] && [ "$last_argument" = "$EXECUTOR_INSTALL_TEST_FAIL_REAL_MV_PATH" ]; then exit 73; fi' \
+ 'exec "$EXECUTOR_INSTALL_TEST_REAL_MV" "$@"' \
+ > "${failure_tools}/mv"
+printf '%s\n' \
+ '#!/bin/sh' \
+ 'last_argument=' \
+ 'for argument in "$@"; do last_argument=$argument; done' \
+ 'if [ -n "${EXECUTOR_INSTALL_TEST_FAIL_REAL_RM_PATH:-}" ] && [ "$last_argument" = "$EXECUTOR_INSTALL_TEST_FAIL_REAL_RM_PATH" ]; then exit 74; fi' \
+ 'exec "$EXECUTOR_INSTALL_TEST_REAL_RM" "$@"' \
+ > "${failure_tools}/rm"
+printf '%s\n' \
+ '#!/bin/sh' \
+ 'if [ "${EXECUTOR_INSTALL_SYNC_LABEL:-}" = "${EXECUTOR_INSTALL_TEST_FAIL_REAL_SYNC_LABEL:-}" ]; then exit 75; fi' \
+ 'exec "$EXECUTOR_INSTALL_TEST_REAL_SYNC" "$@"' \
+ > "${failure_tools}/sync"
+chmod 0755 "${failure_tools}/mv" "${failure_tools}/rm" "${failure_tools}/sync"
+real_mv="$(type -P mv)"
+real_rm="$(type -P rm)"
+real_sync="$(type -P sync)"
+
+install_dir="${fixture}/installed"
+install_durability_log="${fixture}/install-durability.log"
+EXECUTOR_INSTALL_DIR="$install_dir" \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$install_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+
+for installed in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json; do
+ [[ -s "${install_dir}/${installed}" ]]
+done
+[[ -x "${install_dir}/executor" ]]
+assert_event_before \
+ "$install_durability_log" \
+ 'recovery-durable:executor' \
+ 'managed-file-durable:executor'
+assert_event_before \
+ "$install_durability_log" \
+ 'managed-file-durable:executor' \
+ 'manifest-durable:executor'
+assert_event_before \
+ "$install_durability_log" \
+ 'manifest-durable:executor' \
+ 'recovery-retired:executor'
+EXECUTOR_INSTALL_DIR="$install_dir" \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$install_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+assert_event_before \
+ "$install_durability_log" \
+ 'uninstall-file-durable:executor' \
+ 'uninstall-manifest-durable'
+
+no_python_tools="${fixture}/no-python-tools"
+no_python_home="${fixture}/no-python-home"
+mkdir "$no_python_tools" "$no_python_home"
+no_python_commands=(
+ awk
+ bash
+ basename
+ cat
+ chmod
+ cksum
+ cp
+ dirname
+ gzip
+ ln
+ mkdir
+ mktemp
+ mv
+ ps
+ rm
+ sleep
+ stat
+ sync
+ sysctl
+ tail
+ tar
+ uname
+ wc
+)
+if command -v sha256sum >/dev/null 2>&1; then
+ no_python_commands+=(sha256sum)
+else
+ no_python_commands+=(shasum)
+fi
+for command_name in "${no_python_commands[@]}"; do
+ command_path="$(command -v "$command_name")"
+ ln -s "$command_path" "${no_python_tools}/${command_name}"
+done
+printf '# no Python fixture\n' > "${no_python_home}/.bashrc"
+PATH="$no_python_tools" \
+ HOME="$no_python_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" > "${fixture}/no-python-install.log"
+[[ -x "${no_python_home}/.executor/bin/executor" ]]
+[[ -s "${no_python_home}/.executor/bin/.executor-path-ownership" ]]
+no_python_path_command="export PATH=${no_python_home}/.executor/bin:\$PATH"
+grep -Fqx '# Executor' "${no_python_home}/.bashrc"
+grep -Fqx "$no_python_path_command" "${no_python_home}/.bashrc"
+if grep -F 'Add this to' "${fixture}/no-python-install.log" >/dev/null; then
+ printf 'Python-free installer fell back to a manual PATH edit\n' >&2
+ exit 1
+fi
+PATH="$no_python_tools" \
+ HOME="$no_python_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+if grep -Fqx '# Executor' "${no_python_home}/.bashrc"; then
+ printf 'Python-free uninstall left its PATH marker behind\n' >&2
+ exit 1
+fi
+
+real_failure_home="${fixture}/real-failure-home"
+real_failure_config="${real_failure_home}/.bashrc"
+mkdir "$real_failure_home"
+printf '# real failure fixture\n' > "$real_failure_config"
+cp "$real_failure_config" "${fixture}/real-failure-config-before"
+if HOME="$real_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_REAL_CONFIG_WRITE=1 \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" > "${fixture}/real-write-failure.log" 2>&1; then
+ printf 'installer ignored a real shell-config write failure\n' >&2
+ exit 1
+fi
+cmp "$real_failure_config" "${fixture}/real-failure-config-before"
+[[ -x "${real_failure_home}/.executor/bin/executor" ]]
+[[ -s "${real_failure_home}/.executor/bin/.executor-install-manifest" ]]
+[[ -s "${real_failure_home}/.executor/bin/.executor-path-ownership" ]]
+[[ ! -e "${real_failure_config}.executor-edit-lock" ]]
+HOME="$real_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+cp "$real_failure_config" "${fixture}/real-failure-config-with-path"
+if PATH="${failure_tools}:$PATH" \
+ HOME="$real_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_REAL_MV_PATH="$real_failure_config" \
+ EXECUTOR_INSTALL_TEST_REAL_MV="$real_mv" \
+ EXECUTOR_INSTALL_TEST_REAL_RM="$real_rm" \
+ EXECUTOR_INSTALL_TEST_REAL_SYNC="$real_sync" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall > "${fixture}/real-move-failure.log" 2>&1; then
+ printf 'uninstaller ignored a real shell-config move failure\n' >&2
+ exit 1
+fi
+cmp "$real_failure_config" "${fixture}/real-failure-config-with-path"
+[[ -x "${real_failure_home}/.executor/bin/executor" ]]
+[[ -s "${real_failure_home}/.executor/bin/.executor-install-manifest" ]]
+[[ ! -e "${real_failure_config}.executor-edit-lock" ]]
+if PATH="${failure_tools}:$PATH" \
+ HOME="$real_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_REAL_RM_PATH="${real_failure_config}.executor-edit-lock" \
+ EXECUTOR_INSTALL_TEST_REAL_MV="$real_mv" \
+ EXECUTOR_INSTALL_TEST_REAL_RM="$real_rm" \
+ EXECUTOR_INSTALL_TEST_REAL_SYNC="$real_sync" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall > "${fixture}/real-unlink-failure.log" 2>&1; then
+ printf 'uninstaller ignored a real shell-config lock unlink failure\n' >&2
+ exit 1
+fi
+[[ -x "${real_failure_home}/.executor/bin/executor" ]]
+[[ -s "${real_failure_home}/.executor/bin/.executor-install-manifest" ]]
+[[ -s "${real_failure_config}.executor-edit-lock" ]]
+if grep -Fqx '# Executor' "$real_failure_config"; then
+ printf 'real unlink failure did not reach the config-lock release boundary\n' >&2
+ exit 1
+fi
+HOME="$real_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+[[ ! -e "${real_failure_config}.executor-edit-lock" ]]
+
+real_sync_home="${fixture}/real-sync-home"
+mkdir "$real_sync_home"
+if PATH="${failure_tools}:$PATH" \
+ HOME="$real_sync_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_REAL_SYNC_LABEL='manifest-durable:executor' \
+ EXECUTOR_INSTALL_TEST_REAL_MV="$real_mv" \
+ EXECUTOR_INSTALL_TEST_REAL_RM="$real_rm" \
+ EXECUTOR_INSTALL_TEST_REAL_SYNC="$real_sync" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/real-sync-failure.log" 2>&1; then
+ printf 'installer ignored a real durability sync failure\n' >&2
+ exit 1
+fi
+[[ -d "${real_sync_home}/.executor/bin/.executor-install-recovery" ]]
+[[ -s "${real_sync_home}/.executor/bin/.executor-install-manifest" ]]
+HOME="$real_sync_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+[[ ! -e "${real_sync_home}/.executor/bin/.executor-install-recovery" ]]
+HOME="$real_sync_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+printf 'unexpected\n' > "${stage}/extra"
+invalid_archive="${fixture}/invalid.tar.gz"
+tar -C "$stage" -czf "$invalid_archive" \
+ executor LICENSE THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json extra
+if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$invalid_archive" > "${invalid_archive}.sha256"
+else
+ shasum -a 256 "$invalid_archive" > "${invalid_archive}.sha256"
+fi
+
+if EXECUTOR_INSTALL_DIR="${fixture}/rejected" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$invalid_archive" \
+ --no-modify-path >/dev/null 2>&1; then
+ printf 'installer accepted an archive with an unexpected member\n' >&2
+ exit 1
+fi
+
+unsafe_tmp="${fixture}/unsafe-tmp"
+mkdir "$unsafe_tmp"
+chmod 1777 "$unsafe_tmp"
+if TMPDIR="$unsafe_tmp" \
+ EXECUTOR_INSTALL_DIR="${fixture}/unsafe-tmp-install" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null 2>&1; then
+ printf 'installer accepted a current-user-owned world-writable TMPDIR\n' >&2
+ exit 1
+fi
+[[ ! -e "${fixture}/unsafe-tmp-install/executor" ]]
+
+canonical_tmp="${fixture}/canonical-tmp"
+canonical_tmp_alias="${fixture}/canonical-tmp-alias"
+canonical_tmp_install="${fixture}/canonical-tmp-install"
+mkdir "$canonical_tmp"
+chmod 0700 "$canonical_tmp"
+ln -s "$canonical_tmp" "$canonical_tmp_alias"
+TMPDIR="$canonical_tmp_alias" \
+ EXECUTOR_INSTALL_DIR="$canonical_tmp_install" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+[[ -x "${canonical_tmp_install}/executor" ]]
+EXECUTOR_INSTALL_DIR="$canonical_tmp_install" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+swap_tmp="${fixture}/swap-tmp"
+mkdir "$swap_tmp"
+chmod 0700 "$swap_tmp"
+if TMPDIR="$swap_tmp" \
+ EXECUTOR_INSTALL_DIR="${fixture}/swapped-tmp-install" \
+ EXECUTOR_INSTALL_TEST_SWAP_TEMP_DIRECTORY=1 \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null 2>&1; then
+ printf 'installer accepted a replaced private temporary directory\n' >&2
+ exit 1
+fi
+[[ ! -e "${fixture}/swapped-tmp-install/executor" ]]
+
+staged_source_tmp="${fixture}/staged-source-tmp"
+staged_source_install="${fixture}/staged-source-install"
+staged_source_archive="${fixture}/staged-source.tar.gz"
+mkdir "$staged_source_tmp"
+chmod 0700 "$staged_source_tmp"
+cp "$archive" "$staged_source_archive"
+cp "${archive}.sha256" "${staged_source_archive}.sha256"
+TMPDIR="$staged_source_tmp" \
+ EXECUTOR_INSTALL_DIR="$staged_source_install" \
+ EXECUTOR_INSTALL_TEST_REMOVE_SOURCE_AFTER_STAGE=1 \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$staged_source_archive" \
+ --no-modify-path >/dev/null
+[[ ! -e "$staged_source_archive" ]]
+[[ ! -e "${staged_source_archive}.sha256" ]]
+[[ -x "${staged_source_install}/executor" ]]
+EXECUTOR_INSTALL_DIR="$staged_source_install" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+shared_install_dir="${fixture}/shared-bin"
+mkdir -p "$shared_install_dir"
+printf 'unrelated license sentinel\n' > "${shared_install_dir}/LICENSE"
+if EXECUTOR_INSTALL_DIR="$shared_install_dir" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null 2>&1; then
+ printf 'installer overwrote an unowned shared-directory file\n' >&2
+ exit 1
+fi
+grep -Fqx 'unrelated license sentinel' "${shared_install_dir}/LICENSE"
+[[ ! -e "${shared_install_dir}/executor" ]]
+[[ ! -e "${shared_install_dir}/.executor-install-manifest" ]]
+
+umask_install_dir="${fixture}/umask-install-parent/bin"
+(
+ umask 0002
+ EXECUTOR_INSTALL_DIR="$umask_install_dir" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+)
+[[ -x "${umask_install_dir}/executor" ]]
+EXECUTOR_INSTALL_DIR="$umask_install_dir" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+unsafe_install_parent="${fixture}/unsafe-install-parent"
+mkdir "$unsafe_install_parent"
+chmod 0777 "$unsafe_install_parent"
+if EXECUTOR_INSTALL_DIR="${unsafe_install_parent}/bin" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/unsafe-install-parent.log" 2>&1; then
+ printf 'installer accepted a writable install-path ancestor\n' >&2
+ exit 1
+fi
+grep -F 'install path components cannot be group or world writable' \
+ "${fixture}/unsafe-install-parent.log" >/dev/null
+[[ ! -e "${unsafe_install_parent}/bin" ]]
+chmod 0700 "$unsafe_install_parent"
+
+symlink_install_target="${fixture}/symlink-install-target"
+symlink_install_component="${fixture}/symlink-install-component"
+mkdir "$symlink_install_target"
+ln -s "$symlink_install_target" "$symlink_install_component"
+if EXECUTOR_INSTALL_DIR="${symlink_install_component}/bin" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/symlink-install-component.log" 2>&1; then
+ printf 'installer accepted a symlinked install-path component\n' >&2
+ exit 1
+fi
+grep -F 'install path component is not a real directory' \
+ "${fixture}/symlink-install-component.log" >/dev/null
+[[ ! -e "${symlink_install_target}/bin" ]]
+
+ancestor_swap_parent="${fixture}/ancestor-swap-parent"
+ancestor_swap_original="${fixture}/ancestor-swap-original"
+ancestor_swap_ready="${fixture}/ancestor-swap-ready"
+ancestor_swap_release="${fixture}/ancestor-swap-release"
+mkdir "$ancestor_swap_parent"
+EXECUTOR_INSTALL_DIR="${ancestor_swap_parent}/bin" \
+ EXECUTOR_INSTALL_TEST_PAUSE_POINT=after-install-root \
+ EXECUTOR_INSTALL_TEST_PAUSE_READY="$ancestor_swap_ready" \
+ EXECUTOR_INSTALL_TEST_PAUSE_RELEASE="$ancestor_swap_release" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/ancestor-swap.log" 2>&1 &
+ancestor_swap_pid=$!
+wait_for_file "$ancestor_swap_ready" "$ancestor_swap_pid"
+mv "$ancestor_swap_parent" "$ancestor_swap_original"
+mkdir -p "${ancestor_swap_parent}/bin"
+touch "$ancestor_swap_release"
+if wait "$ancestor_swap_pid"; then
+ printf 'installer accepted a swapped install-path ancestor\n' >&2
+ exit 1
+fi
+grep -F 'install root parent changed after validation' \
+ "${fixture}/ancestor-swap.log" >/dev/null
+[[ ! -e "${ancestor_swap_parent}/bin/executor" ]]
+
+service_home="${fixture}/service-preservation-home"
+service_root="${service_home}/.executor/service"
+mkdir -p "${service_root}/bin"
+printf '#!/bin/sh\nprintf "service copy\\n"\n' > "${service_root}/bin/executor"
+chmod 0755 "${service_root}/bin/executor"
+printf 'service-install-manifest-v1\nfixture\n' > "${service_root}/service-install.manifest"
+printf 'service configuration fixture\n' > "${service_root}/service-config"
+cp -R "$service_root" "${fixture}/expected-service-root"
+HOME="$service_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+HOME="$service_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+HOME="$service_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+diff -r "${fixture}/expected-service-root" "$service_root"
+
+concurrent_home="${fixture}/concurrent-home"
+concurrent_ready="${fixture}/concurrent-ready"
+concurrent_release="${fixture}/concurrent-release"
+mkdir -p "$concurrent_home"
+HOME="$concurrent_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_PAUSE_POINT=after-lock \
+ EXECUTOR_INSTALL_TEST_PAUSE_READY="$concurrent_ready" \
+ EXECUTOR_INSTALL_TEST_PAUSE_RELEASE="$concurrent_release" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/concurrent-first.log" 2>&1 &
+concurrent_pid=$!
+wait_for_file "$concurrent_ready" "$concurrent_pid"
+if HOME="$concurrent_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/concurrent-second.log" 2>&1; then
+ printf 'a concurrent installer acquired a live install lock\n' >&2
+ exit 1
+fi
+grep -F 'another Executor installer is active' "${fixture}/concurrent-second.log" >/dev/null
+if HOME="$concurrent_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_PROCESS_IDENTITY_PID="$concurrent_pid" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/concurrent-identity-failure.log" 2>&1; then
+ printf 'installer reclaimed a live lock after an identity lookup failure\n' >&2
+ exit 1
+fi
+grep -F 'could not verify live install-lock owner pid' \
+ "${fixture}/concurrent-identity-failure.log" >/dev/null
+if HOME="$concurrent_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path > "${fixture}/concurrent-uninstall.log" 2>&1; then
+ printf 'a concurrent uninstall acquired a live install lock\n' >&2
+ exit 1
+fi
+grep -F 'another Executor installer is active' "${fixture}/concurrent-uninstall.log" >/dev/null
+touch "$concurrent_release"
+wait "$concurrent_pid"
+[[ -x "${concurrent_home}/.executor/bin/executor" ]]
+[[ ! -e "${concurrent_home}/.executor/bin/.executor-install-lock" ]]
+HOME="$concurrent_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+crashed_home="${fixture}/crashed-home"
+crashed_ready="${fixture}/crashed-ready"
+crashed_release="${fixture}/crashed-release"
+mkdir -p "$crashed_home"
+HOME="$crashed_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_PAUSE_POINT=after-install-file-1 \
+ EXECUTOR_INSTALL_TEST_PAUSE_READY="$crashed_ready" \
+ EXECUTOR_INSTALL_TEST_PAUSE_RELEASE="$crashed_release" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/crashed-first.log" 2>&1 &
+crashed_pid=$!
+wait_for_file "$crashed_ready" "$crashed_pid"
+[[ -x "${crashed_home}/.executor/bin/executor" ]]
+[[ -d "${crashed_home}/.executor/bin/.executor-install-recovery" ]]
+if HOME="$crashed_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path > "${fixture}/crashed-second.log" 2>&1; then
+ printf 'a concurrent installer rolled back a live mid-commit transaction\n' >&2
+ exit 1
+fi
+grep -F 'another Executor installer is active' "${fixture}/crashed-second.log" >/dev/null
+[[ -x "${crashed_home}/.executor/bin/executor" ]]
+[[ -d "${crashed_home}/.executor/bin/.executor-install-recovery" ]]
+kill -KILL "$crashed_pid"
+if wait "$crashed_pid" 2>/dev/null; then
+ printf 'killed installer exited successfully\n' >&2
+ exit 1
+fi
+[[ -s "${crashed_home}/.executor/bin/.executor-install-lock" ]]
+HOME="$crashed_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+[[ -x "${crashed_home}/.executor/bin/executor" ]]
+[[ -s "${crashed_home}/.executor/bin/.executor-install-manifest" ]]
+[[ ! -e "${crashed_home}/.executor/bin/.executor-install-recovery" ]]
+[[ ! -e "${crashed_home}/.executor/bin/.executor-install-lock" ]]
+HOME="$crashed_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+reused_pid_install="${fixture}/reused-pid-install"
+mkdir -p "$reused_pid_install"
+chmod 0700 "$reused_pid_install"
+printf 'executor-install-lock-v1\npid %s\nidentity ps:0:0\n' "$$" \
+ > "${reused_pid_install}/.executor-install-lock"
+chmod 0600 "${reused_pid_install}/.executor-install-lock"
+EXECUTOR_INSTALL_DIR="$reused_pid_install" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+[[ -x "${reused_pid_install}/executor" ]]
+[[ ! -e "${reused_pid_install}/.executor-install-lock" ]]
+EXECUTOR_INSTALL_DIR="$reused_pid_install" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+recovery_home="${fixture}/recovery-home"
+recovery_durability_log="${fixture}/recovery-durability.log"
+mkdir -p "$recovery_home"
+if HOME="$recovery_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_AFTER_INSTALL_FILE=1 \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$recovery_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null 2>&1; then
+ printf 'installer ignored an injected mid-install failure\n' >&2
+ exit 1
+fi
+[[ -d "${recovery_home}/.executor/bin/.executor-install-recovery" ]]
+[[ -x "${recovery_home}/.executor/bin/executor" ]]
+[[ ! -e "${recovery_home}/.executor/bin/.executor-install-manifest" ]]
+assert_event_before \
+ "$recovery_durability_log" \
+ 'recovery-durable:executor' \
+ 'managed-file-durable:executor'
+if HOME="$recovery_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$recovery_durability_log" \
+ EXECUTOR_INSTALL_TEST_FAIL_DURABILITY_POINT='rollback-managed-durable:executor' \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null 2>&1; then
+ printf 'installer ignored an injected rollback durability failure\n' >&2
+ exit 1
+fi
+[[ -d "${recovery_home}/.executor/bin/.executor-install-recovery" ]]
+HOME="$recovery_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$recovery_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+[[ -s "${recovery_home}/.executor/bin/.executor-install-manifest" ]]
+[[ ! -e "${recovery_home}/.executor/bin/.executor-install-recovery" ]]
+assert_event_before \
+ "$recovery_durability_log" \
+ 'rollback-managed-durable:executor' \
+ 'rollback-manifest-durable'
+assert_event_before \
+ "$recovery_durability_log" \
+ 'rollback-manifest-durable' \
+ 'recovery-retired:rollback'
+HOME="$recovery_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --no-modify-path >/dev/null
+
+unowned_path_home="${fixture}/unowned-path-home"
+mkdir -p "$unowned_path_home"
+unowned_path_command="export PATH=${unowned_path_home}/.executor/bin:\$PATH"
+printf '# Executor\n%s\n' "$unowned_path_command" > "${unowned_path_home}/.bashrc"
+HOME="$unowned_path_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+grep -Fqx '# Executor' "${unowned_path_home}/.bashrc"
+grep -Fqx "$unowned_path_command" "${unowned_path_home}/.bashrc"
+
+preexisting_path_home="${fixture}/preexisting-path-home"
+mkdir -p "$preexisting_path_home"
+preexisting_path_command="export PATH=${preexisting_path_home}/.executor/bin:\$PATH"
+printf '# preexisting fixture\n# Executor\n%s\n' \
+ "$preexisting_path_command" \
+ > "${preexisting_path_home}/.bashrc"
+HOME="$preexisting_path_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+[[ ! -e "${preexisting_path_home}/.executor/bin/.executor-path-ownership" ]]
+HOME="$preexisting_path_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+grep -Fqx '# Executor' "${preexisting_path_home}/.bashrc"
+grep -Fqx "$preexisting_path_command" "${preexisting_path_home}/.bashrc"
+
+path_failure_home="${fixture}/path-failure-home"
+path_durability_log="${fixture}/path-durability.log"
+mkdir -p "$path_failure_home"
+printf '# path failure fixture\n' > "${path_failure_home}/.bashrc"
+if HOME="$path_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_AFTER_PATH_CONFIG=1 \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$path_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null 2>&1; then
+ printf 'installer ignored an injected post-PATH-update failure\n' >&2
+ exit 1
+fi
+[[ -x "${path_failure_home}/.executor/bin/executor" ]]
+[[ -s "${path_failure_home}/.executor/bin/.executor-path-ownership" ]]
+[[ -s "${path_failure_home}/.executor/bin/.executor-install-manifest" ]]
+grep -Fqx '# Executor' "${path_failure_home}/.bashrc"
+assert_event_before \
+ "$path_durability_log" \
+ 'managed-file-durable:.executor-path-ownership' \
+ 'manifest-durable:.executor-path-ownership'
+assert_event_before \
+ "$path_durability_log" \
+ 'manifest-durable:.executor-path-ownership' \
+ 'recovery-retired:.executor-path-ownership'
+assert_event_before \
+ "$path_durability_log" \
+ 'recovery-retired:.executor-path-ownership' \
+ 'path-config-durable:add'
+HOME="$path_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$path_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+HOME="$path_failure_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$path_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+assert_event_before \
+ "$path_durability_log" \
+ 'path-config-durable:remove' \
+ 'uninstall-file-durable:.executor-path-ownership'
+assert_event_before \
+ "$path_durability_log" \
+ 'uninstall-file-durable:.executor-path-ownership' \
+ 'uninstall-manifest-durable'
+if grep -Fqx '# Executor' "${path_failure_home}/.bashrc"; then
+ printf 'uninstaller left a PATH block after a recovered PATH update failure\n' >&2
+ exit 1
+fi
+
+cross_root_home="${fixture}/cross-root-home"
+cross_root_a="${fixture}/cross-root-a"
+cross_root_b="${fixture}/cross-root-b"
+cross_root_ready="${fixture}/cross-root-ready"
+cross_root_release="${fixture}/cross-root-release"
+mkdir "$cross_root_home"
+printf '# cross-root fixture\n' > "${cross_root_home}/.bashrc"
+HOME="$cross_root_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_DIR="$cross_root_a" \
+ EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_READY="$cross_root_ready" \
+ EXECUTOR_INSTALL_TEST_CONFIG_PAUSE_RELEASE="$cross_root_release" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" > "${fixture}/cross-root-a.log" 2>&1 &
+cross_root_a_pid=$!
+wait_for_file "$cross_root_ready" "$cross_root_a_pid"
+HOME="$cross_root_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_DIR="$cross_root_b" \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" > "${fixture}/cross-root-b.log" 2>&1 &
+cross_root_b_pid=$!
+wait_for_file "${cross_root_b}/.executor-path-ownership" "$cross_root_b_pid"
+touch "$cross_root_release"
+wait "$cross_root_a_pid"
+wait "$cross_root_b_pid"
+cross_root_a_command="export PATH=${cross_root_a}:\$PATH"
+cross_root_b_command="export PATH=${cross_root_b}:\$PATH"
+[[ "$(grep -Fxc '# Executor' "${cross_root_home}/.bashrc")" -eq 2 ]]
+[[ "$(grep -Fxc "$cross_root_a_command" "${cross_root_home}/.bashrc")" -eq 1 ]]
+[[ "$(grep -Fxc "$cross_root_b_command" "${cross_root_home}/.bashrc")" -eq 1 ]]
+HOME="$cross_root_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_DIR="$cross_root_a" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+HOME="$cross_root_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_DIR="$cross_root_b" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+if grep -Fqx '# Executor' "${cross_root_home}/.bashrc"; then
+ printf 'cross-root uninstall left an Executor PATH marker\n' >&2
+ exit 1
+fi
+
+no_modify_home="${fixture}/no-modify-home"
+mkdir -p "$no_modify_home"
+no_modify_path_command="export PATH=${no_modify_home}/.executor/bin:\$PATH"
+printf '# Executor\n%s\n' "$no_modify_path_command" > "${no_modify_home}/.bashrc"
+HOME="$no_modify_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" \
+ --no-modify-path >/dev/null
+[[ ! -e "${no_modify_home}/.executor/bin/.executor-path-ownership" ]]
+HOME="$no_modify_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+grep -Fqx '# Executor' "${no_modify_home}/.bashrc"
+grep -Fqx "$no_modify_path_command" "${no_modify_home}/.bashrc"
+
+lost_state_home="${fixture}/lost-state-home"
+mkdir -p "$lost_state_home"
+printf '# lost state fixture\n' > "${lost_state_home}/.bashrc"
+HOME="$lost_state_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+rm -f "${lost_state_home}/.executor/bin/.executor-path-ownership"
+if HOME="$lost_state_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null 2>&1; then
+ printf 'uninstaller accepted missing PATH ownership state out of delete order\n' >&2
+ exit 1
+fi
+grep -Fqx '# Executor' "${lost_state_home}/.bashrc"
+[[ -x "${lost_state_home}/.executor/bin/executor" ]]
+[[ -s "${lost_state_home}/.executor/bin/.executor-install-manifest" ]]
+
+corrupt_state_home="${fixture}/corrupt-state-home"
+mkdir -p "$corrupt_state_home"
+printf '# corrupt state fixture\n' > "${corrupt_state_home}/.bashrc"
+HOME="$corrupt_state_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+printf 'corrupt ownership state\n' \
+ > "${corrupt_state_home}/.executor/bin/.executor-path-ownership"
+if HOME="$corrupt_state_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null 2>&1; then
+ printf 'uninstaller accepted corrupt PATH ownership state\n' >&2
+ exit 1
+fi
+grep -Fqx '# Executor' "${corrupt_state_home}/.bashrc"
+[[ -x "${corrupt_state_home}/.executor/bin/executor" ]]
+[[ -s "${corrupt_state_home}/.executor/bin/.executor-install-manifest" ]]
+
+shell_change_home="${fixture}/shell-change-home"
+shell_change_zdot="${shell_change_home}/original-zdot"
+mkdir -p "$shell_change_zdot"
+printf '# original zsh config\n' > "${shell_change_zdot}/.zshrc"
+printf '# untouched bash config\n' > "${shell_change_home}/.bashrc"
+HOME="$shell_change_home" \
+ SHELL=/bin/zsh \
+ ZDOTDIR="$shell_change_zdot" \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+grep -Fqx '# Executor' "${shell_change_zdot}/.zshrc"
+[[ -s "${shell_change_home}/.executor/bin/.executor-path-ownership" ]]
+HOME="$shell_change_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+grep -Fqx '# Executor' "${shell_change_home}/.bashrc"
+[[ "$(grep -c $'^/' "${shell_change_home}/.executor/bin/.executor-path-ownership")" -eq 2 ]]
+HOME="$shell_change_home" \
+ SHELL=/bin/fish \
+ ZDOTDIR="${shell_change_home}/different-zdot" \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+if grep -Fqx '# Executor' "${shell_change_zdot}/.zshrc"; then
+ printf 'uninstaller left a PATH block recorded under the original shell\n' >&2
+ exit 1
+fi
+if grep -Fqx '# Executor' "${shell_change_home}/.bashrc"; then
+ printf 'uninstaller left a second recorded shell PATH block\n' >&2
+ exit 1
+fi
+grep -Fqx '# original zsh config' "${shell_change_zdot}/.zshrc"
+grep -Fqx '# untouched bash config' "${shell_change_home}/.bashrc"
+
+lifecycle_home="${fixture}/home"
+lifecycle_install_dir="${lifecycle_home}/.executor/bin"
+lifecycle_config="${lifecycle_home}/.bashrc"
+mkdir -p "$lifecycle_home"
+printf '# fixture shell config\n' > "$lifecycle_config"
+chmod 0640 "$lifecycle_config"
+config_metadata="$(
+ python3 - "$lifecycle_config" <<'PY'
+import os
+import stat
+import sys
+
+metadata = os.stat(sys.argv[1], follow_symlinks=False)
+print(f"{stat.S_IMODE(metadata.st_mode)}:{metadata.st_uid}:{metadata.st_gid}")
+PY
+)"
+HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null
+
+path_command="export PATH=${lifecycle_install_dir}:\$PATH"
+[[ "$(grep -Fxc '# Executor' "$lifecycle_config")" -eq 1 ]]
+[[ "$(grep -Fxc "$path_command" "$lifecycle_config")" -eq 1 ]]
+[[ -s "${lifecycle_install_dir}/.executor-install-manifest" ]]
+[[ "$(
+ python3 - "${lifecycle_install_dir}/.executor-install-manifest" <<'PY'
+import os
+import stat
+import sys
+
+print(stat.S_IMODE(os.stat(sys.argv[1], follow_symlinks=False).st_mode))
+PY
+)" -eq 384 ]]
+[[ "$(
+ python3 - "$lifecycle_config" <<'PY'
+import os
+import stat
+import sys
+
+metadata = os.stat(sys.argv[1], follow_symlinks=False)
+print(f"{stat.S_IMODE(metadata.st_mode)}:{metadata.st_uid}:{metadata.st_gid}")
+PY
+)" == "$config_metadata" ]]
+mkdir -p "${lifecycle_home}/.executor/data"
+printf 'preserve data\n' > "${lifecycle_home}/.executor/data/database.fixture"
+printf 'preserve unrelated file\n' > "${lifecycle_install_dir}/user-owned.fixture"
+cp "$lifecycle_config" "${fixture}/expected-shell-config"
+
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_CONFIG_WRITE=1 \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null 2>&1; then
+ printf 'uninstaller ignored an injected shell-config write failure\n' >&2
+ exit 1
+fi
+cmp "$lifecycle_config" "${fixture}/expected-shell-config"
+[[ -x "${lifecycle_install_dir}/executor" ]]
+[[ -s "${lifecycle_install_dir}/.executor-install-manifest" ]]
+
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_CONFIG_RENAME=1 \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null 2>&1; then
+ printf 'uninstaller ignored an injected shell-config rename failure\n' >&2
+ exit 1
+fi
+cmp "$lifecycle_config" "${fixture}/expected-shell-config"
+[[ -x "${lifecycle_install_dir}/executor" ]]
+[[ -s "${lifecycle_install_dir}/.executor-install-manifest" ]]
+[[ -z "$(find "$lifecycle_home" -maxdepth 1 -name '.bashrc.executor-*' -print -quit)" ]]
+
+upgrade_binary="${fixture}/executor-upgrade"
+printf '#!/bin/sh\nprintf "upgraded\\n"\n' > "$upgrade_binary"
+chmod 0755 "$upgrade_binary"
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_MANIFEST=1 \
+ "${repo_root}/scripts/install.sh" \
+ --binary "$upgrade_binary" >/dev/null 2>&1; then
+ printf 'installer ignored an injected manifest publication failure\n' >&2
+ exit 1
+fi
+cmp "$upgrade_binary" "${lifecycle_install_dir}/executor"
+[[ -d "${lifecycle_install_dir}/.executor-install-recovery" ]]
+HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --binary "$upgrade_binary" >/dev/null
+cmp "$upgrade_binary" "${lifecycle_install_dir}/executor"
+[[ -s "${lifecycle_install_dir}/THIRD_PARTY_LICENSES.html" ]]
+[[ -s "${lifecycle_install_dir}/.executor-install-manifest" ]]
+[[ ! -e "${lifecycle_install_dir}/.executor-install-recovery" ]]
+
+printf 'replacement sentinel\n' > "${lifecycle_install_dir}/LICENSE"
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --archive "$archive" >/dev/null 2>&1; then
+ printf 'installer overwrote a replaced installer-owned file\n' >&2
+ exit 1
+fi
+grep -Fqx 'replacement sentinel' "${lifecycle_install_dir}/LICENSE"
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null 2>&1; then
+ printf 'uninstaller removed a replaced installer-owned file\n' >&2
+ exit 1
+fi
+grep -Fqx 'replacement sentinel' "${lifecycle_install_dir}/LICENSE"
+cmp "$lifecycle_config" "${fixture}/expected-shell-config"
+[[ -x "${lifecycle_install_dir}/executor" ]]
+[[ -s "${lifecycle_install_dir}/.executor-install-manifest" ]]
+cp "${stage}/LICENSE" "${lifecycle_install_dir}/LICENSE"
+
+uninstall_durability_log="${fixture}/uninstall-durability.log"
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_FAIL_AFTER_UNINSTALL_DELETE=1 \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$uninstall_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null 2>&1; then
+ printf 'uninstaller ignored an injected partial-delete failure\n' >&2
+ exit 1
+fi
+[[ ! -e "${lifecycle_install_dir}/executor" ]]
+[[ -s "${lifecycle_install_dir}/LICENSE" ]]
+[[ -s "${lifecycle_install_dir}/.executor-install-manifest" ]]
+[[ -s "${lifecycle_home}/.executor/data/database.fixture" ]]
+[[ -s "${lifecycle_install_dir}/user-owned.fixture" ]]
+event_line "$uninstall_durability_log" 'uninstall-file-durable:executor' >/dev/null
+if event_line "$uninstall_durability_log" 'uninstall-manifest-durable' >/dev/null 2>&1; then
+ printf 'partial uninstall published the manifest deletion too early\n' >&2
+ exit 1
+fi
+
+HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ EXECUTOR_INSTALL_TEST_DURABILITY_LOG="$uninstall_durability_log" \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+assert_event_before \
+ "$uninstall_durability_log" \
+ 'uninstall-file-durable:executor' \
+ 'uninstall-file-durable:LICENSE'
+assert_event_before \
+ "$uninstall_durability_log" \
+ 'uninstall-file-durable:.executor-path-ownership' \
+ 'uninstall-manifest-durable'
+
+for removed in \
+ executor \
+ LICENSE \
+ THIRD_PARTY_LICENSES.html \
+ THIRD_PARTY_JAVASCRIPT_LICENSES.json \
+ .executor-path-ownership \
+ .executor-install-manifest; do
+ [[ ! -e "${lifecycle_install_dir}/${removed}" ]]
+done
+[[ -s "${lifecycle_home}/.executor/data/database.fixture" ]]
+[[ -s "${lifecycle_install_dir}/user-owned.fixture" ]]
+grep -Fqx '# fixture shell config' "$lifecycle_config"
+[[ "$(
+ python3 - "$lifecycle_config" <<'PY'
+import os
+import stat
+import sys
+
+metadata = os.stat(sys.argv[1], follow_symlinks=False)
+print(f"{stat.S_IMODE(metadata.st_mode)}:{metadata.st_uid}:{metadata.st_gid}")
+PY
+)" == "$config_metadata" ]]
+if grep -Fqx '# Executor' "$lifecycle_config"; then
+ printf 'uninstaller left its shell marker behind\n' >&2
+ exit 1
+fi
+if grep -Fqx "$path_command" "$lifecycle_config"; then
+ printf 'uninstaller left its PATH entry behind\n' >&2
+ exit 1
+fi
+
+HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+
+empty_home="${fixture}/empty-home"
+mkdir -p "$empty_home"
+HOME="$empty_home" \
+ SHELL=/bin/zsh \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall >/dev/null
+if HOME="$lifecycle_home" \
+ SHELL=/bin/bash \
+ GITHUB_ACTIONS=false \
+ "${repo_root}/scripts/install.sh" \
+ --uninstall \
+ --archive "$archive" >/dev/null 2>&1; then
+ printf 'uninstaller accepted an install source option\n' >&2
+ exit 1
+fi
+
+printf 'release archive installer fixture passed\n'
diff --git a/scripts/test-install-systemd-master-key.sh b/scripts/test-install-systemd-master-key.sh
new file mode 100755
index 000000000..950b8a423
--- /dev/null
+++ b/scripts/test-install-systemd-master-key.sh
@@ -0,0 +1,823 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+readonly SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
+source "${SCRIPT_DIR}/lib/install-systemd-master-key.sh"
+
+readonly CURRENT_UID=$(id -u)
+readonly CURRENT_GID=$(id -g)
+TEST_NUMBER=0
+test_root=$(mktemp -d)
+trap 'rm -rf -- "${test_root}"' EXIT
+staging_parent="${test_root}/staging-parent"
+mkdir "${staging_parent}"
+chmod 0700 "${staging_parent}"
+
+pass() {
+ TEST_NUMBER=$((TEST_NUMBER + 1))
+ printf 'ok %s - %s\n' "${TEST_NUMBER}" "$1"
+}
+
+fail() {
+ printf 'not ok %s - %s\n' "$((TEST_NUMBER + 1))" "$1" >&2
+ exit 1
+}
+
+assert() {
+ local description=$1
+ shift
+ if ! "$@"; then
+ fail "${description}"
+ fi
+}
+
+layout_dir="${test_root}/layout"
+mkdir "${layout_dir}"
+layout_sentinel="${layout_dir}/managed-unit"
+printf 'managed unit sentinel\n' > "${layout_sentinel}"
+if executor_require_disjoint_paths \
+ "${layout_sentinel}" "persistent state" \
+ "${layout_sentinel}" "managed unit" 2>/dev/null; then
+ fail "equal persistent and managed paths were accepted"
+fi
+if executor_require_disjoint_paths \
+ "${layout_dir}" "persistent state" \
+ "${layout_sentinel}" "managed unit" 2>/dev/null; then
+ fail "nested persistent and managed paths were accepted"
+fi
+executor_require_disjoint_paths \
+ "${layout_dir}/master.key" "persistent state" \
+ "${test_root}/systemd/executor.service" "managed unit"
+assert "path-layout rejection changed the managed sentinel" test \
+ "$(cat "${layout_sentinel}")" = "managed unit sentinel"
+pass "systemd persistent and managed paths stay disjoint without mutation"
+
+valid_dir="${test_root}/valid"
+mkdir "${valid_dir}"
+valid_key="${valid_dir}/master.key"
+printf '0123456789abcdef0123456789abcdef' > "${valid_key}"
+chmod 0600 "${valid_key}"
+valid_copy="${test_root}/valid-key.copy"
+cp "${valid_key}" "${valid_copy}"
+valid_inode=$(stat --format='%i' -- "${valid_key}")
+executor_ensure_master_key \
+ "${staging_parent}" "${valid_dir}" "${valid_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "valid key bytes changed" cmp -s "${valid_copy}" "${valid_key}"
+assert "valid key inode changed" test \
+ "$(stat --format='%i' -- "${valid_key}")" = "${valid_inode}"
+pass "valid existing key is preserved byte-for-byte"
+
+mode_dir="${test_root}/wrong-mode"
+mkdir "${mode_dir}"
+mode_key="${mode_dir}/master.key"
+printf '89abcdef0123456789abcdef01234567' > "${mode_key}"
+chmod 0640 "${mode_key}"
+if executor_ensure_master_key \
+ "${staging_parent}" "${mode_dir}" "${mode_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "wrong-mode key was accepted"
+fi
+assert "wrong-mode key permissions were changed" test \
+ "$(stat --format='%a' -- "${mode_key}")" = 640
+assert "wrong-mode key bytes were changed" test \
+ "$(cat "${mode_key}")" = '89abcdef0123456789abcdef01234567'
+pass "existing key with unsafe mode is rejected without repair"
+
+symlink_dir="${test_root}/symlink"
+mkdir "${symlink_dir}"
+symlink_target="${symlink_dir}/target"
+printf 'fedcba9876543210fedcba9876543210' > "${symlink_target}"
+chmod 0600 "${symlink_target}"
+ln -s "${symlink_target}" "${symlink_dir}/master.key"
+if executor_ensure_master_key \
+ "${staging_parent}" "${symlink_dir}" "${symlink_dir}/master.key" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "symbolic-link key was accepted"
+fi
+assert "symbolic-link target changed" test \
+ "$(cat "${symlink_target}")" = 'fedcba9876543210fedcba9876543210'
+pass "symbolic-link key is rejected without touching its target"
+
+hardlink_dir="${test_root}/hardlink"
+mkdir "${hardlink_dir}"
+hardlink_key="${hardlink_dir}/master.key"
+printf '00112233445566778899aabbccddeeff' > "${hardlink_key}"
+chmod 0600 "${hardlink_key}"
+ln "${hardlink_key}" "${hardlink_dir}/alias"
+if executor_ensure_master_key \
+ "${staging_parent}" "${hardlink_dir}" "${hardlink_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "hard-linked key was accepted"
+fi
+assert "hard-linked key was mutated" test \
+ "$(cat "${hardlink_dir}/alias")" = '00112233445566778899aabbccddeeff'
+pass "hard-linked key is rejected without mutation"
+
+directory_target="${test_root}/directory-target"
+mkdir "${directory_target}"
+ln -s "${directory_target}" "${test_root}/directory-link"
+if executor_require_safe_directory "${test_root}/directory-link" 2>/dev/null; then
+ fail "symbolic-link managed directory was accepted"
+fi
+pass "symbolic-link managed directory is rejected"
+
+new_dir="${test_root}/new"
+mkdir "${new_dir}"
+new_key="${new_dir}/master.key"
+executor_ensure_master_key \
+ "${staging_parent}" "${new_dir}" "${new_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "new key has the wrong size" test \
+ "$(stat --format='%s' -- "${new_key}")" = 32
+assert "new key has the wrong link count" test \
+ "$(stat --format='%h' -- "${new_key}")" = 1
+assert "new key has the wrong mode" test \
+ "$(stat --format='%a' -- "${new_key}")" = 600
+pass "new key is safely published with validated metadata"
+
+unsafe_staging_parent="${test_root}/unsafe-staging-parent"
+unsafe_staging_data="${test_root}/unsafe-staging-data"
+mkdir "${unsafe_staging_parent}" "${unsafe_staging_data}"
+chmod 0770 "${unsafe_staging_parent}"
+if executor_ensure_master_key \
+ "${unsafe_staging_parent}" "${unsafe_staging_data}" \
+ "${unsafe_staging_data}/master.key" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "group-writable staging parent was accepted"
+fi
+pass "staging parent must exclude unprivileged writers"
+
+contention_dir="${test_root}/lock-contention"
+mkdir "${contention_dir}"
+contention_key="${contention_dir}/master.key"
+printf 'contention-key-must-stay-intact!' > "${contention_key}"
+chmod 0600 "${contention_key}"
+contention_copy="${test_root}/contention-key.copy"
+cp "${contention_key}" "${contention_copy}"
+lock_path="${staging_parent}/.executor-master-key.lock"
+exec {held_lock_fd}<>"${lock_path}"
+flock -n -x "${held_lock_fd}"
+if executor_ensure_master_key \
+ "${staging_parent}" "${contention_dir}" "${contention_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "a live master key installation lock was ignored"
+fi
+assert "lock contention changed the existing master key" cmp -s \
+ "${contention_copy}" "${contention_key}"
+flock -u "${held_lock_fd}"
+exec {held_lock_fd}>&-
+executor_ensure_master_key \
+ "${staging_parent}" "${contention_dir}" "${contention_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+pass "master key installation lock rejects contention without blocking"
+
+spoof_staging_parent="${test_root}/spoof-staging-parent"
+spoof_data_dir="${test_root}/spoof-data"
+mkdir "${spoof_staging_parent}" "${spoof_data_dir}"
+chmod 0700 "${spoof_staging_parent}"
+spoof_lock="${spoof_staging_parent}/.executor-master-key.lock"
+spoof_victim="${test_root}/spoof-lock-victim"
+printf 'victim must remain unchanged\n' > "${spoof_victim}"
+ln -s "${spoof_victim}" "${spoof_lock}"
+if executor_ensure_master_key \
+ "${spoof_staging_parent}" "${spoof_data_dir}" \
+ "${spoof_data_dir}/master.key" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "symbolic-link master key lock was accepted"
+fi
+assert "symbolic-link lock changed its victim" test \
+ "$(cat "${spoof_victim}")" = "victim must remain unchanged"
+rm -f -- "${spoof_lock}"
+: > "${spoof_lock}"
+chmod 0644 "${spoof_lock}"
+if executor_ensure_master_key \
+ "${spoof_staging_parent}" "${spoof_data_dir}" \
+ "${spoof_data_dir}/master.key" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "broad-mode master key lock was accepted"
+fi
+chmod 0600 "${spoof_lock}"
+spoof_lock_alias="${test_root}/spoof-lock-alias"
+ln "${spoof_lock}" "${spoof_lock_alias}"
+if executor_ensure_master_key \
+ "${spoof_staging_parent}" "${spoof_data_dir}" \
+ "${spoof_data_dir}/master.key" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "hard-linked master key lock was accepted"
+fi
+assert "hard-linked lock was mutated" test \
+ "$(stat --format='%a:%h:%s' -- "${spoof_lock}")" = 600:2:0
+pass "master key installation lock rejects spoofed inodes"
+
+recovery_publish_dir="${test_root}/recovery-publish-failure"
+recovery_publish_shim="${test_root}/recovery-publish-shim"
+mkdir "${recovery_publish_dir}" "${recovery_publish_shim}"
+recovery_publish_key="${recovery_publish_dir}/master.key"
+real_mv=$(command -v mv)
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'destination=${!#}' \
+ 'case "${destination}" in' \
+ ' */.executor-master-key.recovery) exit 76 ;;' \
+ 'esac' \
+ 'exec "${REAL_MV}" "$@"' > "${recovery_publish_shim}/mv"
+chmod 0700 "${recovery_publish_shim}/mv"
+if PATH="${recovery_publish_shim}:${PATH}" REAL_MV="${real_mv}" \
+ executor_ensure_master_key \
+ "${staging_parent}" "${recovery_publish_dir}" \
+ "${recovery_publish_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "recovery record publication failure was ignored"
+fi
+assert "key was published without a durable recovery record" test \
+ ! -e "${recovery_publish_key}"
+if compgen -G "${staging_parent}/.executor-master-key.recovery*" >/dev/null; then
+ fail "failed recovery publication left temporary control state"
+fi
+executor_ensure_master_key \
+ "${staging_parent}" "${recovery_publish_dir}" "${recovery_publish_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+pass "recovery record publication failure stops key mutation"
+
+recovery_retire_dir="${test_root}/recovery-retire-failure"
+recovery_retire_shim="${test_root}/recovery-retire-shim"
+mkdir "${recovery_retire_dir}" "${recovery_retire_shim}"
+recovery_retire_key="${recovery_retire_dir}/master.key"
+real_rm=$(command -v rm)
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'target=${!#}' \
+ 'case "${target}" in' \
+ ' */.executor-master-key.recovery) exit 77 ;;' \
+ 'esac' \
+ 'exec "${REAL_RM}" "$@"' > "${recovery_retire_shim}/rm"
+chmod 0700 "${recovery_retire_shim}/rm"
+if PATH="${recovery_retire_shim}:${PATH}" REAL_RM="${real_rm}" \
+ executor_ensure_master_key \
+ "${staging_parent}" "${recovery_retire_dir}" \
+ "${recovery_retire_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "recovery record retirement failure was ignored"
+fi
+assert "retirement failure removed the recovery record" test \
+ -e "${staging_parent}/.executor-master-key.recovery"
+assert "retirement failure lost the published key" test -e "${recovery_retire_key}"
+recovery_retire_copy="${test_root}/recovery-retire-key.copy"
+cp "${recovery_retire_key}" "${recovery_retire_copy}"
+executor_ensure_master_key \
+ "${staging_parent}" "${recovery_retire_dir}" "${recovery_retire_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "retirement retry changed the published key" cmp -s \
+ "${recovery_retire_copy}" "${recovery_retire_key}"
+pass "recovery retirement failure remains safely retryable"
+
+status_dir="${test_root}/validation-status"
+mkdir "${status_dir}"
+status_key="${status_dir}/master.key"
+validator_definition=$(declare -f executor_validate_master_key)
+executor_validate_master_key() {
+ return 73
+}
+create_succeeded=false
+validation_status=0
+if executor_create_master_key \
+ "${staging_parent}" "${status_dir}" "${status_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"; then
+ create_succeeded=true
+else
+ validation_status=$?
+fi
+eval "${validator_definition}"
+if [[ ${create_succeeded} == true ]]; then
+ fail "temporary key validation failure was reported as success"
+fi
+assert "temporary key validation status was not propagated" test \
+ "${validation_status}" = 73
+assert "failed temporary key was published" test ! -e "${status_key}"
+executor_ensure_master_key \
+ "${staging_parent}" "${status_dir}" "${status_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+pass "temporary key validation status is propagated"
+
+file_sync_dir="${test_root}/file-sync-failure"
+mkdir "${file_sync_dir}"
+file_sync_key="${file_sync_dir}/master.key"
+writer_definition=$(declare -f executor_write_and_sync_master_key)
+executor_write_and_sync_master_key() {
+ printf '0123456789abcdef0123456789abcdef' > "$1"
+ return 73
+}
+file_sync_succeeded=false
+file_sync_status=0
+if executor_ensure_master_key \
+ "${staging_parent}" "${file_sync_dir}" "${file_sync_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ file_sync_succeeded=true
+else
+ file_sync_status=$?
+fi
+eval "${writer_definition}"
+if [[ ${file_sync_succeeded} == true ]]; then
+ fail "staged key fsync failure was reported as success"
+fi
+assert "staged key fsync status was not propagated" test \
+ "${file_sync_status}" = 73
+assert "key was published after staged file fsync failure" test ! -e "${file_sync_key}"
+executor_ensure_master_key \
+ "${staging_parent}" "${file_sync_dir}" "${file_sync_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+pass "staged master key fsync failure prevents publication"
+
+directory_sync_dir="${test_root}/directory-sync-failure"
+mkdir "${directory_sync_dir}"
+directory_sync_key="${directory_sync_dir}/master.key"
+directory_sync_ledger="${test_root}/directory-sync-ledger"
+sync_definition=$(declare -f executor_sync_filesystem_path)
+executor_sync_filesystem_path() {
+ if [[ $1 == "${directory_sync_dir}" ]]; then
+ printf '%s\n' "$1" >> "${directory_sync_ledger}"
+ return 74
+ fi
+ command sync -f -- "$1"
+}
+directory_sync_succeeded=false
+directory_sync_status=0
+if executor_ensure_master_key \
+ "${staging_parent}" "${directory_sync_dir}" "${directory_sync_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ directory_sync_succeeded=true
+else
+ directory_sync_status=$?
+fi
+eval "${sync_definition}"
+if [[ ${directory_sync_succeeded} == true ]]; then
+ fail "published key directory sync failure was reported as success"
+fi
+assert "published key directory sync status was not propagated" test \
+ "${directory_sync_status}" = 74
+assert "directory sync did not run on the destination data directory" test \
+ "$(cat "${directory_sync_ledger}")" = "${directory_sync_dir}"
+assert "atomically published key disappeared after sync failure" test \
+ -e "${directory_sync_key}"
+directory_sync_copy="${test_root}/directory-sync-key.copy"
+cp "${directory_sync_key}" "${directory_sync_copy}"
+executor_ensure_master_key \
+ "${staging_parent}" "${directory_sync_dir}" "${directory_sync_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "retry changed the published key after sync failure" cmp -s \
+ "${directory_sync_copy}" "${directory_sync_key}"
+pass "directory sync failures stop installation and recover on retry"
+
+sync_order_dir="${test_root}/sync-order"
+mkdir "${sync_order_dir}"
+sync_order_key="${sync_order_dir}/master.key"
+sync_order_ledger="${test_root}/sync-order-ledger"
+writer_definition=$(declare -f executor_write_and_sync_master_key)
+sync_definition=$(declare -f executor_sync_filesystem_path)
+executor_write_and_sync_master_key() {
+ printf 'file-sync|destination=%s\n' "$(test -e "${sync_order_key}" && printf present || printf absent)" \
+ >> "${sync_order_ledger}"
+ dd if=/dev/urandom of="$1" bs=32 count=1 \
+ iflag=fullblock oflag=nofollow conv=fsync status=none
+}
+executor_sync_filesystem_path() {
+ if [[ $1 == "${staging_parent}"/.executor-master-key.*/master.key ]]; then
+ printf 'metadata-sync|destination=%s\n' \
+ "$(test -e "${sync_order_key}" && printf present || printf absent)" \
+ >> "${sync_order_ledger}"
+ elif [[ $1 == "${sync_order_dir}" \
+ || $1 == "${staging_parent}" \
+ && -e ${sync_order_key} \
+ && -e ${staging_parent}/.executor-master-key.recovery ]]; then
+ printf 'directory-sync|%s|destination=%s\n' "$1" \
+ "$(test -e "${sync_order_key}" && printf present || printf absent)" \
+ >> "${sync_order_ledger}"
+ fi
+ command sync -f -- "$1"
+}
+executor_ensure_master_key \
+ "${staging_parent}" "${sync_order_dir}" "${sync_order_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+eval "${writer_definition}"
+eval "${sync_definition}"
+first_sync_event=$(sed -n '1p' "${sync_order_ledger}")
+metadata_sync_event=$(sed -n '2p' "${sync_order_ledger}")
+first_directory_event=$(sed -n '3p' "${sync_order_ledger}")
+assert "file fsync did not happen before publication" test \
+ "${first_sync_event}" = "file-sync|destination=absent"
+assert "final key metadata was not synced before publication" test \
+ "${metadata_sync_event}" = "metadata-sync|destination=absent"
+assert "destination directory sync did not happen after publication" test \
+ "${first_directory_event}" = \
+ "directory-sync|${sync_order_dir}|destination=present"
+assert "publication cleanup did not receive all required directory syncs" test \
+ "$(wc -l < "${sync_order_ledger}" | tr -d ' ')" = 5
+pass "master key file and directory sync ordering is explicit"
+
+metadata_crash_dir="${test_root}/metadata-sync-crash"
+mkdir "${metadata_crash_dir}"
+metadata_crash_key="${metadata_crash_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_METADATA_SYNC=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${metadata_crash_dir}" "${metadata_crash_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "metadata-sync crash injection reported success"
+fi
+recovery_path="${staging_parent}/.executor-master-key.recovery"
+metadata_staging_name=$(awk '$1 == "staging" { print $2 }' "${recovery_path}")
+metadata_recorded_alias="${staging_parent}/${metadata_staging_name}/master.key"
+metadata_recorded_copy="${test_root}/metadata-recorded-key.copy"
+cp "${metadata_recorded_alias}" "${metadata_recorded_copy}"
+assert "metadata-sync crash exposed the destination before publication" test \
+ ! -e "${metadata_crash_key}"
+assert "metadata-sync crash did not preserve final staged metadata" test \
+ "$(stat --format='%u:%g:%a:%s:%h' -- "${metadata_recorded_alias}")" = \
+ "${CURRENT_UID}:${CURRENT_GID}:600:32:1"
+chmod 0640 "${metadata_recorded_alias}"
+if executor_ensure_master_key \
+ "${staging_parent}" "${metadata_crash_dir}" "${metadata_crash_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "recovery accepted staged metadata that differed from its record"
+fi
+assert "metadata mismatch retired the recovery record" test -e "${recovery_path}"
+chmod 0600 "${metadata_recorded_alias}"
+executor_ensure_master_key \
+ "${staging_parent}" "${metadata_crash_dir}" "${metadata_crash_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "metadata-sync recovery did not publish the exact staged key" cmp -s \
+ "${metadata_recorded_copy}" "${metadata_crash_key}"
+pass "metadata-sync crash recovers only the exact final key metadata"
+
+destination_crash_dir="${test_root}/destination-sync-crash"
+mkdir "${destination_crash_dir}"
+destination_crash_key="${destination_crash_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_DESTINATION_SYNC=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${destination_crash_dir}" \
+ "${destination_crash_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "destination-sync crash injection reported success"
+fi
+assert "destination-sync crash did not publish the key" test \
+ -e "${destination_crash_key}"
+assert "destination-sync crash did not leave the recorded two-link state" test \
+ "$(stat --format='%h' -- "${destination_crash_key}")" = 2
+destination_crash_copy="${test_root}/destination-sync-crash.copy"
+cp "${destination_crash_key}" "${destination_crash_copy}"
+executor_ensure_master_key \
+ "${staging_parent}" "${destination_crash_dir}" \
+ "${destination_crash_key}" "${CURRENT_UID}" "${CURRENT_GID}"
+assert "destination-sync recovery changed the key" cmp -s \
+ "${destination_crash_copy}" "${destination_crash_key}"
+assert "destination-sync recovery did not retire the staging alias" test \
+ "$(stat --format='%h' -- "${destination_crash_key}")" = 1
+pass "destination-sync crash recovers the exact staged key"
+
+alias_cleanup_dir="${test_root}/alias-cleanup-failure"
+alias_cleanup_shim="${test_root}/alias-cleanup-shim"
+mkdir "${alias_cleanup_dir}" "${alias_cleanup_shim}"
+alias_cleanup_key="${alias_cleanup_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_DESTINATION_SYNC=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${alias_cleanup_dir}" "${alias_cleanup_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "alias cleanup crash injection reported success"
+fi
+alias_cleanup_copy="${test_root}/alias-cleanup-key.copy"
+cp "${alias_cleanup_key}" "${alias_cleanup_copy}"
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'target=${!#}' \
+ 'case "${target}" in' \
+ ' */.executor-master-key.????????/master.key) exit 78 ;;' \
+ 'esac' \
+ 'exec "${REAL_RM}" "$@"' > "${alias_cleanup_shim}/rm"
+chmod 0700 "${alias_cleanup_shim}/rm"
+if PATH="${alias_cleanup_shim}:${PATH}" REAL_RM="${real_rm}" \
+ executor_ensure_master_key \
+ "${staging_parent}" "${alias_cleanup_dir}" "${alias_cleanup_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "recorded alias cleanup failure was ignored"
+fi
+assert "alias cleanup failure removed the recovery record" test \
+ -e "${staging_parent}/.executor-master-key.recovery"
+assert "alias cleanup failure changed the two-link key" test \
+ "$(stat --format='%h' -- "${alias_cleanup_key}")" = 2
+executor_ensure_master_key \
+ "${staging_parent}" "${alias_cleanup_dir}" "${alias_cleanup_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "alias cleanup retry changed the published key" cmp -s \
+ "${alias_cleanup_copy}" "${alias_cleanup_key}"
+pass "recorded alias cleanup failure remains safely retryable"
+
+alias_unlink_crash_dir="${test_root}/alias-unlink-crash"
+mkdir "${alias_unlink_crash_dir}"
+alias_unlink_crash_key="${alias_unlink_crash_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_ALIAS_UNLINK=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${alias_unlink_crash_dir}" \
+ "${alias_unlink_crash_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "alias-unlink crash injection reported success"
+fi
+assert "alias-unlink crash did not leave the published key" test \
+ -e "${alias_unlink_crash_key}"
+assert "alias-unlink crash left an additional key link" test \
+ "$(stat --format='%h' -- "${alias_unlink_crash_key}")" = 1
+alias_unlink_crash_copy="${test_root}/alias-unlink-crash.copy"
+cp "${alias_unlink_crash_key}" "${alias_unlink_crash_copy}"
+executor_ensure_master_key \
+ "${staging_parent}" "${alias_unlink_crash_dir}" \
+ "${alias_unlink_crash_key}" "${CURRENT_UID}" "${CURRENT_GID}"
+assert "alias-unlink recovery changed the key" cmp -s \
+ "${alias_unlink_crash_copy}" "${alias_unlink_crash_key}"
+pass "alias-unlink crash retires the durable recovery record"
+
+substitution_dir="${test_root}/recovery-substitution"
+mkdir "${substitution_dir}"
+substitution_key="${substitution_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_DESTINATION_SYNC=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${substitution_dir}" "${substitution_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "substitution crash injection reported success"
+fi
+recovery_path="${staging_parent}/.executor-master-key.recovery"
+substitution_staging_name=$(awk '$1 == "staging" { print $2 }' "${recovery_path}")
+substitution_alias="${staging_parent}/${substitution_staging_name}/master.key"
+substitution_unknown_alias="${test_root}/unknown-master-key-alias"
+substitution_copy="${test_root}/recovery-substitution.copy"
+cp "${substitution_key}" "${substitution_copy}"
+rm -f -- "${substitution_alias}"
+ln "${substitution_key}" "${substitution_unknown_alias}"
+printf 'attacker-substitution-must-stay!' > "${substitution_alias}"
+chmod 0600 "${substitution_alias}"
+substitution_alias_copy="${test_root}/recovery-substitution-alias.copy"
+cp "${substitution_alias}" "${substitution_alias_copy}"
+if executor_ensure_master_key \
+ "${staging_parent}" "${substitution_dir}" "${substitution_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "mismatched recorded staging alias was accepted"
+fi
+assert "failed substitution recovery changed the destination key" cmp -s \
+ "${substitution_copy}" "${substitution_key}"
+assert "failed substitution recovery changed the attacker file" cmp -s \
+ "${substitution_alias_copy}" "${substitution_alias}"
+assert "failed substitution recovery removed an unknown hard link" test \
+ -e "${substitution_unknown_alias}"
+rm -f -- "${substitution_alias}" "${substitution_unknown_alias}"
+executor_ensure_master_key \
+ "${staging_parent}" "${substitution_dir}" "${substitution_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "cleanup retry changed the destination key" cmp -s \
+ "${substitution_copy}" "${substitution_key}"
+pass "recovery rejects substituted and unknown master key aliases"
+
+foreign_with_alias_dir="${test_root}/foreign-destination-with-alias"
+mkdir "${foreign_with_alias_dir}"
+foreign_with_alias_key="${foreign_with_alias_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_DESTINATION_SYNC=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${foreign_with_alias_dir}" \
+ "${foreign_with_alias_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "foreign-destination alias crash injection reported success"
+fi
+recovery_path="${staging_parent}/.executor-master-key.recovery"
+foreign_staging_name=$(awk '$1 == "staging" { print $2 }' "${recovery_path}")
+foreign_recorded_alias="${staging_parent}/${foreign_staging_name}/master.key"
+foreign_recorded_copy="${test_root}/foreign-recorded-key.copy"
+cp "${foreign_recorded_alias}" "${foreign_recorded_copy}"
+rm -f -- "${foreign_with_alias_key}"
+printf 'foreign-destination-must-stay!!!' > "${foreign_with_alias_key}"
+chmod 0600 "${foreign_with_alias_key}"
+foreign_destination_copy="${test_root}/foreign-destination.copy"
+cp "${foreign_with_alias_key}" "${foreign_destination_copy}"
+if executor_ensure_master_key \
+ "${staging_parent}" "${foreign_with_alias_dir}" \
+ "${foreign_with_alias_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "foreign destination was accepted while the recorded alias existed"
+fi
+assert "foreign destination changed after failed recovery" cmp -s \
+ "${foreign_destination_copy}" "${foreign_with_alias_key}"
+assert "recorded alias changed after failed recovery" cmp -s \
+ "${foreign_recorded_copy}" "${foreign_recorded_alias}"
+assert "recovery record was retired after destination substitution" test \
+ -e "${recovery_path}"
+rm -f -- "${foreign_with_alias_key}"
+ln "${foreign_recorded_alias}" "${foreign_with_alias_key}"
+executor_ensure_master_key \
+ "${staging_parent}" "${foreign_with_alias_dir}" \
+ "${foreign_with_alias_key}" "${CURRENT_UID}" "${CURRENT_GID}"
+assert "restored recorded key changed during recovery" cmp -s \
+ "${foreign_recorded_copy}" "${foreign_with_alias_key}"
+pass "recovery preserves a recorded alias when the destination is foreign"
+
+foreign_without_alias_dir="${test_root}/foreign-destination-without-alias"
+mkdir "${foreign_without_alias_dir}"
+foreign_without_alias_key="${foreign_without_alias_dir}/master.key"
+if EXECUTOR_INSTALL_TEST_CRASH_AFTER_KEY_DESTINATION_SYNC=1 \
+ executor_ensure_master_key \
+ "${staging_parent}" "${foreign_without_alias_dir}" \
+ "${foreign_without_alias_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "foreign-destination no-alias crash injection reported success"
+fi
+recovery_path="${staging_parent}/.executor-master-key.recovery"
+foreign_staging_name=$(awk '$1 == "staging" { print $2 }' "${recovery_path}")
+foreign_recorded_alias="${staging_parent}/${foreign_staging_name}/master.key"
+rm -f -- "${foreign_without_alias_key}" "${foreign_recorded_alias}"
+printf 'foreign-destination-must-stay!!!' > "${foreign_without_alias_key}"
+chmod 0600 "${foreign_without_alias_key}"
+foreign_destination_copy="${test_root}/foreign-no-alias-destination.copy"
+cp "${foreign_without_alias_key}" "${foreign_destination_copy}"
+if executor_ensure_master_key \
+ "${staging_parent}" "${foreign_without_alias_dir}" \
+ "${foreign_without_alias_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "foreign destination was accepted after the recorded alias disappeared"
+fi
+assert "foreign no-alias destination changed after failed recovery" cmp -s \
+ "${foreign_destination_copy}" "${foreign_without_alias_key}"
+assert "no-alias recovery record was retired after substitution" test \
+ -e "${recovery_path}"
+rm -f -- "${foreign_without_alias_key}"
+executor_ensure_master_key \
+ "${staging_parent}" "${foreign_without_alias_dir}" \
+ "${foreign_without_alias_key}" "${CURRENT_UID}" "${CURRENT_GID}"
+assert "cleanup retry did not create a valid key" test \
+ "$(stat --format='%s:%h:%a' -- "${foreign_without_alias_key}")" = 32:1:600
+pass "recovery preserves state when a foreign destination has no recorded alias"
+
+attack_dir="${test_root}/temp-swap"
+attack_shim_dir="${test_root}/temp-swap-shim"
+mkdir "${attack_dir}" "${attack_shim_dir}"
+attack_key="${attack_dir}/master.key"
+attack_victim="${test_root}/temp-swap-victim"
+attack_victim_copy="${test_root}/temp-swap-victim.copy"
+printf 'victim-must-remain-byte-for-byte!' > "${attack_victim}"
+cp "${attack_victim}" "${attack_victim_copy}"
+real_dd=$(command -v dd)
+real_ln=$(command -v ln)
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'output_path=""' \
+ 'for argument in "$@"; do' \
+ ' case "${argument}" in' \
+ ' of=*) output_path=${argument#of=} ;;' \
+ ' esac' \
+ 'done' \
+ 'printf seen > "${DD_SEEN}"' \
+ 'case "${output_path}" in' \
+ ' "${ATTACKER_DATA_DIR}"/.master.key.*)' \
+ ' printf attacked > "${ATTACK_TRIGGER}"' \
+ ' rm -f -- "${output_path}"' \
+ ' "${REAL_LN}" -s -- "${ATTACKER_VICTIM}" "${output_path}"' \
+ ' ;;' \
+ 'esac' \
+ 'exec "${REAL_DD}" "$@"' > "${attack_shim_dir}/dd"
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'source_path=""' \
+ 'destination=""' \
+ 'for argument in "$@"; do' \
+ ' source_path=${destination}' \
+ ' destination=${argument}' \
+ 'done' \
+ 'printf seen > "${LN_SEEN}"' \
+ 'case "${source_path}" in' \
+ ' "${ATTACKER_DATA_DIR}"/.master.key.*)' \
+ ' printf attacked > "${ATTACK_TRIGGER}"' \
+ ' rm -f -- "${source_path}"' \
+ ' "${REAL_LN}" -s -- "${ATTACKER_VICTIM}" "${source_path}"' \
+ ' ;;' \
+ 'esac' \
+ 'exec "${REAL_LN}" "$@"' > "${attack_shim_dir}/ln"
+chmod 0700 "${attack_shim_dir}/dd" "${attack_shim_dir}/ln"
+attack_trigger="${test_root}/temp-swap-trigger"
+dd_seen="${test_root}/temp-swap-dd-seen"
+ln_seen="${test_root}/temp-swap-ln-seen"
+PATH="${attack_shim_dir}:${PATH}" \
+ ATTACKER_DATA_DIR="${attack_dir}" \
+ ATTACKER_VICTIM="${attack_victim}" \
+ ATTACK_TRIGGER="${attack_trigger}" \
+ DD_SEEN="${dd_seen}" \
+ LN_SEEN="${ln_seen}" \
+ REAL_DD="${real_dd}" \
+ REAL_LN="${real_ln}" \
+ executor_ensure_master_key \
+ "${staging_parent}" "${attack_dir}" "${attack_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}"
+assert "adversarial dd shim did not run" test -e "${dd_seen}"
+assert "adversarial ln shim did not run" test -e "${ln_seen}"
+assert "attacker reached a temporary key in the data directory" test \
+ ! -e "${attack_trigger}"
+assert "temporary key swap changed the victim" cmp -s \
+ "${attack_victim_copy}" "${attack_victim}"
+assert "staged key was not published" test -e "${attack_key}"
+pass "temporary key stays outside the executor-owned data directory"
+
+directory_race_dir="${test_root}/directory-race"
+directory_race_target="${test_root}/directory-race-target"
+directory_race_shim="${test_root}/directory-race-shim"
+mkdir "${directory_race_dir}" "${directory_race_target}" \
+ "${directory_race_shim}"
+directory_race_key="${directory_race_dir}/master.key"
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'destination=${!#}' \
+ '"${REAL_LN}" -s -- "${RACE_DIRECTORY}" "${destination}"' \
+ 'exec "${REAL_LN}" "$@"' > "${directory_race_shim}/ln"
+chmod 0700 "${directory_race_shim}/ln"
+if PATH="${directory_race_shim}:${PATH}" \
+ REAL_LN="${real_ln}" \
+ RACE_DIRECTORY="${directory_race_target}" \
+ executor_ensure_master_key \
+ "${staging_parent}" "${directory_race_dir}" \
+ "${directory_race_key}" "${CURRENT_UID}" "${CURRENT_GID}" \
+ 2>/dev/null; then
+ fail "destination symlink to a directory was accepted"
+fi
+assert "directory-race destination is not a symlink" test \
+ -L "${directory_race_key}"
+if find "${directory_race_target}" -mindepth 1 -print -quit | read -r _; then
+ fail "key was linked inside a racing destination directory"
+fi
+pass "destination symlink to a directory cannot redirect publication"
+
+sentinel_dir="${test_root}/sentinel"
+shim_dir="${test_root}/shim"
+mkdir "${sentinel_dir}" "${shim_dir}"
+printf '%s\n' \
+ '#!/usr/bin/env bash' \
+ 'set -euo pipefail' \
+ 'destination=${!#}' \
+ 'printf sentinel > "${destination}"' \
+ 'exec "${REAL_LN}" "$@"' > "${shim_dir}/ln"
+chmod 0700 "${shim_dir}/ln"
+sentinel_key="${sentinel_dir}/master.key"
+if PATH="${shim_dir}:${PATH}" REAL_LN="${real_ln}" \
+ executor_ensure_master_key \
+ "${staging_parent}" "${sentinel_dir}" "${sentinel_key}" \
+ "${CURRENT_UID}" "${CURRENT_GID}" 2>/dev/null; then
+ fail "racing sentinel was overwritten"
+fi
+assert "racing sentinel content changed" test \
+ "$(cat "${sentinel_key}")" = sentinel
+if compgen -G "${sentinel_dir}/.master.key.*" >/dev/null; then
+ fail "temporary key remained after publication race"
+fi
+pass "racing sentinel is preserved and temporary key is removed"
+
+if [[ ${EUID} -eq 0 ]]; then
+ wrong_owner_dir="${test_root}/wrong-owner"
+ mkdir "${wrong_owner_dir}"
+ wrong_owner_key="${wrong_owner_dir}/master.key"
+ printf '76543210fedcba9876543210fedcba98' > "${wrong_owner_key}"
+ chmod 0600 "${wrong_owner_key}"
+ if executor_ensure_master_key \
+ "${staging_parent}" "${wrong_owner_dir}" \
+ "${wrong_owner_key}" 65534 65534 \
+ 2>/dev/null; then
+ fail "wrong-owner key was accepted"
+ fi
+ assert "wrong-owner key ownership was changed" test \
+ "$(stat --format='%u:%g' -- "${wrong_owner_key}")" = 0:0
+ pass "root does not repair an existing key's ownership"
+
+ alternate_dir="${test_root}/alternate-owner"
+ mkdir "${alternate_dir}"
+ alternate_key="${alternate_dir}/master.key"
+ executor_ensure_master_key \
+ "${staging_parent}" "${alternate_dir}" \
+ "${alternate_key}" 65534 65534
+ assert "root-created key has the wrong owner" test \
+ "$(stat --format='%u:%g' -- "${alternate_key}")" = 65534:65534
+ pass "root publishes ownership before exposing the key"
+else
+ TEST_NUMBER=$((TEST_NUMBER + 1))
+ printf 'ok %s - root leaves wrong ownership unchanged # SKIP requires root\n' \
+ "${TEST_NUMBER}"
+ TEST_NUMBER=$((TEST_NUMBER + 1))
+ printf 'ok %s - root publishes alternate ownership # SKIP requires root\n' \
+ "${TEST_NUMBER}"
+fi
+
+if find "${staging_parent}" -mindepth 1 \
+ ! -name '.executor-master-key.lock' -print -quit | read -r _; then
+ fail "private master key staging directory was not cleaned up"
+fi
+assert "persistent master key lock has unsafe metadata" test \
+ "$(stat --format='%F:%u:%a:%h:%s' -- \
+ "${staging_parent}/.executor-master-key.lock")" = \
+ "regular empty file:${CURRENT_UID}:600:1:0"
+
+printf '1..%s\n' "${TEST_NUMBER}"
diff --git a/scripts/verify-apple-toolchain.sh b/scripts/verify-apple-toolchain.sh
new file mode 100644
index 000000000..9bfc1f49a
--- /dev/null
+++ b/scripts/verify-apple-toolchain.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "$#" -ne 6 ]]; then
+ printf 'Usage: %s \n' "$0" >&2
+ exit 2
+fi
+
+developer_dir=$1
+expected_xcode_version=$2
+expected_xcode_build=$3
+expected_sdk_version=$4
+expected_clang_version=$5
+expected_ld_version=$6
+
+[[ -d "$developer_dir" && ! -L "$developer_dir" ]] \
+ || { printf 'Pinned Xcode developer directory is unavailable: %s\n' "$developer_dir" >&2; exit 1; }
+
+export DEVELOPER_DIR="$developer_dir"
+
+xcode_info="$(xcodebuild -version)"
+expected_xcode_info="$(printf 'Xcode %s\nBuild version %s' \
+ "$expected_xcode_version" \
+ "$expected_xcode_build")"
+[[ "$xcode_info" == "$expected_xcode_info" ]] \
+ || { printf 'Unexpected Xcode identity:\n%s\n' "$xcode_info" >&2; exit 1; }
+
+sdk_version="$(xcrun --sdk macosx --show-sdk-version)"
+[[ "$sdk_version" == "$expected_sdk_version" ]] \
+ || { printf 'Unexpected macOS SDK version: %s\n' "$sdk_version" >&2; exit 1; }
+sdk_path="$(xcrun --sdk macosx --show-sdk-path)"
+[[ "$sdk_path" == */"MacOSX${expected_sdk_version}.sdk" ]] \
+ || { printf 'Unexpected macOS SDK path: %s\n' "$sdk_path" >&2; exit 1; }
+
+clang_info="$(xcrun clang --version)"
+clang_banner="${clang_info%%$'\n'*}"
+[[ "$clang_banner" == "$expected_clang_version" ]] \
+ || { printf 'Unexpected Apple clang version: %s\n' "$clang_banner" >&2; exit 1; }
+
+ld_info="$(xcrun ld -v 2>&1)"
+ld_banner="${ld_info%%$'\n'*}"
+[[ "$ld_banner" == "@(#)PROGRAM:ld PROJECT:ld-${expected_ld_version}" ]] \
+ || { printf 'Unexpected Apple ld version: %s\n' "$ld_banner" >&2; exit 1; }
+
+toolchain_bin="${developer_dir}/Toolchains/XcodeDefault.xctoolchain/usr/bin"
+[[ "$(xcrun --find clang)" == "${toolchain_bin}/clang" ]] \
+ || { printf 'xcrun resolved clang outside the pinned Xcode toolchain\n' >&2; exit 1; }
+[[ "$(xcrun --find ld)" == "${toolchain_bin}/ld" ]] \
+ || { printf 'xcrun resolved ld outside the pinned Xcode toolchain\n' >&2; exit 1; }
+
+printf 'Verified Xcode %s (%s), macOS SDK %s, %s, and ld-%s\n' \
+ "$expected_xcode_version" \
+ "$expected_xcode_build" \
+ "$expected_sdk_version" \
+ "$expected_clang_version" \
+ "$expected_ld_version"
diff --git a/scripts/verify-release-index.py b/scripts/verify-release-index.py
new file mode 100644
index 000000000..44d9e9b19
--- /dev/null
+++ b/scripts/verify-release-index.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+import argparse
+import json
+import sys
+
+
+EXPECTED_PLATFORMS = {
+ ("linux", "amd64"),
+ ("linux", "arm64"),
+}
+VERSION_ANNOTATION = "org.opencontainers.image.version"
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Verify the release OCI index platforms and version annotation"
+ )
+ parser.add_argument("--version", required=True)
+ return parser.parse_args()
+
+
+def main():
+ args = parse_args()
+ try:
+ index = json.load(sys.stdin)
+ except json.JSONDecodeError as error:
+ raise SystemExit(f"release index is not valid JSON: {error}") from error
+ if not isinstance(index, dict):
+ raise SystemExit("release index must be a JSON object")
+
+ annotations = index.get("annotations")
+ if not isinstance(annotations, dict):
+ raise SystemExit("release index has no annotations object")
+ if annotations.get(VERSION_ANNOTATION) != args.version:
+ raise SystemExit(
+ f"release index version annotation does not equal {args.version}"
+ )
+
+ manifests = index.get("manifests")
+ if not isinstance(manifests, list):
+ raise SystemExit("release index has no manifests array")
+ platforms = []
+ for manifest in manifests:
+ if not isinstance(manifest, dict) or not isinstance(manifest.get("platform"), dict):
+ raise SystemExit("release index contains a manifest without a platform")
+ platform = manifest["platform"]
+ operating_system = platform.get("os")
+ architecture = platform.get("architecture")
+ if not isinstance(operating_system, str) or not isinstance(architecture, str):
+ raise SystemExit("release index contains an invalid platform")
+ platforms.append((operating_system, architecture))
+ if len(platforms) != len(EXPECTED_PLATFORMS) or set(platforms) != EXPECTED_PLATFORMS:
+ raise SystemExit(f"release index has unexpected platforms: {platforms}")
+
+ print(f"verified release index for {args.version}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/actor.rs b/src/actor.rs
new file mode 100644
index 000000000..d54fee5b3
--- /dev/null
+++ b/src/actor.rs
@@ -0,0 +1,109 @@
+use serde::Serialize;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ActorKind {
+ ApiToken,
+ Admin,
+ System,
+}
+
+impl ActorKind {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::ApiToken => "api_token",
+ Self::Admin => "admin",
+ Self::System => "system",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum SystemActor {
+ LocalCli,
+}
+
+impl SystemActor {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::LocalCli => "local_cli",
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ToolActor {
+ ApiToken {
+ id: String,
+ name_snapshot: Option,
+ },
+ Admin {
+ id: i64,
+ },
+ System(SystemActor),
+}
+
+impl ToolActor {
+ pub fn api_token(id: impl Into, name_snapshot: Option) -> Self {
+ Self::ApiToken {
+ id: id.into(),
+ name_snapshot,
+ }
+ }
+
+ pub const fn admin(id: i64) -> Self {
+ Self::Admin { id }
+ }
+
+ pub const fn local_cli() -> Self {
+ Self::System(SystemActor::LocalCli)
+ }
+
+ pub const fn kind(&self) -> ActorKind {
+ match self {
+ Self::ApiToken { .. } => ActorKind::ApiToken,
+ Self::Admin { .. } => ActorKind::Admin,
+ Self::System(_) => ActorKind::System,
+ }
+ }
+
+ pub fn id(&self) -> String {
+ match self {
+ Self::ApiToken { id, .. } => id.clone(),
+ Self::Admin { id } => id.to_string(),
+ Self::System(actor) => actor.as_str().to_owned(),
+ }
+ }
+
+ pub fn name_snapshot(&self) -> Option<&str> {
+ match self {
+ Self::ApiToken { name_snapshot, .. } => name_snapshot.as_deref(),
+ Self::Admin { .. } | Self::System(_) => None,
+ }
+ }
+
+ pub fn api_token_id(&self) -> Option<&str> {
+ match self {
+ Self::ApiToken { id, .. } => Some(id),
+ Self::Admin { .. } | Self::System(_) => None,
+ }
+ }
+
+ pub(crate) fn from_stored(
+ kind: &str,
+ id: String,
+ api_token_id: Option,
+ name_snapshot: Option,
+ ) -> Option {
+ match kind {
+ "api_token" if api_token_id.as_deref() == Some(id.as_str()) => {
+ Some(Self::ApiToken { id, name_snapshot })
+ }
+ "admin" if api_token_id.is_none() => id.parse().ok().map(|id| Self::Admin { id }),
+ "system" if api_token_id.is_none() && id == SystemActor::LocalCli.as_str() => {
+ Some(Self::System(SystemActor::LocalCli))
+ }
+ _ => None,
+ }
+ }
+}
diff --git a/src/api.rs b/src/api.rs
new file mode 100644
index 000000000..91942ee16
--- /dev/null
+++ b/src/api.rs
@@ -0,0 +1,1712 @@
+use std::{
+ collections::{HashMap, VecDeque},
+ future::Future,
+ net::{IpAddr, SocketAddr},
+ sync::{Arc, Mutex},
+ time::{Duration, Instant},
+};
+
+use axum::{
+ Json, Router,
+ extract::{
+ ConnectInfo, DefaultBodyLimit, Extension, FromRequestParts, Path, State,
+ rejection::JsonRejection,
+ },
+ http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header, request::Parts},
+ middleware::{self, Next},
+ response::{IntoResponse, Response},
+ routing::{delete, get, post},
+};
+use ipnet::IpNet;
+use serde::{Deserialize, Serialize};
+use subtle::ConstantTimeEq;
+use tokio::sync::{OwnedSemaphorePermit, Semaphore, oneshot};
+use uuid::Uuid;
+
+use crate::{
+ AppConfig,
+ catalog::CatalogStore,
+ crypto::{generate_secret, hash_password, verify_password},
+ database::{Database, SETUP_TOKEN_TTL_SECONDS},
+ execution::ExecutionService,
+ invocation::ToolCallService,
+ mcp::downstream::McpState,
+ oauth::OAuthService,
+ protocols::SourceService,
+ request_logs::RequestLogSink,
+ tasks::TaskTracker,
+ unix_timestamp,
+};
+
+mod approvals;
+mod catalog;
+mod oauth;
+pub(crate) mod openapi;
+mod protocols;
+mod token_delivery;
+
+const SESSION_COOKIE: &str = "executor_session";
+const CSRF_COOKIE: &str = "executor_csrf";
+const CSRF_HEADER: &str = "x-executor-csrf";
+const MAX_API_BODY_BYTES: usize = 16 * 1024;
+const PASSWORD_HASH_CONCURRENCY: usize = 2;
+const MAX_USERNAME_CHARACTERS: usize = 64;
+const MAX_USERNAME_BYTES: usize = 256;
+const MIN_PASSWORD_CHARACTERS: usize = 12;
+const MAX_PASSWORD_BYTES: usize = 1024;
+const MAX_SETUP_TOKEN_BYTES: usize = 128;
+const MAX_TOKEN_NAME_CHARACTERS: usize = 80;
+const MAX_TOKEN_NAME_BYTES: usize = 320;
+const LOGIN_ATTEMPT_LIMIT: usize = 5;
+const LOGIN_ATTEMPT_WINDOW: Duration = Duration::from_secs(60);
+const MAX_LOGIN_RATE_LIMIT_CLIENTS: usize = 4096;
+const MAX_FORWARDED_FOR_HOPS: usize = 64;
+const MAX_FORWARDED_FOR_BYTES: usize = 4 * 1024;
+const X_FORWARDED_FOR: &str = "x-forwarded-for";
+const IDEMPOTENCY_KEY: &str = "idempotency-key";
+const IDEMPOTENCY_REPLAYED: &str = "idempotency-replayed";
+const REVOKE_RECONCILED: &str = "revoke-reconciled";
+const TOKEN_LAST_USED_WRITE_INTERVAL_SECONDS: i64 = 60;
+const TOKEN_LAST_USED_WRITE_INTERVAL: Duration = Duration::from_secs(60);
+const MAX_TOKEN_LAST_USED_ATTEMPTS: usize = 4096;
+
+#[derive(Clone)]
+struct AppState {
+ database: Database,
+ catalog: CatalogStore,
+ sources: SourceService,
+ tool_calls: ToolCallService,
+ execution: ExecutionService,
+ mcp: McpState,
+ oauth: OAuthService,
+ request_logs: RequestLogSink,
+ origin: Arc,
+ session_ttl_seconds: i64,
+ secure_cookies: bool,
+ password_hash_slots: Arc,
+ gateway_search_slots: Arc,
+ dummy_password_hash: Arc,
+ login_rate_limiter: Arc,
+ token_last_used_tracker: Arc,
+ trusted_proxies: Arc<[IpNet]>,
+ background_tasks: TaskTracker,
+ source_creation_supervisors: protocols::SourceCreationSupervisorRegistry,
+}
+
+#[derive(Clone)]
+struct RequestId(String);
+
+#[derive(Clone, Hash, Eq, PartialEq)]
+enum LoginClientKey {
+ PeerIp(IpAddr),
+ InProcess,
+ InvalidForwardedChain(&'static str),
+}
+
+struct LoginRateLimiter {
+ attempts: Mutex>>,
+}
+
+struct TokenLastUsedTracker {
+ attempts: Mutex>,
+ write_slot: Arc,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ErrorEnvelope {
+ error: ErrorDetail,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ErrorDetail {
+ code: &'static str,
+ message: String,
+ request_id: String,
+}
+
+struct ApiError {
+ status: StatusCode,
+ code: &'static str,
+ message: String,
+ request_id: String,
+ retry_after_seconds: Option,
+}
+
+impl ApiError {
+ fn new(
+ request_id: &RequestId,
+ status: StatusCode,
+ code: &'static str,
+ message: impl Into,
+ ) -> Self {
+ Self {
+ status,
+ code,
+ message: message.into(),
+ request_id: request_id.0.clone(),
+ retry_after_seconds: None,
+ }
+ }
+
+ fn unauthorized(request_id: &RequestId, message: impl Into) -> Self {
+ Self::new(
+ request_id,
+ StatusCode::UNAUTHORIZED,
+ "unauthorized",
+ message,
+ )
+ }
+
+ fn internal(request_id: &RequestId) -> Self {
+ Self::new(
+ request_id,
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "internal_error",
+ "The request could not be completed.",
+ )
+ }
+
+ fn internal_logged(request_id: &RequestId, error: impl std::fmt::Display) -> Self {
+ tracing::error!(request_id = %request_id.0, error = %error, "control API request failed");
+ Self::internal(request_id)
+ }
+
+ fn password_work_saturated(request_id: &RequestId) -> Self {
+ Self::new(
+ request_id,
+ StatusCode::TOO_MANY_REQUESTS,
+ "password_work_saturated",
+ "Password verification is busy. Try again shortly.",
+ )
+ .with_retry_after(1)
+ }
+
+ fn login_rate_limited(request_id: &RequestId, retry_after_seconds: u64) -> Self {
+ Self::new(
+ request_id,
+ StatusCode::TOO_MANY_REQUESTS,
+ "login_rate_limited",
+ "Too many login attempts. Try again later.",
+ )
+ .with_retry_after(retry_after_seconds)
+ }
+
+ fn with_retry_after(mut self, retry_after_seconds: u64) -> Self {
+ self.retry_after_seconds = Some(retry_after_seconds);
+ self
+ }
+}
+
+impl IntoResponse for ApiError {
+ fn into_response(self) -> Response {
+ let retry_after_seconds = self.retry_after_seconds;
+ let mut response = (
+ self.status,
+ Json(ErrorEnvelope {
+ error: ErrorDetail {
+ code: self.code,
+ message: self.message,
+ request_id: self.request_id,
+ },
+ }),
+ )
+ .into_response();
+ if let Some(retry_after_seconds) = retry_after_seconds {
+ response.headers_mut().insert(
+ header::RETRY_AFTER,
+ HeaderValue::from_str(&retry_after_seconds.to_string())
+ .expect("retry delays are valid header values"),
+ );
+ }
+ response
+ }
+}
+
+impl LoginRateLimiter {
+ fn new() -> Self {
+ Self {
+ attempts: Mutex::new(HashMap::new()),
+ }
+ }
+
+ fn check(&self, client: &LoginClientKey) -> Result<(), u64> {
+ let now = Instant::now();
+ let cutoff = now.checked_sub(LOGIN_ATTEMPT_WINDOW).unwrap_or(now);
+ let mut attempts_by_client = self
+ .attempts
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+
+ attempts_by_client.retain(|_, attempts| {
+ while attempts.front().is_some_and(|attempt| *attempt <= cutoff) {
+ attempts.pop_front();
+ }
+ !attempts.is_empty()
+ });
+
+ if !attempts_by_client.contains_key(client)
+ && attempts_by_client.len() >= MAX_LOGIN_RATE_LIMIT_CLIENTS
+ {
+ let oldest_client = attempts_by_client
+ .iter()
+ .filter_map(|(client, attempts)| attempts.front().map(|attempt| (client, attempt)))
+ .min_by_key(|(_, attempt)| *attempt)
+ .map(|(client, _)| client.clone());
+ if let Some(oldest_client) = oldest_client {
+ attempts_by_client.remove(&oldest_client);
+ }
+ }
+
+ let attempts = attempts_by_client.entry(client.clone()).or_default();
+ if attempts.len() >= LOGIN_ATTEMPT_LIMIT {
+ let oldest = attempts.front().copied().unwrap_or(now);
+ let remaining = LOGIN_ATTEMPT_WINDOW.saturating_sub(now.duration_since(oldest));
+ return Err(remaining.as_secs().max(1));
+ }
+ attempts.push_back(now);
+ Ok(())
+ }
+
+ fn clear(&self, client: &LoginClientKey) {
+ self.attempts
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ .remove(client);
+ }
+}
+
+impl TokenLastUsedTracker {
+ fn new() -> Self {
+ Self {
+ attempts: Mutex::new(HashMap::new()),
+ write_slot: Arc::new(Semaphore::new(1)),
+ }
+ }
+
+ fn schedule(
+ self: &Arc,
+ database: Database,
+ token_id: String,
+ used_at: i64,
+ request_id: String,
+ background_tasks: &TaskTracker,
+ ) {
+ let attempted_at = Instant::now();
+ let mut attempts = self
+ .attempts
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ attempts.retain(|_, previous_attempt| {
+ attempted_at.saturating_duration_since(*previous_attempt)
+ < TOKEN_LAST_USED_WRITE_INTERVAL
+ });
+ if attempts.contains_key(&token_id) {
+ return;
+ }
+ let Ok(permit) = self.write_slot.clone().try_acquire_owned() else {
+ return;
+ };
+ if attempts.len() >= MAX_TOKEN_LAST_USED_ATTEMPTS
+ && let Some(oldest) = attempts
+ .iter()
+ .min_by_key(|(_, attempted_at)| *attempted_at)
+ .map(|(token_id, _)| token_id.clone())
+ {
+ attempts.remove(&oldest);
+ }
+ attempts.insert(token_id.clone(), attempted_at);
+ drop(attempts);
+
+ let tracker = Arc::clone(self);
+ background_tasks.spawn(async move {
+ let _permit = permit;
+ let database_guard = database;
+ let result = sqlx::query(
+ "UPDATE api_tokens SET last_used_at = ? WHERE id = ? AND revoked_at IS NULL \
+ AND (last_used_at IS NULL OR last_used_at <= ?)",
+ )
+ .bind(used_at)
+ .bind(&token_id)
+ .bind(used_at - TOKEN_LAST_USED_WRITE_INTERVAL_SECONDS)
+ .execute(&database_guard.pool)
+ .await;
+ if let Err(error) = result {
+ tracker.clear_failed_attempt(&token_id, attempted_at);
+ tracing::warn!(request_id, error = %error, "API token last-used update failed");
+ }
+ drop(database_guard);
+ });
+ }
+
+ fn clear_failed_attempt(&self, token_id: &str, attempted_at: Instant) {
+ let mut attempts = self
+ .attempts
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ if attempts.get(token_id) == Some(&attempted_at) {
+ attempts.remove(token_id);
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct HealthResponse {
+ status: &'static str,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct BootstrapResponse {
+ setup_required: bool,
+ authenticated: bool,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct SetupRequest {
+ setup_token: String,
+ username: String,
+ password: String,
+}
+
+#[derive(Deserialize)]
+struct LoginRequest {
+ username: String,
+ password: String,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SessionResponse {
+ username: String,
+ csrf_token: Option,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct CreateTokenRequest {
+ name: String,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct CreatedTokenResponse {
+ id: String,
+ name: String,
+ token: String,
+ created_at: i64,
+}
+
+#[derive(Serialize, sqlx::FromRow)]
+#[serde(rename_all = "camelCase")]
+struct TokenMetadata {
+ id: String,
+ name: String,
+ masked_token: String,
+ created_at: i64,
+ last_used_at: Option,
+ revoked_at: Option,
+}
+
+#[derive(Serialize)]
+struct TokenListResponse {
+ tokens: Vec,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GatewayIdentityResponse {
+ token_id: String,
+ token_name: String,
+}
+
+struct GatewayIdentity {
+ token_id: String,
+ token_name: String,
+}
+
+struct AdminMutation(i64);
+
+struct AdminAuthentication(i64);
+
+struct GatewayAuthentication(GatewayIdentity);
+
+pub(crate) struct ApiServices {
+ sources: SourceService,
+ tool_calls: ToolCallService,
+ execution: ExecutionService,
+ mcp: McpState,
+ oauth: OAuthService,
+}
+
+impl ApiServices {
+ pub(crate) fn new(
+ sources: SourceService,
+ tool_calls: ToolCallService,
+ execution: ExecutionService,
+ mcp: McpState,
+ oauth: OAuthService,
+ ) -> Self {
+ Self {
+ sources,
+ tool_calls,
+ execution,
+ mcp,
+ oauth,
+ }
+ }
+}
+
+struct AdminSession {
+ id: i64,
+ username: String,
+ session_digest: Vec,
+ csrf_digest: Vec,
+}
+
+impl FromRequestParts for AdminMutation {
+ type Rejection = ApiError;
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ state: &AppState,
+ ) -> Result {
+ let request_id = parts
+ .extensions
+ .get::()
+ .expect("request ID middleware runs before authentication")
+ .clone();
+ let admin = require_admin_mutation(&request_id, state, &parts.headers).await?;
+ Ok(Self(admin.id))
+ }
+}
+
+impl FromRequestParts for AdminAuthentication {
+ type Rejection = ApiError;
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ state: &AppState,
+ ) -> Result {
+ let request_id = parts
+ .extensions
+ .get::()
+ .expect("request ID middleware runs before authentication")
+ .clone();
+ let admin = require_admin(&request_id, state, &parts.headers).await?;
+ Ok(Self(admin.id))
+ }
+}
+
+impl FromRequestParts for GatewayAuthentication {
+ type Rejection = ApiError;
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ state: &AppState,
+ ) -> Result {
+ let request_id = parts
+ .extensions
+ .get::()
+ .expect("request ID middleware runs before authentication")
+ .clone();
+ let identity = require_gateway_token(&request_id, state, &parts.headers).await?;
+ Ok(Self(identity))
+ }
+}
+
+pub(crate) fn router(
+ database: Database,
+ catalog: CatalogStore,
+ services: ApiServices,
+ request_logs: RequestLogSink,
+ background_tasks: TaskTracker,
+ config: &AppConfig,
+ dummy_password_hash: String,
+) -> Router {
+ let gateway_search_slots = services.tool_calls.discovery_slots();
+ let state = AppState {
+ database,
+ catalog,
+ sources: services.sources,
+ tool_calls: services.tool_calls,
+ execution: services.execution,
+ mcp: services.mcp,
+ oauth: services.oauth,
+ request_logs,
+ origin: Arc::from(config.public_origin()),
+ session_ttl_seconds: config.session_ttl_seconds,
+ secure_cookies: config.origin.scheme() == "https",
+ password_hash_slots: Arc::new(Semaphore::new(PASSWORD_HASH_CONCURRENCY)),
+ gateway_search_slots,
+ dummy_password_hash: Arc::from(dummy_password_hash),
+ login_rate_limiter: Arc::new(LoginRateLimiter::new()),
+ token_last_used_tracker: Arc::new(TokenLastUsedTracker::new()),
+ trusted_proxies: config.trusted_proxies.clone(),
+ background_tasks,
+ source_creation_supervisors: protocols::SourceCreationSupervisorRegistry::default(),
+ };
+ let middleware_state = state.clone();
+
+ Router::new()
+ .route("/healthz", get(health))
+ .route("/api/v1/bootstrap", get(bootstrap))
+ .route("/api/v1/setup", post(setup))
+ .route("/api/v1/session", post(login).get(session).delete(logout))
+ .route("/api/v1/tokens", get(list_tokens).post(create_token))
+ .route("/api/v1/tokens/{id}", delete(revoke_token))
+ .route("/api/v1/gateway/whoami", get(gateway_whoami))
+ .merge(catalog::router())
+ .merge(approvals::router())
+ .merge(protocols::router())
+ .merge(openapi::router())
+ .merge(oauth::router())
+ .merge(crate::mcp::downstream::router(state.mcp.clone()))
+ .fallback(not_found)
+ .method_not_allowed_fallback(method_not_allowed)
+ .with_state(state)
+ .layer(DefaultBodyLimit::max(MAX_API_BODY_BYTES))
+ .layer(middleware::from_fn_with_state(
+ middleware_state,
+ request_id_middleware,
+ ))
+}
+
+async fn request_id_middleware(
+ State(state): State,
+ mut request: axum::extract::Request,
+ next: Next,
+) -> Response {
+ let request_id = RequestId(Uuid::new_v4().to_string());
+ let peer_ip = request
+ .extensions()
+ .get::>()
+ .map(|ConnectInfo(peer)| canonical_ip(peer.ip()));
+ let login_client = login_client_key(peer_ip, request.headers(), &state.trusted_proxies);
+ request.extensions_mut().insert(request_id.clone());
+ request.extensions_mut().insert(login_client);
+ let mut response = next.run(request).await;
+ response.headers_mut().insert(
+ "x-request-id",
+ HeaderValue::from_str(&request_id.0).expect("UUID request IDs are valid headers"),
+ );
+ response
+ .headers_mut()
+ .entry(header::CACHE_CONTROL)
+ .or_insert(HeaderValue::from_static("no-store"));
+ response
+}
+
+fn login_client_key(
+ peer_ip: Option,
+ headers: &HeaderMap,
+ trusted_proxies: &[IpNet],
+) -> LoginClientKey {
+ let Some(peer_ip) = peer_ip else {
+ return LoginClientKey::InProcess;
+ };
+ if !is_trusted_proxy(peer_ip, trusted_proxies) {
+ return LoginClientKey::PeerIp(peer_ip);
+ }
+
+ let forwarded_for = match forwarded_for_chain(headers) {
+ Ok(forwarded_for) => forwarded_for,
+ Err(reason) => return LoginClientKey::InvalidForwardedChain(reason),
+ };
+ let mut client_ip = peer_ip;
+ for forwarded_ip in forwarded_for.into_iter().rev() {
+ if !is_trusted_proxy(client_ip, trusted_proxies) {
+ return LoginClientKey::PeerIp(client_ip);
+ }
+ let Ok(forwarded_ip) = forwarded_ip.parse() else {
+ return LoginClientKey::InvalidForwardedChain("malformed X-Forwarded-For address");
+ };
+ client_ip = canonical_ip(forwarded_ip);
+ }
+ if is_trusted_proxy(client_ip, trusted_proxies) {
+ LoginClientKey::InvalidForwardedChain("no untrusted client address in X-Forwarded-For")
+ } else {
+ LoginClientKey::PeerIp(client_ip)
+ }
+}
+
+fn forwarded_for_chain(headers: &HeaderMap) -> Result, &'static str> {
+ let mut chain = Vec::new();
+ let mut total_bytes = 0_usize;
+ for value in headers.get_all(X_FORWARDED_FOR).iter() {
+ let value = value
+ .to_str()
+ .map_err(|_| "X-Forwarded-For is not valid text")?;
+ total_bytes = total_bytes
+ .checked_add(value.len())
+ .filter(|length| *length <= MAX_FORWARDED_FOR_BYTES)
+ .ok_or("X-Forwarded-For is too large")?;
+ for address in value.split(',') {
+ if chain.len() >= MAX_FORWARDED_FOR_HOPS {
+ return Err("X-Forwarded-For has too many hops");
+ }
+ chain.push(address.trim());
+ }
+ }
+ if chain.is_empty() {
+ Err("X-Forwarded-For is missing")
+ } else {
+ Ok(chain)
+ }
+}
+
+fn is_trusted_proxy(address: IpAddr, trusted_proxies: &[IpNet]) -> bool {
+ trusted_proxies
+ .iter()
+ .any(|network| network.contains(&address))
+}
+
+fn canonical_ip(address: IpAddr) -> IpAddr {
+ match address {
+ IpAddr::V6(address) => address
+ .to_ipv4_mapped()
+ .map(IpAddr::V4)
+ .unwrap_or(IpAddr::V6(address)),
+ address => address,
+ }
+}
+
+async fn health() -> Json {
+ Json(HealthResponse { status: "ok" })
+}
+
+async fn bootstrap(
+ Extension(request_id): Extension,
+ State(state): State,
+ headers: HeaderMap,
+) -> Result, ApiError> {
+ let setup_required =
+ sqlx::query_scalar::<_, i64>("SELECT NOT EXISTS(SELECT 1 FROM admins WHERE id = 1)")
+ .fetch_one(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?
+ != 0;
+ let authenticated = optional_admin_session(&state, &headers)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?
+ .is_some();
+ Ok(Json(BootstrapResponse {
+ setup_required,
+ authenticated,
+ }))
+}
+
+async fn setup(
+ Extension(request_id): Extension,
+ State(state): State,
+ headers: HeaderMap,
+ payload: Result, JsonRejection>,
+) -> Result {
+ require_origin(&request_id, &state, &headers)?;
+ let Json(payload) = parse_json(&request_id, payload)?;
+ let username = validate_username(&request_id, &payload.username)?;
+ validate_setup_password(&request_id, &payload.password)?;
+ if payload.setup_token.is_empty() || payload.setup_token.len() > MAX_SETUP_TOKEN_BYTES {
+ return Err(ApiError::unauthorized(
+ &request_id,
+ "The setup token is invalid or has expired.",
+ ));
+ }
+
+ let digest = state
+ .database
+ .keyring
+ .digest("setup-token", payload.setup_token.as_bytes());
+ let now = unix_timestamp();
+ let setup_status = sqlx::query_scalar::<_, i64>(
+ "SELECT CASE \
+ WHEN EXISTS(SELECT 1 FROM admins WHERE id = 1) THEN 2 \
+ WHEN EXISTS(SELECT 1 FROM setup_state WHERE id = 1 AND used_at IS NULL \
+ AND token_digest = ? AND created_at >= ?) THEN 1 \
+ ELSE 0 END",
+ )
+ .bind(digest.to_vec())
+ .bind(now - SETUP_TOKEN_TTL_SECONDS)
+ .fetch_one(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ match setup_status {
+ 2 => {
+ return Err(ApiError::new(
+ &request_id,
+ StatusCode::CONFLICT,
+ "setup_complete",
+ "This Executor instance is already configured.",
+ ));
+ }
+ 1 => {}
+ _ => {
+ return Err(ApiError::unauthorized(
+ &request_id,
+ "The setup token is invalid or has expired.",
+ ));
+ }
+ }
+
+ let permit = acquire_password_slot(&request_id, &state)?;
+ let password = payload.password;
+ let password_hash = tokio::task::spawn_blocking(move || {
+ let _permit = permit;
+ hash_password(&password)
+ })
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ let claim_time = unix_timestamp();
+ let mut transaction = state
+ .database
+ .pool
+ .begin()
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ let inserted = sqlx::query(
+ "INSERT OR IGNORE INTO admins (id, username, password_hash, created_at) \
+ SELECT 1, ?, ?, ? FROM setup_state \
+ WHERE id = 1 AND used_at IS NULL AND token_digest = ? AND created_at >= ?",
+ )
+ .bind(&username)
+ .bind(password_hash)
+ .bind(claim_time)
+ .bind(digest.to_vec())
+ .bind(claim_time - SETUP_TOKEN_TTL_SECONDS)
+ .execute(&mut *transaction)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?
+ .rows_affected();
+
+ if inserted == 0 {
+ let setup_complete =
+ sqlx::query_scalar::<_, i64>("SELECT EXISTS(SELECT 1 FROM admins WHERE id = 1)")
+ .fetch_one(&mut *transaction)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?
+ != 0;
+ transaction
+ .rollback()
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ return if setup_complete {
+ Err(ApiError::new(
+ &request_id,
+ StatusCode::CONFLICT,
+ "setup_complete",
+ "This Executor instance is already configured.",
+ ))
+ } else {
+ Err(ApiError::unauthorized(
+ &request_id,
+ "The setup token is invalid or has expired.",
+ ))
+ };
+ }
+
+ sqlx::query("UPDATE setup_state SET used_at = ? WHERE id = 1")
+ .bind(claim_time)
+ .execute(&mut *transaction)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ transaction
+ .commit()
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+
+ Ok((
+ StatusCode::CREATED,
+ Json(SessionResponse {
+ username,
+ csrf_token: None,
+ }),
+ ))
+}
+
+async fn login(
+ Extension(request_id): Extension,
+ Extension(login_client): Extension,
+ State(state): State,
+ headers: HeaderMap,
+ payload: Result, JsonRejection>,
+) -> Result {
+ require_origin(&request_id, &state, &headers)?;
+ let Json(payload) = parse_json(&request_id, payload)?;
+ let requested_username = validate_username(&request_id, &payload.username)?;
+ validate_login_password(&request_id, &payload.password)?;
+ if let LoginClientKey::InvalidForwardedChain(reason) = &login_client {
+ tracing::warn!(request_id = %request_id.0, reason, "trusted proxy chain rejected");
+ return Err(ApiError::unauthorized(
+ &request_id,
+ "The request could not be authenticated.",
+ ));
+ }
+ state
+ .login_rate_limiter
+ .check(&login_client)
+ .map_err(|retry_after| ApiError::login_rate_limited(&request_id, retry_after))?;
+ let admin = sqlx::query_as::<_, (i64, String, String)>(
+ "SELECT id, username, password_hash FROM admins WHERE username = ?",
+ )
+ .bind(&requested_username)
+ .fetch_optional(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ let (admin_id, username, password_hash, admin_exists) = match admin {
+ Some((admin_id, username, password_hash)) => (admin_id, username, password_hash, true),
+ None => (
+ 0,
+ requested_username,
+ state.dummy_password_hash.to_string(),
+ false,
+ ),
+ };
+ let permit = acquire_password_slot(&request_id, &state)?;
+ let password = payload.password;
+ let verified = tokio::task::spawn_blocking(move || {
+ let _permit = permit;
+ verify_password(&password, &password_hash)
+ })
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ if !admin_exists || !verified {
+ return Err(ApiError::unauthorized(
+ &request_id,
+ "The username or password is incorrect.",
+ ));
+ }
+
+ let session_token = generate_secret("ses_");
+ let csrf_token = generate_secret("csrf_");
+ let session_digest = state
+ .database
+ .keyring
+ .digest("admin-session", session_token.as_bytes());
+ let csrf_digest = state
+ .database
+ .keyring
+ .digest("csrf-token", csrf_token.as_bytes());
+ let now = unix_timestamp();
+ let expires_at = now + state.session_ttl_seconds;
+ sqlx::query("DELETE FROM admin_sessions WHERE expires_at <= ?")
+ .bind(now)
+ .execute(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ sqlx::query(
+ "INSERT INTO admin_sessions \
+ (session_digest, admin_id, csrf_digest, created_at, expires_at) \
+ VALUES (?, ?, ?, ?, ?)",
+ )
+ .bind(session_digest.to_vec())
+ .bind(admin_id)
+ .bind(csrf_digest.to_vec())
+ .bind(now)
+ .bind(expires_at)
+ .execute(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ state.login_rate_limiter.clear(&login_client);
+
+ let mut response = Json(SessionResponse {
+ username,
+ csrf_token: Some(csrf_token.clone()),
+ })
+ .into_response();
+ append_set_cookie(
+ response.headers_mut(),
+ session_cookie(&state, &session_token),
+ );
+ append_set_cookie(response.headers_mut(), csrf_cookie(&state, &csrf_token));
+ Ok(response)
+}
+
+async fn session(
+ Extension(request_id): Extension,
+ State(state): State,
+ headers: HeaderMap,
+) -> Result, ApiError> {
+ let admin = require_admin(&request_id, &state, &headers).await?;
+ Ok(Json(SessionResponse {
+ username: admin.username,
+ csrf_token: None,
+ }))
+}
+
+async fn logout(
+ Extension(request_id): Extension,
+ State(state): State,
+ headers: HeaderMap,
+) -> Result {
+ let admin = require_admin_mutation(&request_id, &state, &headers).await?;
+ sqlx::query("DELETE FROM admin_sessions WHERE session_digest = ?")
+ .bind(admin.session_digest)
+ .execute(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+
+ let mut response = StatusCode::NO_CONTENT.into_response();
+ append_set_cookie(response.headers_mut(), clear_cookie(SESSION_COOKIE));
+ append_set_cookie(response.headers_mut(), clear_cookie(CSRF_COOKIE));
+ Ok(response)
+}
+
+async fn create_token(
+ Extension(request_id): Extension,
+ State(state): State,
+ headers: HeaderMap,
+ payload: Result, JsonRejection>,
+) -> Result {
+ let admin = require_admin_mutation(&request_id, &state, &headers).await?;
+ let idempotency_key = required_idempotency_key(&request_id, &headers)?;
+ let Json(payload) = parse_json(&request_id, payload)?;
+ let name = payload.name.trim();
+ if name.is_empty()
+ || name.chars().count() > MAX_TOKEN_NAME_CHARACTERS
+ || name.len() > MAX_TOKEN_NAME_BYTES
+ {
+ return Err(ApiError::new(
+ &request_id,
+ StatusCode::BAD_REQUEST,
+ "invalid_token_name",
+ "Token names must contain between 1 and 80 characters.",
+ ));
+ }
+
+ let claim = token_delivery::claim_token_creation(
+ &state.database.pool,
+ &state.database.keyring,
+ admin.id,
+ name,
+ idempotency_key,
+ unix_timestamp(),
+ )
+ .await
+ .map_err(|error| token_delivery_error(&request_id, error))?;
+
+ match claim {
+ token_delivery::TokenCreationClaim::Fresh(token) => {
+ Ok(created_token_response(token, false))
+ }
+ token_delivery::TokenCreationClaim::Replay(token) => {
+ Ok(created_token_response(token, true))
+ }
+ token_delivery::TokenCreationClaim::Mismatch => Err(ApiError::new(
+ &request_id,
+ StatusCode::CONFLICT,
+ "idempotency_mismatch",
+ "The Idempotency-Key is already bound to a different token creation request.",
+ )),
+ token_delivery::TokenCreationClaim::Revoked => {
+ let mut response = ApiError::new(
+ &request_id,
+ StatusCode::CONFLICT,
+ "idempotency_replay_revoked",
+ "The token created by this Idempotency-Key has been revoked.",
+ )
+ .into_response();
+ response
+ .headers_mut()
+ .insert(IDEMPOTENCY_REPLAYED, HeaderValue::from_static("true"));
+ Ok(response)
+ }
+ }
+}
+
+fn required_idempotency_key<'a>(
+ request_id: &RequestId,
+ headers: &'a HeaderMap,
+) -> Result<&'a str, ApiError> {
+ let mut values = headers.get_all(IDEMPOTENCY_KEY).iter();
+ let Some(value) = values.next() else {
+ return Err(ApiError::new(
+ request_id,
+ StatusCode::BAD_REQUEST,
+ "idempotency_key_required",
+ "An Idempotency-Key header is required when creating an API token.",
+ ));
+ };
+ if values.next().is_some() {
+ return Err(ApiError::new(
+ request_id,
+ StatusCode::BAD_REQUEST,
+ "invalid_idempotency_key",
+ "The Idempotency-Key header is invalid.",
+ ));
+ }
+ let key = value.to_str().map_err(|_| {
+ ApiError::new(
+ request_id,
+ StatusCode::BAD_REQUEST,
+ "invalid_idempotency_key",
+ "The Idempotency-Key header is invalid.",
+ )
+ })?;
+ token_delivery::validate_idempotency_key(key).map_err(|_| {
+ ApiError::new(
+ request_id,
+ StatusCode::BAD_REQUEST,
+ "invalid_idempotency_key",
+ "The Idempotency-Key header is invalid.",
+ )
+ })?;
+ Ok(key)
+}
+
+fn token_delivery_error(
+ request_id: &RequestId,
+ error: token_delivery::TokenDeliveryError,
+) -> ApiError {
+ match error {
+ token_delivery::TokenDeliveryError::InvalidKey => ApiError::new(
+ request_id,
+ StatusCode::BAD_REQUEST,
+ "invalid_idempotency_key",
+ "The Idempotency-Key header is invalid.",
+ ),
+ error @ (token_delivery::TokenDeliveryError::Database(_)
+ | token_delivery::TokenDeliveryError::CorruptData) => {
+ ApiError::internal_logged(request_id, error)
+ }
+ }
+}
+
+fn created_token_response(token: token_delivery::DeliveredToken, replayed: bool) -> Response {
+ let mut response = (
+ StatusCode::CREATED,
+ Json(CreatedTokenResponse {
+ id: token.id,
+ name: token.name,
+ token: token.token,
+ created_at: token.created_at,
+ }),
+ )
+ .into_response();
+ if replayed {
+ response
+ .headers_mut()
+ .insert(IDEMPOTENCY_REPLAYED, HeaderValue::from_static("true"));
+ }
+ response
+}
+
+async fn list_tokens(
+ Extension(request_id): Extension,
+ State(state): State,
+ headers: HeaderMap,
+) -> Result, ApiError> {
+ require_admin(&request_id, &state, &headers).await?;
+ let tokens = sqlx::query_as::<_, TokenMetadata>(
+ "SELECT id, name, token_prefix || '...' || token_suffix AS masked_token, \
+ created_at, last_used_at, revoked_at \
+ FROM api_tokens ORDER BY created_at DESC, id DESC",
+ )
+ .fetch_all(&state.database.pool)
+ .await
+ .map_err(|error| ApiError::internal_logged(&request_id, error))?;
+ Ok(Json(TokenListResponse { tokens }))
+}
+
+async fn revoke_token(
+ Extension(request_id): Extension