commit 85ca0cd124040308b30dae082a84e3eb6c9b5a9f Author: Codex Bot Date: Tue Oct 20 01:10:02 2026 +0800 feat: establish Hestia server with preserved deployment resources diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9d2e1d6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +target +data +work +outputs +ci-output +.env +*.db +*.db-shm +*.db-wal +**/.git +**/target +**/AGENTS.md +**/work +**/ci-output diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3527928 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Docker Compose reads .env; programs read exported host settings. +ASTER_PORT=8080 +ASTER_LISTEN=127.0.0.1:8080 +# JSON host information, separate from databases: +ASTER_STATE_DIR=data/host +# PostgreSQL provisioning only: credentials are not consumed by the VM host. +# Database-backed objects will receive an explicit DatabaseConfig. +POSTGRES_PASSWORD=replace-with-a-random-hex-password diff --git a/.gitea/README.md b/.gitea/README.md new file mode 100644 index 0000000..a68a774 --- /dev/null +++ b/.gitea/README.md @@ -0,0 +1,24 @@ +# Hestia Server automation + +[workflows](workflows/README.md) contains repository workflows. + +## CI checkout prerequisites + +All workflows use `runs-on: ces-control`; no shared Runner configuration is changed. +Configure repository or organization secrets `CODEX_BOT_SSH_KEY` (the dedicated +codex-bot private SSH key) and `GITEA_SSH_KNOWN_HOSTS` (the independently verified +known_hosts entry for `[gitea.entroia.com]:22222`). The bot needs read access to the +repository and its Aster submodule. Workflows use an isolated SSH command with +`-F /dev/null`, `IdentitiesOnly=yes`, an explicit key and port, strict host-key +checking, and temporary credential files removed after checkout. Keys are never +written into tracked files. Missing secrets fail checkout explicitly. + +The SSH submodule URL's repository owner is separate from the authenticated bot. +Local Git operations must use the same dedicated bot identity; configure the +checkout's isolated command in `GIT_SSH_COMMAND` for submodule initialization. + +Workflow definitions do not prove CI passed. CI/deployment state is not queried +automatically. Local runtime checks are not performed for Aster, Lyra or Hestia; +Tessera permits local native builds/window runs, with automated tests in CI. + +Hestia deployment uses a separate manual-only [deploy.yml](workflows/deploy.yml); ordinary pushes and pull requests only verify. diff --git a/.gitea/workflows/README.md b/.gitea/workflows/README.md new file mode 100644 index 0000000..15df70f --- /dev/null +++ b/.gitea/workflows/README.md @@ -0,0 +1,5 @@ +# Hestia Server workflows + +[ci.yml](ci.yml) verifies this project on main pushes, pull requests and manual dispatch. It runs on ces-control, with a 60-minute timeout. Checkout and secret requirements are in [the parent README](../README.md). + +[deploy.yml](deploy.yml) is main-only and manual-only. It repeats verification, builds the runtime image, then runs the existing deployment script. Production resource names, paths, lock and database mounts remain unchanged. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..f2d5133 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,48 @@ +name: Verify Hestia server + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + verify: + runs-on: ces-control + timeout-minutes: 60 + env: + DOCKER_BUILDKIT: "0" + steps: + - name: Checkout with codex-bot + env: + CODEX_BOT_SSH_KEY: ${{ secrets.CODEX_BOT_SSH_KEY }} + GITEA_SSH_KNOWN_HOSTS: ${{ secrets.GITEA_SSH_KNOWN_HOSTS }} + SOURCE_SHA: ${{ gitea.sha }} + SOURCE_REPOSITORY: ${{ gitea.repository }} + run: | + set -eu + test -n "$CODEX_BOT_SSH_KEY" + test -n "$GITEA_SSH_KNOWN_HOSTS" + case "$SOURCE_SHA" in *[!0-9a-f]*|'') exit 1;; esac + test "${#SOURCE_SHA}" -eq 40 + umask 077 + checkout_keys=$(mktemp -d) + trap 'rm -rf "$checkout_keys"' EXIT + printf '%s\n' "$CODEX_BOT_SSH_KEY" > "$checkout_keys/key" + printf '%s\n' "$GITEA_SSH_KNOWN_HOSTS" > "$checkout_keys/known_hosts" + unset CODEX_BOT_SSH_KEY GITEA_SSH_KNOWN_HOSTS + export GIT_SSH_COMMAND="ssh -F /dev/null -o IdentitiesOnly=yes -i $checkout_keys/key -p 22222 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$checkout_keys/known_hosts" + git init . + source_url="ssh://git@gitea.entroia.com:22222/$SOURCE_REPOSITORY.git" + if git remote get-url origin >/dev/null 2>&1; then + git remote set-url origin "$source_url" + else + git remote add origin "$source_url" + fi + git fetch --no-tags --depth=1 origin "$SOURCE_SHA" + git checkout --detach --force FETCH_HEAD + git submodule update --init --recursive + - name: Verify SQLite, PostgreSQL and HTTP host + env: + ASTER_RELEASE_SHA: ${{ gitea.sha }} + run: sh scripts/ci-ces-control.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..e4d4eae --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,55 @@ +name: Deploy Hestia manually + +on: + workflow_dispatch: + +concurrency: + group: endorphin-aster-vm-production + cancel-in-progress: false + +jobs: + verify-and-deploy: + if: gitea.ref == 'refs/heads/main' + runs-on: ces-control + timeout-minutes: 60 + env: + DOCKER_BUILDKIT: "0" + ASTER_RELEASE_SHA: ${{ gitea.sha }} + steps: + - name: Checkout with codex-bot + env: + CODEX_BOT_SSH_KEY: ${{ secrets.CODEX_BOT_SSH_KEY }} + GITEA_SSH_KNOWN_HOSTS: ${{ secrets.GITEA_SSH_KNOWN_HOSTS }} + SOURCE_SHA: ${{ gitea.sha }} + SOURCE_REPOSITORY: ${{ gitea.repository }} + run: | + set -eu + test -n "$CODEX_BOT_SSH_KEY" + test -n "$GITEA_SSH_KNOWN_HOSTS" + case "$SOURCE_SHA" in *[!0-9a-f]*|'') exit 1;; esac + test "${#SOURCE_SHA}" -eq 40 + umask 077 + checkout_keys=$(mktemp -d) + trap 'rm -rf "$checkout_keys"' EXIT + printf '%s\n' "$CODEX_BOT_SSH_KEY" > "$checkout_keys/key" + printf '%s\n' "$GITEA_SSH_KNOWN_HOSTS" > "$checkout_keys/known_hosts" + unset CODEX_BOT_SSH_KEY GITEA_SSH_KNOWN_HOSTS + export GIT_SSH_COMMAND="ssh -F /dev/null -o IdentitiesOnly=yes -i $checkout_keys/key -p 22222 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$checkout_keys/known_hosts" + git init . + source_url="ssh://git@gitea.entroia.com:22222/$SOURCE_REPOSITORY.git" + if git remote get-url origin >/dev/null 2>&1; then + git remote set-url origin "$source_url" + else + git remote add origin "$source_url" + fi + git fetch --no-tags --depth=1 origin "$SOURCE_SHA" + git checkout --detach --force FETCH_HEAD + git submodule update --init --recursive + - name: Verify SQLite, PostgreSQL and HTTP host + env: + ASTER_RELEASE_SHA: ${{ gitea.sha }} + run: sh scripts/ci-ces-control.sh + - name: Build verified production image + run: docker build --target runtime -t "endorphin-aster-vm:$ASTER_RELEASE_SHA" . + - name: Deploy with existing resource identifiers + run: sh scripts/deploy-ces-control.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cbc573 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target/ +/data/ +/work/ +/outputs/ +/ci-output/ +.env +*.db +*.db-shm +*.db-wal +__pycache__/ +AGENTS.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..aa04b20 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/aster"] + path = vendor/aster + url = ssh://git@gitea.entroia.com:22222/Zayden-Jung/endorphin-aster-vm.git diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..074195a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1888 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "admin" +version = "0.1.0" +dependencies = [ + "core", + "host", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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", + "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", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[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", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "core" +version = "0.1.0" +dependencies = [ + "objects", +] + +[[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 = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "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", + "libc", + "wasi", +] + +[[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", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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 = "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 = "host" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objects" +version = "0.1.0" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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 = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags", +] + +[[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 = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[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 = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +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 = "server" +version = "0.1.0" +dependencies = [ + "axum", + "core", + "host", + "serde", + "tokio", +] + +[[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 = "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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[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-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 2.0.119", +] + +[[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-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[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", + "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 = "storage" +version = "0.1.0" +dependencies = [ + "sqlx", + "tempfile", + "tokio", +] + +[[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 = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +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" + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[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 = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "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", +] + +[[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 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[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-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[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 = "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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[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.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +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-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0110437 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[workspace] +members = ["crates/server", "crates/admin", "crates/host", "crates/storage"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.91" +license = "MIT" + +[workspace.dependencies] +core = { path = "vendor/aster/crates/core" } +storage = { path = "crates/storage" } +host = { path = "crates/host" } +axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio", "tls-rustls-ring", "sqlite", "postgres"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "fs"] } + +[workspace.lints.rust] +unsafe_code = "forbid" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..879c085 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM rust:1.91.1-bookworm AS source +WORKDIR /build +ENV CARGO_BUILD_JOBS=1 +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates +COPY vendor ./vendor +COPY scripts ./scripts + +FROM source AS ci +RUN apt-get update && apt-get install -y --no-install-recommends python3 \ + && rm -rf /var/lib/apt/lists/* \ + && rustup component add rustfmt clippy +ENV CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 +CMD ["sh", "scripts/check.sh"] + +FROM source AS build +RUN cargo build --locked --release -p server -p admin + +FROM debian:bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --system --uid 10001 --create-home aster \ + && mkdir -p /data/host && chown -R aster:aster /data +COPY --from=build /build/target/release/server /usr/local/bin/server +COPY --from=build /build/target/release/cli /usr/local/bin/cli +USER 10001 +WORKDIR /data +ENV ASTER_LISTEN=0.0.0.0:8080 ASTER_STATE_DIR=/data/host +EXPOSE 8080 +HEALTHCHECK --interval=10s --timeout=6s --start-period=10s --retries=3 \ + CMD curl --fail --silent http://127.0.0.1:8080/health || exit 1 +CMD ["server"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a8dfea --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Aster VM contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac9f610 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Hestia Server + +Hestia 是独立 Endor HTTP 宿主。使用固定版本的 Aster 对象与 VM 库;当前仅提供 /health 和只读 /bootstrap,没有源码执行或认证 API。 +源码迁自 Aster 提交 51cd64e6884c1036564c22e5de582c083d6db142。 + +| 入口 | 职责 | +| --- | --- | +| [crates](crates/README.md) | HTTP server、兼容管理命令 admin、文件信息 host、显式数据库适配 storage | +| [vendor](vendor/README.md) | 固定 Aster 子模块 | +| [scripts](scripts/README.md) | 验证、HTTP/文件信息冒烟和生产部署 | +| [docs](docs/README.md) | 配置、数据保护和部署契约 | +| [.gitea](.gitea/README.md) | ces-control CI,以及独立手动 CD | +| Dockerfile | 远端 source/ci/build/runtime 阶段 | +| docker-compose*.yml | 兼容既有服务和数据库资源的部署定义 | +| Cargo.toml / Cargo.lock | Rust 1.91+ 宿主工作区和锁定依赖 | +| .env.example / .gitignore / .dockerignore / LICENSE | 示例、排除规则与许可 | + +Server 默认监听 127.0.0.1:8080,可由 ASTER_LISTEN 调整;宿主 JSON 存放于 ASTER_STATE_DIR。二进制 server 保留原名。admin 包产出兼容命令 cli,只提供 bootstrap、metadata put/get/import 和 help,供已有部署脚本使用;Endor run/eval/parse 属于独立 Lyra。 + +Host 和 Server 不连接数据库。Storage 仅在显式 DatabaseConfig 下连接,不创建应用表;真实 PostgreSQL 与 SQLite 测试均由 Hestia CI 承担。生产既有数据库、卷和目录完整保留。 + +main 推送和 PR 只验证;生产部署须显式手动触发 deploy.yml,且再次验证成功后才构建并发布。保留旧 ASTER_* 变量、Compose 项目/服务、镜像名和生产路径,不把拆仓当成数据迁移。见 [部署](docs/operations/deployment.md)。本地不构建/测试或运行 Docker,不主动查询 CI/部署。CI 所需 bot SSH secrets 见 [.gitea](.gitea/README.md)。 diff --git a/crates/README.md b/crates/README.md new file mode 100644 index 0000000..b9579bf --- /dev/null +++ b/crates/README.md @@ -0,0 +1,10 @@ +# Hestia 宿主包 + +| 目录 | 职责 | +| --- | --- | +| [server](server/README.md) | HTTP server,依赖固定 Aster core 和本地 host | +| [admin](admin/README.md) | 兼容 cli 二进制,只提供 bootstrap 与文件信息管理 | +| [host](host/README.md) | UTF-8 JSON 基础信息,不连接数据库 | +| [storage](storage/README.md) | 特殊类未来使用的显式 SQLite/PostgreSQL 适配 | + +Aster 代码位于 vendor/aster,不复制引擎实现。返回 [项目入口](../README.md)。 diff --git a/crates/admin/Cargo.toml b/crates/admin/Cargo.toml new file mode 100644 index 0000000..d901b50 --- /dev/null +++ b/crates/admin/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "admin" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[[bin]] +name = "cli" +path = "src/main.rs" + +[dependencies] +core.workspace = true +host.workspace = true + +[lints] +workspace = true diff --git a/crates/admin/README.md b/crates/admin/README.md new file mode 100644 index 0000000..a41078f --- /dev/null +++ b/crates/admin/README.md @@ -0,0 +1,3 @@ +# 兼容管理程序 + +Cargo 包名 admin,二进制 cli。仅提供 bootstrap、metadata put/get/import 和 help,保持既有部署脚本兼容。源码位于 [src](src/README.md),Cargo.toml 声明固定 Aster core 和本地 host 依赖。语言执行命令属于独立 Lyra。返回 [crates](../README.md)。 diff --git a/crates/admin/src/README.md b/crates/admin/src/README.md new file mode 100644 index 0000000..a18d32b --- /dev/null +++ b/crates/admin/src/README.md @@ -0,0 +1,3 @@ +# 管理命令实现 + +main.rs 负责对象引导与释放、宿主 JSON 读写和缺失键导入;不提供 run/eval/parse,也不连接数据库。运行验证由 scripts/smoke.py 覆盖。返回 [admin](../README.md)。 diff --git a/crates/admin/src/main.rs b/crates/admin/src/main.rs new file mode 100644 index 0000000..e586040 --- /dev/null +++ b/crates/admin/src/main.rs @@ -0,0 +1,68 @@ +use std::error::Error; +use std::process::ExitCode; + +use endor_core::{Value, Vm}; +use host::MetadataStore; + +const USAGE: &str = "Usage: cli [bootstrap | metadata put | metadata get | metadata import | help]"; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("cli: {error}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), Box> { + let args: Vec = std::env::args().skip(1).collect(); + match args + .iter() + .map(String::as_str) + .collect::>() + .as_slice() + { + [] | ["bootstrap"] => { + let mut vm = Vm::new()?; + let first = vm.new_object()?; + let second = vm.new_object()?; + vm.set_attr( + first, + "message", + Value::Text("Hello from EndorObject".into()), + )?; + println!("runtime=EndorObject type_system=disabled"); + println!( + "objects_distinct={} heap_objects={}", + first != second, + vm.object_count() + ); + vm.release(first)?; + vm.release(second)?; + println!("released heap_objects={}", vm.object_count()); + println!("bootstrap uses no database"); + } + ["help"] | ["--help"] | ["-h"] => println!("{USAGE}"), + ["metadata", "put", key, value] => { + MetadataStore::from_env()?.put(key, value)?; + println!("stored (file)"); + } + ["metadata", "get", key] => { + println!( + "{}", + MetadataStore::from_env()? + .get(key)? + .as_deref() + .unwrap_or("(missing)") + ); + } + ["metadata", "import"] => { + let count = MetadataStore::from_env()?.import_missing(std::io::stdin().lock())?; + println!("imported {count} missing entries (file)"); + } + _ => return Err(USAGE.into()), + } + Ok(()) +} diff --git a/crates/host/Cargo.toml b/crates/host/Cargo.toml new file mode 100644 index 0000000..db6bc5a --- /dev/null +++ b/crates/host/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "host" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +tempfile = "3" + +[lints] +workspace = true diff --git a/crates/host/README.md b/crates/host/README.md new file mode 100644 index 0000000..69e8a24 --- /dev/null +++ b/crates/host/README.md @@ -0,0 +1,11 @@ +# host + +宿主基础信息的文件层,与 Core 和数据库 Storage 分开。CLI/Server 使用它保存 host.version、deployment.release 和其他命名信息;不打开数据库,不保存 VM 堆。 + +| 内容 | 职责 | +| --- | --- | +| Cargo.toml | host 包、Serde/JSON 依赖及远端检查依赖 | +| [src](src/README.md) | MetadataStore、UTF-8 JSON 文件与迁移导入 | +| [tests](tests/README.md) | 持久读取、替换、并发、坏记录、路径边界和幂等导入 | + +ASTER_STATE_DIR 指定信息目录;默认 data/host,容器使用 /data/host。协议、生命周期及旧数据库一次性迁移见 [宿主信息](../../docs/operations/host-information.md)。返回 [工作区](../README.md)。 diff --git a/crates/host/src/README.md b/crates/host/src/README.md new file mode 100644 index 0000000..4fb6fd8 --- /dev/null +++ b/crates/host/src/README.md @@ -0,0 +1,8 @@ +# Host 源码导航 + +| 文件 | 职责 | +| --- | --- | +| [lib.rs](lib.rs) | 导出 MetadataStore,明确文件层不依赖 VM 或 SQL | +| [metadata.rs](metadata.rs) | 目录配置、键校验、逐键 JSON 读取/同步写入、原子发布、仅导入缺失项 | + +文件数据以 UTF-8 JSON 保存,操作系统路径用 PathBuf;坏记录不自动覆盖。每个键独立发布,不提供多键事务。旧库导入不覆盖新文件,规则见 [宿主信息](../../../docs/operations/host-information.md)。返回 [host](../README.md)。 diff --git a/crates/host/src/lib.rs b/crates/host/src/lib.rs new file mode 100644 index 0000000..2758209 --- /dev/null +++ b/crates/host/src/lib.rs @@ -0,0 +1,4 @@ +//! Host information stored in UTF-8 JSON files, independently of the VM and SQL. +mod metadata; + +pub use metadata::MetadataStore; diff --git a/crates/host/src/metadata.rs b/crates/host/src/metadata.rs new file mode 100644 index 0000000..e5c6ca5 --- /dev/null +++ b/crates/host/src/metadata.rs @@ -0,0 +1,172 @@ +use std::fs::{self, OpenOptions}; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use serde::{Deserialize, Serialize}; + +static NEXT_TEMP: AtomicU64 = AtomicU64::new(0); +const FORMAT_VERSION: u32 = 1; + +#[derive(Deserialize)] +struct LegacyRecord { + key: String, + value: String, +} + +#[derive(Serialize, Deserialize)] +struct Record { + format_version: u32, + key: String, + value: String, +} + +/// One independently replaceable JSON file per ASCII metadata key. Clones +/// share a directory, not a cached map; reads always observe the file system. +#[derive(Debug, Clone)] +pub struct MetadataStore { + directory: PathBuf, +} + +impl MetadataStore { + /// Does not create the directory until the first write. + pub fn new(directory: impl Into) -> io::Result { + let directory = directory.into(); + if directory.as_os_str().is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "metadata directory is empty", + )); + } + Ok(Self { directory }) + } + + /// Preserve OS paths without requiring the environment value to be UTF-8. + pub fn from_env() -> io::Result { + Self::new( + std::env::var_os("ASTER_STATE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("data/host")), + ) + } + + pub fn directory(&self) -> &Path { + &self.directory + } + + pub fn get(&self, key: &str) -> io::Result> { + let path = self.path(key)?; + let bytes = match fs::read(path) { + Ok(bytes) => bytes, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(error), + }; + let record: Record = serde_json::from_slice(&bytes) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + if record.format_version != FORMAT_VERSION || record.key != key { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "metadata key or format version mismatch", + )); + } + Ok(Some(record.value)) + } + + /// Writes a new sibling file, syncs it, then publishes it with rename. + /// Different keys do not share a read/modify/write map. For the same key, + /// the last successful rename wins. This is not a multi-key transaction. + pub fn put(&self, key: &str, value: &str) -> io::Result<()> { + self.store(key, value, true).map(|_| ()) + } + + /// One-time migration input: a JSON array of key/value string records. + /// Validate all keys first, and never overwrite an existing file record. + pub fn import_missing(&self, reader: impl Read) -> io::Result { + let records: Vec = serde_json::from_reader(reader) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + for record in &records { + self.path(&record.key)?; + } + let mut imported = 0; + for record in records { + if self.store(&record.key, &record.value, false)? { + imported += 1; + } else if self.get(&record.key)?.is_none() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "existing metadata disappeared during import", + )); + } + } + Ok(imported) + } + + fn store(&self, key: &str, value: &str, replace: bool) -> io::Result { + let destination = self.path(key)?; + let mut contents = serde_json::to_vec_pretty(&Record { + format_version: FORMAT_VERSION, + key: key.into(), + value: value.into(), + }) + .map_err(io::Error::other)?; + contents.push(b'\n'); + fs::create_dir_all(&self.directory)?; + for _ in 0..32 { + let sequence = NEXT_TEMP.fetch_add(1, Ordering::Relaxed); + let temporary = self + .directory + .join(format!(".{key}.{}.{sequence}.tmp", std::process::id())); + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = match options.open(&temporary) { + Ok(file) => file, + Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error), + }; + let result = (|| { + file.write_all(&contents)?; + file.sync_all()?; + drop(file); + if replace { + fs::rename(&temporary, &destination)?; + Ok(true) + } else { + // Atomic create-if-absent: a concurrent writer's record + // cannot be replaced by the legacy import. + match fs::hard_link(&temporary, &destination) { + Ok(()) => Ok(true), + Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false), + Err(error) => Err(error), + } + } + })(); + let _ = fs::remove_file(&temporary); + return result; + } + Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "cannot reserve a metadata temporary file", + )) + } + + fn path(&self, key: &str) -> io::Result { + if key.is_empty() + || key.len() > 128 + || !key.as_bytes()[0].is_ascii_alphanumeric() + || !key + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) + { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "metadata keys must start with an ASCII letter/digit and contain at most 128 ASCII letters, digits, dots, underscores or hyphens", + )); + } + Ok(self.directory.join(format!("{key}.json"))) + } +} diff --git a/crates/host/tests/README.md b/crates/host/tests/README.md new file mode 100644 index 0000000..09b8297 --- /dev/null +++ b/crates/host/tests/README.md @@ -0,0 +1,5 @@ +# Host 文件信息检查 + +[metadata.rs](metadata.rs) 检查缺失读取不建目录、日文/中文/emoji 值和日文路径、重开与同键替换、非法键不越界、坏 JSON/版本/键不匹配不改原文件、并发替换可见完整记录,以及旧记录幂等导入和保留更新值。 + +检查使用临时目录,仅在指定远端环境运行。本地不执行文件行为测试;当前是否通过应以对应提交的 CI 为准。命令见 [验证](../../../vendor/aster/docs/operations/validation.md),返回 [host](../README.md)。 diff --git a/crates/host/tests/metadata.rs b/crates/host/tests/metadata.rs new file mode 100644 index 0000000..30a3be8 --- /dev/null +++ b/crates/host/tests/metadata.rs @@ -0,0 +1,131 @@ +use host::MetadataStore; + +#[test] +fn missing_reads_do_not_create_state() { + let temporary = tempfile::tempdir().unwrap(); + let directory = temporary.path().join("state"); + let store = MetadataStore::new(&directory).unwrap(); + assert_eq!(store.get("missing").unwrap(), None); + assert!(!directory.exists()); +} + +#[test] +fn utf8_values_survive_reopening_and_same_key_replacement() { + let temporary = tempfile::tempdir().unwrap(); + let directory = temporary.path().join("日本語"); + let store = MetadataStore::new(&directory).unwrap(); + store.put("host.version", "0.1.0").unwrap(); + store.put("project.note", "日本語・中文・😀").unwrap(); + let reopened = MetadataStore::new(&directory).unwrap(); + assert_eq!( + reopened.get("project.note").unwrap().as_deref(), + Some("日本語・中文・😀") + ); + reopened.put("host.version", "0.2.0").unwrap(); + assert_eq!(store.get("host.version").unwrap().as_deref(), Some("0.2.0")); + assert_eq!( + store.get("project.note").unwrap().as_deref(), + Some("日本語・中文・😀") + ); + let record: serde_json::Value = + serde_json::from_slice(&std::fs::read(directory.join("project.note.json")).unwrap()) + .unwrap(); + assert_eq!(record["format_version"], 1); + assert_eq!(record["key"], "project.note"); +} + +#[test] +fn keys_cannot_escape_the_state_directory() { + let temporary = tempfile::tempdir().unwrap(); + let directory = temporary.path().join("state"); + let store = MetadataStore::new(&directory).unwrap(); + for key in [ + "", + "../outside", + "/outside", + "nested/key", + "nested\\key", + ".hidden", + "日本語", + ] { + assert!(store.put(key, "invalid").is_err(), "{key}"); + assert!(store.get(key).is_err(), "{key}"); + } + assert!(store.put(&"x".repeat(129), "invalid").is_err()); + assert!(!directory.exists()); +} + +#[test] +fn corrupt_or_mismatched_records_fail_without_being_rewritten() { + let temporary = tempfile::tempdir().unwrap(); + let store = MetadataStore::new(temporary.path()).unwrap(); + let path = temporary.path().join("host.version.json"); + for bytes in [ + b"not-json".as_slice(), + b"\xff".as_slice(), + br#"{"format_version":2,"key":"host.version","value":"old"}"#.as_slice(), + br#"{"format_version":1,"key":"wrong","value":"old"}"#.as_slice(), + ] { + std::fs::write(&path, bytes).unwrap(); + assert!(store.get("host.version").is_err()); + assert_eq!(std::fs::read(&path).unwrap(), bytes); + } +} + +#[test] +fn concurrent_replacements_expose_complete_records_and_preserve_other_keys() { + let temporary = tempfile::tempdir().unwrap(); + let store = MetadataStore::new(temporary.path()).unwrap(); + store.put("independent", "keep").unwrap(); + let a = "a".repeat(8192); + let b = "b".repeat(8192); + store.put("shared", &a).unwrap(); + std::thread::scope(|scope| { + for value in [&a, &b] { + let store = &store; + scope.spawn(move || { + for _ in 0..8 { + store.put("shared", value).unwrap(); + } + }); + } + for _ in 0..64 { + let value = store.get("shared").unwrap().unwrap(); + assert!(value == a || value == b); + } + }); + assert_eq!(store.get("independent").unwrap().as_deref(), Some("keep")); + assert!(std::fs::read_dir(temporary.path()).unwrap().all(|entry| { + !entry + .unwrap() + .file_name() + .to_string_lossy() + .ends_with(".tmp") + })); +} + +#[test] +fn legacy_import_is_repeatable_and_preserves_newer_file_values() { + let temporary = tempfile::tempdir().unwrap(); + let store = MetadataStore::new(temporary.path()).unwrap(); + store.put("host.version", "new").unwrap(); + let input = + br#"[{"key":"host.version","value":"old"},{"key":"project.note","value":"legacy"}]"#; + assert_eq!(store.import_missing(input.as_slice()).unwrap(), 1); + assert_eq!(store.import_missing(input.as_slice()).unwrap(), 0); + assert_eq!(store.get("host.version").unwrap().as_deref(), Some("new")); + assert_eq!( + store.get("project.note").unwrap().as_deref(), + Some("legacy") + ); +} + +#[test] +fn invalid_import_keys_are_rejected_before_any_record_is_written() { + let temporary = tempfile::tempdir().unwrap(); + let directory = temporary.path().join("state"); + let store = MetadataStore::new(&directory).unwrap(); + let input = br#"[{"key":"valid","value":"keep"},{"key":"../outside","value":"invalid"}]"#; + assert!(store.import_missing(input.as_slice()).is_err()); + assert!(!directory.exists()); +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml new file mode 100644 index 0000000..749cb40 --- /dev/null +++ b/crates/server/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "server" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +core.workspace = true +host.workspace = true +axum.workspace = true +serde.workspace = true +tokio.workspace = true + +[lints] +workspace = true diff --git a/crates/server/README.md b/crates/server/README.md new file mode 100644 index 0000000..e61c6e4 --- /dev/null +++ b/crates/server/README.md @@ -0,0 +1,10 @@ +# server + +最小常驻 HTTP 宿主,提供存活检查和只读 VM 信息。通过 Arc 共享 Vm,启动不读取数据库配置、不连接或写入数据库;通过 host 包把 host.version 写入文件。特殊类的数据库能力尚未接入 HTTP 宿主。 + +| 内容 | 职责 | +| --- | --- | +| Cargo.toml | core/host、Axum、Tokio、Serde 依赖;无 storage 依赖 | +| [src](src/README.md) | AppState、启动、路由、信号与退出 | + +HTTP 返回内容见 [运行指南](../../vendor/aster/docs/operations/README.md),监听变量见 [配置](../../docs/operations/configuration.md),预置 PostgreSQL 资源见 [部署](../../docs/operations/deployment.md)。没有语言执行接口或可变 VM 请求上下文。返回 [工作区](../README.md)。 diff --git a/crates/server/src/README.md b/crates/server/src/README.md new file mode 100644 index 0000000..573c96f --- /dev/null +++ b/crates/server/src/README.md @@ -0,0 +1,13 @@ +# Server 入口 + +[main.rs](main.rs) 包含所有宿主启动代码: + +| 部分 | 职责 | +| --- | --- | +| AppState | 共享只读 Arc | +| main/run | 创建空 VM、绑定地址、设置退出信号、写文件 host.version、组装路由;不创建数据库连接 | +| health | 返回 200 和 status=ok,仅检查 HTTP 宿主可响应 | +| bootstrap | 返回对象模型、type_system=false 和堆对象数量,不分配对象 | +| shutdown_signal | Ctrl-C;Unix 上同时接受 SIGTERM,触发 Axum 优雅退出 | + +/health 不表示 PostgreSQL、特殊类查询或执行脚本正常;数据库检查应属于实际消费连接的对象。宿主独立于数据库的行为由远端 smoke.py 验证,见 [验证](../../../vendor/aster/docs/operations/validation.md)。改变端点同步 [运行指南](../../../vendor/aster/docs/operations/README.md) 和 [scripts](../../../scripts/README.md)。返回 [server](../README.md)。 diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs new file mode 100644 index 0000000..76d0612 --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,77 @@ +use std::error::Error; +use std::future::Future; +use std::process::ExitCode; +use std::sync::Arc; + +use axum::{Json, Router, extract::State, routing::get}; +use endor_core::Vm; +use host::MetadataStore; +use serde::Serialize; + +#[derive(Clone)] +struct AppState { + vm: Arc, +} + +#[derive(Serialize)] +struct Health { + status: &'static str, +} + +#[derive(Serialize)] +struct BootstrapInfo { + object_model: &'static str, + type_system: bool, + heap_objects: usize, +} + +#[tokio::main] +async fn main() -> ExitCode { + match run().await { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("server: {error}"); + ExitCode::FAILURE + } + } +} + +async fn run() -> Result<(), Box> { + let vm = Arc::new(Vm::new()?); + let address = std::env::var("ASTER_LISTEN").unwrap_or_else(|_| "127.0.0.1:8080".into()); + let listener = tokio::net::TcpListener::bind(&address).await?; + let shutdown = shutdown_signal()?; + MetadataStore::from_env()?.put("host.version", env!("CARGO_PKG_VERSION"))?; + let app = Router::new() + .route("/health", get(health)) + .route("/bootstrap", get(bootstrap)) + .with_state(AppState { vm }); + println!("Endor host listening on {}", listener.local_addr()?); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown) + .await?; + Ok(()) +} + +async fn health() -> Json { + Json(Health { status: "ok" }) +} + +async fn bootstrap(State(state): State) -> Json { + Json(BootstrapInfo { + object_model: "EndorObject", + type_system: false, + heap_objects: state.vm.object_count(), + }) +} + +fn shutdown_signal() -> std::io::Result> { + #[cfg(unix)] + let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; + Ok(async move { + #[cfg(unix)] + tokio::select! { _ = tokio::signal::ctrl_c() => {}, _ = terminate.recv() => {} } + #[cfg(not(unix))] + let _ = tokio::signal::ctrl_c().await; + }) +} diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml new file mode 100644 index 0000000..d9de0b9 --- /dev/null +++ b/crates/storage/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "storage" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +sqlx.workspace = true +tokio.workspace = true + +[dev-dependencies] +tempfile = "3" + +[lints] +workspace = true diff --git a/crates/storage/README.md b/crates/storage/README.md new file mode 100644 index 0000000..951b2e5 --- /dev/null +++ b/crates/storage/README.md @@ -0,0 +1,11 @@ +# storage + +按需数据库连接适配层,供将来需要数据库的特殊类或宿主适配器使用。依赖 SQLx/Tokio,不依赖 VM 内核;普通对象、CLI 和 Server 不连接这层。 + +| 内容 | 入口 | +| --- | --- | +| Cargo.toml | SQLx、Tokio 与远端检查依赖 | +| [src](src/README.md) | 显式 DatabaseConfig、连接池与关闭 | +| [tests](tests/README.md) | 连接不建表、调用方 SQL、隔离/持久化、真实 PostgreSQL | + +没有默认数据库、自动环境变量读取、基础信息表或通用元数据 put/get。特殊类的名称、布局与查询接口尚未定义;当前只保留连接能力。设计边界见 [数据库对象](../../docs/runtime/database-objects.md),参数见 [配置](../../docs/operations/configuration.md)。返回 [工作区](../README.md)。 diff --git a/crates/storage/src/README.md b/crates/storage/src/README.md new file mode 100644 index 0000000..124bb60 --- /dev/null +++ b/crates/storage/src/README.md @@ -0,0 +1,7 @@ +# 数据库连接源码导航 + +[lib.rs](lib.rs) 是唯一实现文件:DatabaseConfig 显式选择 SQLite 文件/内存或 PostgreSQL;DatabaseConnection 提供 connect、sqlite_pool/postgres_pool、backend_name、ping 和 close;DatabaseError 对公开错误脱敏。 + +connect 只建立连接池,不执行应用 DDL,不保存基础信息。没有 schema.sql、初始化表、put/get 或 from_env。业务表和查询由未来的数据库特殊类决定;本层不替它定义数据模型。 + +克隆共享连接池,close 会关闭所有克隆;具体生命周期及限制见 [数据库对象](../../../docs/runtime/database-objects.md)。配置变动同步 [配置](../../../docs/operations/configuration.md) 和 [tests](../tests/README.md)。返回 [storage](../README.md)。 diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs new file mode 100644 index 0000000..774040c --- /dev/null +++ b/crates/storage/src/lib.rs @@ -0,0 +1,181 @@ +//! Explicit database connections for database-backed objects. +//! Connecting never creates application tables or stores VM/host metadata. +use std::fmt; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; +use sqlx::{PgPool, SqlitePool}; + +/// An explicit PostgreSQL configuration never falls back to SQLite. +#[derive(Clone)] +pub enum DatabaseConfig { + SqliteFile(PathBuf), + SqliteMemory, + Postgres(String), +} + +impl fmt::Debug for DatabaseConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SqliteFile(path) => f.debug_tuple("SqliteFile").field(path).finish(), + Self::SqliteMemory => f.write_str("SqliteMemory"), + Self::Postgres(_) => f.write_str("Postgres([REDACTED])"), + } + } +} + +#[derive(Debug)] +pub enum DatabaseError { + Config(&'static str), + Directory(std::io::Error), + Database(sqlx::Error), +} + +impl fmt::Display for DatabaseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Config(message) => f.write_str(message), + Self::Directory(_) => f.write_str("cannot create SQLite parent directory"), + // CLI/HTTP logs must never print a connection URL or credentials. + Self::Database(_) => { + f.write_str("database operation failed; no fallback was attempted") + } + } + } +} + +impl std::error::Error for DatabaseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Config(_) => None, + Self::Directory(error) => Some(error), + Self::Database(error) => Some(error), + } + } +} + +impl From for DatabaseError { + fn from(error: sqlx::Error) -> Self { + Self::Database(error) + } +} + +#[derive(Clone)] +enum Backend { + Sqlite(SqlitePool), + Postgres(PgPool), +} + +/// Created only on explicit request by an object or host adapter. Clones share +/// a pool; close() closes it for all users. There is no automatic schema or data. +/// In-memory SQLite uses one connection to preserve its database lifetime. +#[derive(Clone)] +pub struct DatabaseConnection { + backend: Backend, +} + +impl DatabaseConnection { + /// Connects eagerly to the explicit backend without executing application DDL. + pub async fn connect(config: &DatabaseConfig) -> Result { + let backend = match config { + DatabaseConfig::Postgres(url) => { + if !(url.starts_with("postgres://") || url.starts_with("postgresql://")) { + return Err(DatabaseError::Config( + "expected a PostgreSQL connection URL", + )); + } + let options = PgConnectOptions::from_str(url) + .map_err(|_| DatabaseError::Config("invalid PostgreSQL connection URL"))?; + Backend::Postgres( + PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(5)) + .connect_with(options) + .await?, + ) + } + DatabaseConfig::SqliteFile(path) => { + if path.as_os_str().is_empty() || path == std::path::Path::new(":memory:") { + return Err(DatabaseError::Config( + "expected a nonempty file path; use SqliteMemory for :memory:", + )); + } + if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) { + tokio::fs::create_dir_all(parent) + .await + .map_err(DatabaseError::Directory)?; + } + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .busy_timeout(Duration::from_secs(5)); + Backend::Sqlite( + SqlitePoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(5)) + .connect_with(options) + .await?, + ) + } + DatabaseConfig::SqliteMemory => { + let options = SqliteConnectOptions::new().in_memory(true); + Backend::Sqlite( + SqlitePoolOptions::new() + .max_connections(1) + .min_connections(1) + .idle_timeout(None) + .max_lifetime(None) + .connect_with(options) + .await?, + ) + } + }; + Ok(Self { backend }) + } + + /// Backend-specific access for the database-backed object implementation. + pub fn sqlite_pool(&self) -> Option<&SqlitePool> { + match &self.backend { + Backend::Sqlite(pool) => Some(pool), + Backend::Postgres(_) => None, + } + } + + pub fn postgres_pool(&self) -> Option<&PgPool> { + match &self.backend { + Backend::Postgres(pool) => Some(pool), + Backend::Sqlite(_) => None, + } + } + + pub fn backend_name(&self) -> &'static str { + match &self.backend { + Backend::Sqlite(_) => "sqlite", + Backend::Postgres(_) => "postgresql", + } + } + + pub async fn ping(&self) -> Result<(), DatabaseError> { + match &self.backend { + Backend::Sqlite(pool) => { + sqlx::query("SELECT 1").execute(pool).await?; + } + Backend::Postgres(pool) => { + sqlx::query("SELECT 1").execute(pool).await?; + } + } + Ok(()) + } + + /// Explicitly closes the shared pool, including all DatabaseConnection clones. + pub async fn close(&self) { + match &self.backend { + Backend::Sqlite(pool) => pool.close().await, + Backend::Postgres(pool) => pool.close().await, + } + } +} diff --git a/crates/storage/tests/README.md b/crates/storage/tests/README.md new file mode 100644 index 0000000..41f7647 --- /dev/null +++ b/crates/storage/tests/README.md @@ -0,0 +1,8 @@ +# 数据库连接检查 + +| 文件 | 覆盖与前提 | +| --- | --- | +| [storage.rs](storage.rs) | 显式错误配置、凭据脱敏、SQLite 连接后无应用表、克隆共享与独立内存隔离、调用方自建数据跨文件重连、目录错误、PostgreSQL 失败不降级 | +| [postgres.rs](postgres.rs) | 真实连接后 public 无表;测试在事务中创建临时 class_rows、参数绑定和读取,提交自动删除临时表,再连接;默认 ignore,需全新专用 ASTER_TEST_POSTGRES_URL | + +所有业务表都由测试自己创建,连接层不得自动初始化基础信息表。临时 SQLite 文件和 PostgreSQL 测试库由远端 CI 隔离,不使用生产数据。检查命令见 [验证](../../../vendor/aster/docs/operations/validation.md),原生连接能力边界见 [数据库对象](../../../docs/runtime/database-objects.md)。本地不运行测试或下载依赖。返回 [storage](../README.md)。 diff --git a/crates/storage/tests/postgres.rs b/crates/storage/tests/postgres.rs new file mode 100644 index 0000000..65135aa --- /dev/null +++ b/crates/storage/tests/postgres.rs @@ -0,0 +1,53 @@ +use storage::{DatabaseConfig, DatabaseConnection}; + +/// Run against a fresh disposable database, never a production or shared schema. +#[tokio::test] +#[ignore = "requires ASTER_TEST_POSTGRES_URL pointing to a fresh disposable PostgreSQL database"] +async fn postgres_connects_without_schema_and_executes_caller_owned_sql() { + let url = std::env::var("ASTER_TEST_POSTGRES_URL") + .expect("set ASTER_TEST_POSTGRES_URL to a fresh disposable PostgreSQL database"); + let config = DatabaseConfig::Postgres(url); + let database = DatabaseConnection::connect(&config).await.unwrap(); + assert_eq!(database.backend_name(), "postgresql"); + assert!(database.sqlite_pool().is_none()); + database.ping().await.unwrap(); + let pool = database.postgres_pool().unwrap(); + let tables: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'", + ) + .fetch_one(pool) + .await + .unwrap(); + assert_eq!(tables, 0, "connecting must not create application tables"); + let mut transaction = pool.begin().await.unwrap(); + sqlx::query( + "CREATE TEMP TABLE class_rows (id INTEGER PRIMARY KEY, value TEXT NOT NULL) ON COMMIT DROP", + ) + .execute(&mut *transaction) + .await + .unwrap(); + sqlx::query("INSERT INTO class_rows (id, value) VALUES ($1, $2)") + .bind(1_i64) + .bind("你好 ' ; $1") + .execute(&mut *transaction) + .await + .unwrap(); + let value: String = sqlx::query_scalar("SELECT value FROM class_rows WHERE id = $1") + .bind(1_i64) + .fetch_one(&mut *transaction) + .await + .unwrap(); + assert_eq!(value, "你好 ' ; $1"); + transaction.commit().await.unwrap(); + database.close().await; + let reopened = DatabaseConnection::connect(&config).await.unwrap(); + reopened.ping().await.unwrap(); + let tables: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'", + ) + .fetch_one(reopened.postgres_pool().unwrap()) + .await + .unwrap(); + assert_eq!(tables, 0); + reopened.close().await; +} diff --git a/crates/storage/tests/storage.rs b/crates/storage/tests/storage.rs new file mode 100644 index 0000000..151e091 --- /dev/null +++ b/crates/storage/tests/storage.rs @@ -0,0 +1,130 @@ +use storage::{DatabaseConfig, DatabaseConnection}; + +#[test] +fn postgres_configuration_debug_redacts_credentials() { + let config = DatabaseConfig::Postgres("postgres://user:secret@localhost/db".into()); + assert!(!format!("{config:?}").contains("secret")); +} + +#[tokio::test] +async fn explicit_invalid_backends_fail_without_fallback() { + for config in [ + DatabaseConfig::Postgres(String::new()), + DatabaseConfig::Postgres("mysql://localhost/db".into()), + DatabaseConfig::SqliteFile("".into()), + DatabaseConfig::SqliteFile(":memory:".into()), + ] { + assert!(DatabaseConnection::connect(&config).await.is_err()); + } +} + +#[tokio::test] +async fn memory_connections_start_without_tables_and_clones_share_the_pool() { + let database = DatabaseConnection::connect(&DatabaseConfig::SqliteMemory) + .await + .unwrap(); + let other = DatabaseConnection::connect(&DatabaseConfig::SqliteMemory) + .await + .unwrap(); + assert!(database.postgres_pool().is_none()); + let pool = database.sqlite_pool().unwrap(); + let tables: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'") + .fetch_one(pool) + .await + .unwrap(); + assert_eq!(tables, 0, "connecting must not create application tables"); + // This schema belongs to the test's simulated database-backed object. + sqlx::query("CREATE TABLE class_rows (id INTEGER PRIMARY KEY, value TEXT NOT NULL)") + .execute(pool) + .await + .unwrap(); + sqlx::query("INSERT INTO class_rows (id, value) VALUES (?, ?)") + .bind(1_i64) + .bind("你好 ' ? $1 ;") + .execute(pool) + .await + .unwrap(); + let cloned = database.clone(); + let value: String = sqlx::query_scalar("SELECT value FROM class_rows WHERE id = ?") + .bind(1_i64) + .fetch_one(cloned.sqlite_pool().unwrap()) + .await + .unwrap(); + assert_eq!(value, "你好 ' ? $1 ;"); + let other_tables: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'") + .fetch_one(other.sqlite_pool().unwrap()) + .await + .unwrap(); + assert_eq!(other_tables, 0); + database.close().await; + assert!(cloned.ping().await.is_err()); + other.close().await; +} + +#[tokio::test] +async fn explicit_sqlite_file_preserves_only_caller_created_data_across_reconnects() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("nested/data/class.db"); + let config = DatabaseConfig::SqliteFile(path.clone()); + let database = DatabaseConnection::connect(&config).await.unwrap(); + let pool = database.sqlite_pool().unwrap(); + let tables: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'") + .fetch_one(pool) + .await + .unwrap(); + assert_eq!(tables, 0); + sqlx::query("CREATE TABLE class_rows (id INTEGER PRIMARY KEY, value TEXT NOT NULL)") + .execute(pool) + .await + .unwrap(); + sqlx::query("INSERT INTO class_rows (id, value) VALUES (?, ?)") + .bind(1_i64) + .bind("caller-owned data") + .execute(pool) + .await + .unwrap(); + database.close().await; + assert!(path.is_file()); + let reopened = DatabaseConnection::connect(&config).await.unwrap(); + let value: String = sqlx::query_scalar("SELECT value FROM class_rows WHERE id = ?") + .bind(1_i64) + .fetch_one(reopened.sqlite_pool().unwrap()) + .await + .unwrap(); + assert_eq!(value, "caller-owned data"); + let tables: Vec = + sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'table'") + .fetch_all(reopened.sqlite_pool().unwrap()) + .await + .unwrap(); + assert_eq!(tables, vec!["class_rows"]); + reopened.close().await; +} + +#[tokio::test] +async fn invalid_sqlite_directory_reports_error() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("file"); + std::fs::write(&file, "occupied").unwrap(); + assert!( + DatabaseConnection::connect(&DatabaseConfig::SqliteFile(file.join("class.db"))) + .await + .is_err() + ); +} + +#[tokio::test] +async fn postgres_connection_failure_is_an_error() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!( + "postgres://aster:secret@{}/aster?sslmode=disable", + listener.local_addr().unwrap() + ); + let error = match DatabaseConnection::connect(&DatabaseConfig::Postgres(url)).await { + Ok(_) => panic!("unreachable PostgreSQL must fail"), + Err(error) => error, + }; + assert!(!error.to_string().contains("secret")); + assert!(error.to_string().contains("no fallback")); +} diff --git a/docker-compose.ces-control.yml b/docker-compose.ces-control.yml new file mode 100644 index 0000000..c881c8d --- /dev/null +++ b/docker-compose.ces-control.yml @@ -0,0 +1,41 @@ +name: endorphin-aster-vm + +services: + aster: + image: ${ASTER_IMAGE:?set ASTER_IMAGE to the verified release image} + init: true + ports: + - "127.0.0.1:${ASTER_PORT:-18081}:8080" + environment: + ASTER_LISTEN: 0.0.0.0:8080 + ASTER_STATE_DIR: /data/host + volumes: + - /var/lib/endorphin-aster-vm/host:/data/host + restart: unless-stopped + stop_grace_period: 20s + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" + + postgres: + image: postgres:17-bookworm + environment: + POSTGRES_DB: aster + POSTGRES_USER: aster + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + volumes: + - /var/lib/endorphin-aster-vm/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U aster -d aster"] + interval: 5s + timeout: 3s + retries: 20 + restart: unless-stopped + stop_grace_period: 30s + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" diff --git a/docker-compose.postgres.yml b/docker-compose.postgres.yml new file mode 100644 index 0000000..4d3acc8 --- /dev/null +++ b/docker-compose.postgres.yml @@ -0,0 +1,31 @@ +# Standalone alternative: docker compose -f docker-compose.postgres.yml up --build -d +services: + aster: + build: . + ports: + - "127.0.0.1:${ASTER_PORT:-8080}:8080" + environment: + ASTER_LISTEN: 0.0.0.0:8080 + ASTER_STATE_DIR: /data/host + volumes: + - aster-host:/data/host + restart: unless-stopped + + postgres: + image: postgres:17-bookworm + environment: + POSTGRES_DB: aster + POSTGRES_USER: aster + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + volumes: + - aster-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U aster -d aster"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + +volumes: + aster-host: + aster-postgres: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2f84f5c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + aster: + build: . + ports: + - "127.0.0.1:${ASTER_PORT:-8080}:8080" + environment: + ASTER_LISTEN: 0.0.0.0:8080 + ASTER_STATE_DIR: /data/host + volumes: + - aster-sqlite:/data + restart: unless-stopped + +volumes: + aster-sqlite: diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..77f16a5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# Hestia 文档 + +| 入口 | 内容 | +| --- | --- | +| [operations](operations/README.md) | 运行、配置、部署、验证和宿主信息 | +| [runtime](runtime/README.md) | 显式数据库适配边界 | + +语言对象、字节码和编译说明以固定 Aster 子模块文档为准;命令行源码执行属于 Lyra。返回 [项目入口](../README.md)。 diff --git a/docs/operations/README.md b/docs/operations/README.md new file mode 100644 index 0000000..7214c42 --- /dev/null +++ b/docs/operations/README.md @@ -0,0 +1,11 @@ +# Hestia 运维 + +| 文件 | 内容 | +| --- | --- | +| README.md | 本目录入口 | +| [configuration.md](configuration.md) | 兼容环境变量和显式配置 | +| [host-information.md](host-information.md) | JSON 基础信息、读写与只读旧库导入 | +| [deployment.md](deployment.md) | 手动部署及生产数据保护 | +| [validation.md](validation.md) | 远端 CI 范围 | + +HTTP binary server 提供 /health 与只读 /bootstrap;admin 包的 cli 提供对象引导和文件信息管理。两者不使用数据库;没有语言执行 HTTP API。返回 [文档索引](../README.md)。 diff --git a/docs/operations/configuration.md b/docs/operations/configuration.md new file mode 100644 index 0000000..dd5258d --- /dev/null +++ b/docs/operations/configuration.md @@ -0,0 +1,73 @@ +# 配置、UTF-8 与进程边界 + +## 当前选择 + +当前先明确少量稳定边界,不引入 PyPreConfig/PyConfig 式两阶段初始化,也不新增 VM 配置文件、环境变量前缀或配置框架。Vm::new 从空堆启动;监听与网络由宿主处理,数据库连接仅由真正消费它的对象显式请求。 + +| 配置层 | 当前入口 | 责任 | +| --- | --- | --- | +| 内核 | Vm::new、显式创建对象/函数;call_with_limits/execute_with_limits | 可预测的运行时语义,不读取环境变量、修改 locale、注册信号或加载 .env | +| 宿主 | Server 启动代码与 host::MetadataStore | 读取监听/文件信息目录;基础信息存 JSON 文件,不读取数据库配置 | +| 数据库对象适配 | DatabaseConnection::connect(&DatabaseConfig) | 显式选择后端后连接,不预设应用表;特殊类尚未接入 | +| 部署 | Compose 与 ces-control 脚本 | 端口映射、数据库密码、持久卷和镜像版本 | +| 构建/CI | Cargo.toml、Cargo.lock、Dockerfile、工作流 | 工具链、依赖锁定与验证环境;不作为语言运行语义配置 | + +内核不直接依赖 HTTP、SQLx 或操作系统 locale 接口。这个隔离来自现有代码与分层,不是已经实现的独立安全沙箱。 + +## 为什么不照搬 CPython 的 locale 初始化 + +CPython 的 PyPreConfig 支持选择分配器、UTF-8 模式以及是否配置 LC_CTYPE,以处理它自己的启动和兼容性需求。Python 官方也提供忽略环境、保留 LC_CTYPE、不注册信号处理的隔离初始化。当前 Endor 通过 Rust 宿主接收已构造的值,没有需要复刻这套预初始化的依赖环。[CPython 初始化文档](https://docs.python.org/3.14/c-api/init_config.html)。 + +LC_CTYPE 处理字符分类、大小写和多字节字符相关约定;小数点和数字分组属于 LC_NUMERIC。只调整 LC_CTYPE 不能直接等同于修改数字小数点,这两类配置应区分。[locale 类别说明](https://docs.python.org/3.14/library/locale.html#locale.LC_CTYPE)。 + +云原生环境仍会遇到进程变量、终端、文件路径与外部工具的问题;较少跨语言互操作不代表可以省略输入边界。当前解决方式是让核心语义独立于进程区域设置,把必要的解码留在宿主入口。 + +## 当前文本和数字行为 + +- Value::Text 与 EndorStringObject 使用 Rust String,内容始终是有效 UTF-8。字符访问按 Unicode 标量值,不按字节或字形簇;不支持孤立代理码点。[Rust String](https://doc.rust-lang.org/std/string/struct.String.html#utf-8)。 +- Number 的整数与十进制构造器明确检查 ASCII 数字和语法;十进制点为句点,不根据 LANG/LC_NUMERIC 切换为逗号。当前数值比较、哈希和算术尚未实现。 +- 内核没有 setlocale 调用;不会为了启动 VM 修改 LANG、LC_ALL 或 LC_CTYPE。外部库、终端、字体或下游工具自身的行为不在这项保证之内。 +- 源码 parse/run/eval 属于独立 Lyra CLI,使用 Aster EndorReader;Hestia 管理 cli 不执行源码。来源与解码失败契约见 [阅读器](../../vendor/aster/docs/runtime/reader.md)。Frontend 提供基础标识符与语法解析,见 [源码前端](../../vendor/aster/docs/runtime/frontend.md)。尚无通用字节串对象、语言文件系统 API 或非 UTF-8 源码自动转码入口。 + +日文与外部 Shift_JIS/CP932 的处理见 [文本编码](../../vendor/aster/docs/runtime/text-encoding.md);内部 UTF-8 不因地区切换。 + +## 已有进程与部署变量 + +| 名称 | 使用位置与默认值 | 错误和优先级 | +| --- | --- | --- | +| ASTER_STATE_DIR | Host 文件信息目录;默认 data/host,容器 /data/host | 使用 var_os 保留操作系统路径;显式空值报错,不影响数据库选择 | +| ASTER_LISTEN | Server;默认 127.0.0.1:8080 | 当前 var(...).unwrap_or_else:缺失或非 Unicode 都用默认值;有效 Unicode 的错误地址在绑定时失败 | +| ASTER_PORT | Compose 宿主端口映射 | 独立方案默认 8080,ces-control 默认 18081;不是 Vm 配置 | +| POSTGRES_PASSWORD | PostgreSQL Compose/生产环境文件 | Compose 必填;示例使用随机十六进制以避免 URL 转义歧义;不写入仓库 | +| ASTER_IMAGE | ces-control Compose | 必填已构建的版本镜像,通常由 release.env 提供 | +| ASTER_RELEASE_SHA | CI 与部署脚本 | 必须为 40 位小写十六进制提交 SHA;用于镜像和版本目录 | +| ASTER_TEST_POSTGRES_URL | 指定环境显式运行 PostgreSQL 集成测试 | 专用测试库 URL;不是生产数据库配置 | + +文件信息命令及 Server 通过 host 包读取 ASTER_STATE_DIR,Server 还读取 ASTER_LISTEN;不会把 DATABASE_URL/SQLITE_PATH 作为基础信息存储配置。程序不自动加载 .env,Cargo 运行时须显式导出监听变量。Compose 会按其规则读取 .env;显式 --env-file 的生产文件由部署脚本管理。模板见 [.env.example](../../.env.example),执行链见 [部署](deployment.md)。 + +## 显式数据库配置 + +DatabaseConfig 没有默认值和 from_env,仅允许显式构造:Postgres(url)、SqliteFile(path)、SqliteMemory。没有默认 data/aster.db,也没有应用启动时的全局连接。connect 不执行应用 DDL,不保存基础信息;基础信息另由 [host 文件层](host-information.md) 保存;未来的特殊类负责消费连接和定义数据,见 [数据库对象](../runtime/database-objects.md)。 + +SQLite 文件显式连接时创建父目录与文件、启用 WAL,最多 5 个连接、busy timeout 5 秒;内存库单连接、不因池轮换销毁。PostgreSQL 最多 5 个连接、获取超时 5 秒,错误不降级。数据库 URL 的 Debug 和公开错误脱敏;底层错误 source 保留,不应不加处理地公开输出。 + +## 已有执行限额 + +ExecutionLimits 是每次调用显式传入的普通 Rust 值,不是进程全局开关;字段定义见 [frame.rs](../../vendor/aster/crates/core/src/frame.rs)。 + +| 字段 | 默认值 | 作用 | +| --- | --- | --- | +| max_frames | 1000 | 本次执行允许的活动语言帧数量 | +| max_instructions | usize::MAX | 本次执行共享的逻辑指令预算;默认不设实用的有限预算 | + +call_with_limits/execute_with_limits 使用自定义值;嵌套语言调用共用计数。预算不能抢占正在执行的 Native 回调,也没有超时、内存上限或异步取消能力;不能据此视为安全执行不可信代码的完整方案。 + +## 保留的入口缺口与以后再做的内容 + +CLI 当前使用 std::env::args,遇到非 Unicode 命令行参数可能 panic,并没有统一转换成友好的配置错误。[Rust args 文档](https://doc.rust-lang.org/std/env/fn.args.html#panics)。Server 的非 Unicode ASTER_LISTEN 也尚未严格报错;宿主输入错误接口尚未统一。 + +以后增加宿主输入时,应明确使用 OsString/PathBuf 保留系统路径,或显式拒绝不符合文本协议的输入;不要通过修改整个进程 locale 让隐式解码暂时成功。未来源码文本默认 UTF-8、协议数字固定格式是设计方向,只有加载器实现后才能写成已实现行为。 + +现在不新增通用 VmConfig。出现构造期策略时再集中为实例级不可变配置;进程参数由宿主解析后传入,不让 Vm::new 自动读取一组隐含开关。时区、模块路径、凭据注入、内存配额和网络能力也按实际实现分别设计,不预设已存在。 + +返回 [运行指南](../../vendor/aster/docs/operations/README.md) 或 [文档索引](../../vendor/aster/docs/README.md)。 diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md new file mode 100644 index 0000000..c481665 --- /dev/null +++ b/docs/operations/deployment.md @@ -0,0 +1,22 @@ +# Hestia Docker 部署 + +本仓库承接原 Aster HTTP 宿主部署定义,不代表远端已迁移或正在运行本版本。普通 push/PR 只验证;.gitea/workflows/deploy.yml 仅 main 手动触发,先完整验证 SQLite、真实 PostgreSQL 和宿主流程,再构建 runtime 镜像并调用 scripts/deploy-ces-control.sh。 + +Dockerfile 的 source 阶段复制宿主工作区、固定 Aster 子模块和脚本;ci 阶段提供检查环境;build 构建 server 和 admin,runtime 继续安装 server 和兼容 cli,UID 10001,/data,健康检查 /health。管理 cli 只负责 bootstrap 和 metadata,不包含 Lyra 的语言执行命令。 + +| 兼容标识 | 保留原因 | +| --- | --- | +| Compose 项目 endorphin-aster-vm、服务 aster/postgres | 复用既有服务,不创建替代生产实例 | +| 镜像 endorphin-aster-vm: | 沿用部署脚本接口,SHA 现指宿主提交 | +| ASTER_RELEASE_SHA、ASTER_IMAGE、ASTER_PORT、ASTER_LISTEN、ASTER_STATE_DIR | 保持配置兼容 | +| /opt/endorphin-aster-vm | release.env、releases、current 及回退记录 | +| /etc/endorphin-aster-vm/production.env | 原有生产配置与数据库凭据 | +| /var/lib/endorphin-aster-vm/postgres | 原有 PostgreSQL 数据,禁止删除 | +| /var/lib/endorphin-aster-vm/host | 原有 JSON 文件信息与导入标记 | +| /var/lock/endorphin-aster-vm-deploy.lock | 串行切换生产版本 | + +生产 Compose 继续读取 docker-compose.ces-control.yml。默认 docker-compose.yml 和独立 docker-compose.postgres.yml 保留原命名卷、环境与端口接口;不因拆仓改变卷或 PostgreSQL major 版本。 + +部署只接受已构建镜像。健康、对象引导和释放检查成功后,按需只读导出旧 public.aster_metadata,并由 cli metadata import 仅创建缺失 JSON;不覆盖新文件,不写旧表,不删除原库。最后写 deployment.release,原子切换 current。失败时保留数据并尝试恢复上个健康应用版本;应用回退不等于数据库回退。 + +迁移仓库和移除 Aster CD 均不会执行以上部署命令,不停止现有容器,不改共享 Runner。具体文件信息协议见 [host-information.md](host-information.md),验证边界见 [validation.md](validation.md)。返回 [运维](README.md)。 diff --git a/docs/operations/host-information.md b/docs/operations/host-information.md new file mode 100644 index 0000000..e218a1e --- /dev/null +++ b/docs/operations/host-information.md @@ -0,0 +1,47 @@ +# 宿主基础信息:文件保存 + +基础信息不使用数据库。host 包保存逐键 UTF-8 JSON,CLI 提供读写,Server 启动写 host.version,部署检查通过后写 deployment.release。Storage 的数据库连接仅给未来特殊类使用。 + +## 文件与配置 + +ASTER_STATE_DIR 使用操作系统路径,默认 data/host;显式空值报错。读取一个不存在的键返回 None,不创建目录;写入时创建目录。容器为 /data/host;生产挂载到 /var/lib/endorphin-aster-vm/host,独立 PostgreSQL Compose 使用 aster-host 卷。 + +例如 host.version 对应 host.version.json: + +```json +{ + "format_version": 1, + "key": "host.version", + "value": "0.1.0" +} +``` + +控制键以 ASCII 字母或数字开头,最长 128 字节,其余只能是字母、数字、点、下划线或连字符。它是信息命名协议,不是文本编码限制;值支持日文、中文、emoji 等 Unicode 文本。用户文本不应被当作文件路径或控制键。 + +文件必须是有效 UTF-8 JSON,版本和 key 必须匹配请求。损坏记录返回错误,不清空、不用默认值偷偷替换。文件不属于 VM 堆快照,也不存数据库凭据。 + +## 读写与生命周期 + +CLI 的 metadata put/get 通过 host::MetadataStore 访问文件,不连接 SQL: + +```sh +cli metadata put project.note '日本語・中文・😀' +cli metadata get project.note +cli metadata get host.version +``` + +MetadataStore::clone 只共享目录路径,没有缓存;每次读取重新打开文件。写入先建立同目录唯一临时文件,写完并 sync_all,再通过 rename 发布;Unix 新文件权限 0600。不同键分别写文件,不会因整张 map 的读改写丢失其他键;同键并发是最后一次成功发布生效。 + +没有跨键事务、分布式锁或共享网络文件系统一致性保证;文件同步与原子发布也不等于数据库的完整断电持久性协议。异常进程终止可能留下隐藏临时文件,正常失败会清理自己的临时文件。 + +## 旧基础信息的一次性迁移 + +ces-control 发布时,在 host/.legacy-imported 标记缺失的情况下,只读检查 PostgreSQL 原 public.aster_metadata 表。存在时,使用 UTF-8 客户端导出 key/value JSON,再经 cli metadata import 的标准输入导入。该命令只创建缺失文件,不覆盖 Server 刚写入的版本或其他已有文件;临时导出文件以私有权限创建并清理,不打印数据内容。 + +只导入缺失项通过同目录临时文件的原子 hard_link 实现,因此运行目录应使用支持该操作的本地文件系统。导入可重复执行;只有导入成功或源表不存在才写完成标记。损坏记录、不支持的控制键或 I/O 错误会停止发布,保留原库数据,不能当作迁移成功。 + +本迁移针对已配置的 PostgreSQL 生产路径,不自动扫描或删除其他 SQLite 文件。原数据库表与数据不会被修改/删除。此处说明发布脚本行为,不代表远端已执行成功;按约定不主动跟踪 CI 或服务器。 + +部署记录仍以版本镜像、release.env 与 current 链接为发布来源;deployment.release.json 是文件信息记录,不是数据库迁移表。脚本见 [scripts](../../scripts/README.md)。 + +返回 [运行指南](../../vendor/aster/docs/operations/README.md) 或 [文档索引](../../vendor/aster/docs/README.md)。 diff --git a/docs/operations/validation.md b/docs/operations/validation.md new file mode 100644 index 0000000..d24be8a --- /dev/null +++ b/docs/operations/validation.md @@ -0,0 +1,9 @@ +# Hestia 验证 + +仅在 ces-control 执行 Docker、构建、测试、Clippy 和冒烟;本地不执行,不主动查询结果。 + +ci.yml 在 main push、PR 和手动触发时运行 scripts/ci-ces-control.sh。检查容器执行格式、cargo test --workspace --locked、Clippy,再显式运行 storage 的真实 PostgreSQL ignored 测试和 smoke.py。测试 PostgreSQL 使用独立临时容器、私有网络和 tmpfs,不挂载生产数据;SQLite 和 PostgreSQL 都验证不自动建应用表。 + +smoke.py 验证管理 cli 的对象释放、JSON 跨进程读写/缺失导入、Server 的健康/只读对象信息和 SIGTERM 退出,并检查失效数据库变量不导致隐式连接或创建 SQLite。Endor 五段样例的真实 run/eval 报告属于 Lyra CI,不由 Hestia 执行。 + +生产部署位于单独手动工作流,必须重复通过以上验证。SSH 凭据要求见 ../../.gitea/README.md。工作流定义不代表实际通过,未查询结果如实注明。返回 [运维](README.md)。 diff --git a/docs/runtime/README.md b/docs/runtime/README.md new file mode 100644 index 0000000..fe54b59 --- /dev/null +++ b/docs/runtime/README.md @@ -0,0 +1,3 @@ +# 宿主运行时适配 + +[database-objects.md](database-objects.md) 描述为未来特殊类提供的显式数据库适配;没有默认 VM 数据库或自动应用表。引擎本身不依赖此适配。返回 [文档索引](../README.md)。 diff --git a/docs/runtime/database-objects.md b/docs/runtime/database-objects.md new file mode 100644 index 0000000..fb1ae9e --- /dev/null +++ b/docs/runtime/database-objects.md @@ -0,0 +1,36 @@ +# 数据库特殊类与连接边界 + +数据库连接用于某类明确需要数据库的特殊对象,不用于 VM 版本、部署版本、普通对象信息或统一 key/value 元数据。当前尚未定义该特殊类的名字、载荷布局或语言接口,本页说明已保留的连接能力及接入边界。 + +## 当前分层 + +- Objects 负责核心对象与私有堆,Core 负责 VM 和执行;两者不依赖 SQLx 或数据库。 +- Storage 仅暴露 DatabaseConfig、DatabaseConnection 和 DatabaseError。 +- CLI/Server 不依赖 Storage,启动、bootstrap 和 health 不访问数据库;基础信息由独立 host 文件层处理。 +- 特殊类以后由宿主实现按需取得数据库连接;尚未自动挂入 Vm 或对象工厂。 + +## 显式建立连接 + +调用方先构造 DatabaseConfig::Postgres(url)、SqliteFile(path) 或 SqliteMemory,再 await DatabaseConnection::connect。没有 from_env、默认 SQLite 路径或应用启动时的全局数据库。 + +connect 会建立真实连接,并可在显式请求 SQLite 文件时创建父目录和文件,但不创建任何应用表。SQLite WAL、连接协商等驱动自身操作不等于应用 schema 初始化。不会创建基础信息表,也不会写入运行或发布信息。 + +sqlite_pool/postgres_pool 返回匹配后端的 SQLx 池借用,另一后端返回 None。特殊类以后可在自己的适配实现里执行参数化查询、事务及由该类定义的数据结构;本层不预设通用 KV、ORM、迁移或序列化协议。现有 SQLx API 见 [Pool 文档](https://docs.rs/sqlx/0.8.6/sqlx/struct.Pool.html)。 + +## 生命周期 + +DatabaseConnection::clone 共享连接池;close().await 显式关闭所有克隆使用的池,并等待已借出连接归还。取出的 SQLx 池还能被调用方克隆,因此单个包装值销毁不保证所有使用者立即结束。 + +未来将连接放进原生载荷时,必须另行定义异步关闭、共享所有权和错误传播;当前 VM 的同步 Drop/Native 回调不等于已经完成异步数据库类。连接跨进程不能复用,也不代表 VM 快照。 + +## 既有数据和部署 + +原先的基础信息存储已从代码中移除。已有数据库表、卷、密码和服务器数据未执行删除或迁移;它们是否存在、有哪些记录不由本次代码修改推断。 + +生产继续保留预置 PostgreSQL 服务与既有数据目录,等待实际类的消费接口。应用容器不注入数据库连接变量,当前不消费该服务。部署版本通过镜像标签、release.env 和 current 链接识别,基础信息写入文件;发布时仅为旧数据做一次只读导入,不继续写数据库。详见 [宿主信息](../operations/host-information.md)。 + +## 验证 + +SQLite 检查先确认新连接没有应用表,再由测试自己创建 class_rows 验证查询和重连。真实 PostgreSQL 使用全新临时测试库,检查连接不建表,并在事务中创建提交后自动删除的临时表。这些是适配层测试,不代表特殊类已实现。 + +文件定位见 [storage](../../crates/storage/README.md),运行与生产资源见 [部署](../operations/deployment.md)。返回 [运行时索引](../../vendor/aster/docs/runtime/README.md)。 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..ff9bdcf --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,11 @@ +# Hestia 脚本 + +| 文件 | 职责 | +| --- | --- | +| check.sh | 远端格式、工作区测试和 Clippy | +| ci-ces-control.sh | 专用容器/临时 PostgreSQL,验证 SQLite、真实 PostgreSQL 和宿主行为 | +| smoke.py | 实际 server 与管理 cli 的 HTTP、SIGTERM、JSON 持久化和无隐式数据库访问检查 | +| deploy-ces-control.sh | 已验证镜像部署、只读旧信息导入、健康检查和失败回退 | +| README.md | 本目录入口 | + +CI 使用 Hestia 专用容器/缓存名称。生产脚本保留旧 endorphin-aster-vm 资源标识,不能删除数据库或卷。只在指定远端环境执行。返回 [项目入口](../README.md)。 diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100644 index 0000000..592514f --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Run only in the designated remote validation environment. +set -eu +cargo fmt --all --check +cargo test --workspace --locked +cargo clippy --workspace --all-targets --locked -- -D warnings diff --git a/scripts/ci-ces-control.sh b/scripts/ci-ces-control.sh new file mode 100644 index 0000000..552f690 --- /dev/null +++ b/scripts/ci-ces-control.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# Run only on the designated remote CI host, never on the developer's machine. +set -eu +cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" + +release_sha=${ASTER_RELEASE_SHA:?ASTER_RELEASE_SHA is required} +case "$release_sha" in *[!0-9a-f]*|'') echo "Invalid release SHA" >&2; exit 1;; esac +test "${#release_sha}" -eq 40 + +ci_id="${GITHUB_RUN_ID:-manual}-$$" +network="endorphin-hestia-server-ci-$ci_id" +postgres="endorphin-hestia-server-ci-pg-$ci_id" +checks="endorphin-hestia-server-ci-checks-$ci_id" +image="endorphin-hestia-server-ci:$release_sha" +cleanup() { + docker rm -f "$checks" "$postgres" >/dev/null 2>&1 || true + docker network rm "$network" >/dev/null 2>&1 || true +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +docker build --target ci -t "$image" . +docker network create "$network" >/dev/null +# Disposable test database: no production mounts and no published ports. +docker run -d --name "$postgres" --network "$network" --network-alias postgres \ + --env POSTGRES_USER=aster --env POSTGRES_DB=aster \ + --env POSTGRES_PASSWORD=aster-ci-only \ + --tmpfs /var/lib/postgresql/data postgres:17-bookworm >/dev/null + +ready=false +for attempt in $(seq 1 60); do + if docker exec "$postgres" pg_isready -h 127.0.0.1 -U aster -d aster >/dev/null 2>&1; then + ready=true + break + fi + sleep 1 +done +if [ "$ready" != true ]; then + echo "CI PostgreSQL did not become ready" >&2 + exit 1 +fi + +check_status=0 +docker run --name "$checks" --init --network "$network" \ + --mount type=volume,src=endorphin-hestia-server-ci-cargo,dst=/usr/local/cargo/registry \ + --mount type=volume,src=endorphin-hestia-server-ci-target,dst=/build/target \ + --env ASTER_TEST_POSTGRES_URL='postgres://aster:aster-ci-only@postgres:5432/aster?sslmode=disable' \ + --env ASTER_RELEASE_SHA="$release_sha" \ + "$image" sh -eu -c ' + checks=0 + sh scripts/check.sh || checks=$? + test "$checks" -eq 0 + cargo test -p storage --test postgres --locked -- --ignored + python3 scripts/smoke.py + ' || check_status=$? + +exit "$check_status" diff --git a/scripts/deploy-ces-control.sh b/scripts/deploy-ces-control.sh new file mode 100644 index 0000000..046d890 --- /dev/null +++ b/scripts/deploy-ces-control.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# Deploy only the image produced after the remote database checks succeed. +set -eu +cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +test "$(id -u)" -eq 0 || { echo "Deployment requires root on ces-control" >&2; exit 1; } +release_sha=${ASTER_RELEASE_SHA:?ASTER_RELEASE_SHA is required} +case "$release_sha" in *[!0-9a-f]*|'') echo "Invalid release SHA" >&2; exit 1;; esac +test "${#release_sha}" -eq 40 +image="endorphin-aster-vm:$release_sha" +docker image inspect "$image" >/dev/null + +umask 077 +root=/opt/endorphin-aster-vm +config=/etc/endorphin-aster-vm/production.env +release="$root/releases/$release_sha" +mkdir -p "$release" /etc/endorphin-aster-vm /var/lib/endorphin-aster-vm/postgres /var/lib/endorphin-aster-vm/host +chown 10001:10001 /var/lib/endorphin-aster-vm/host +chmod 700 /var/lib/endorphin-aster-vm/host +exec 9>/var/lock/endorphin-aster-vm-deploy.lock +flock 9 + +if [ ! -f "$config" ]; then + password=$(openssl rand -hex 24) + printf 'POSTGRES_PASSWORD=%s\nASTER_PORT=18081\n' "$password" > "$config" + unset password +fi +chmod 600 "$config" +cp docker-compose.ces-control.yml "$release/compose.yml" +printf 'ASTER_IMAGE=%s\n' "$image" > "$release/release.env" +previous=$(readlink -f "$root/current" 2>/dev/null || true) +legacy_export= +cleanup_export() { + if [ -n "$legacy_export" ]; then rm -f "$legacy_export"; fi +} +trap cleanup_export EXIT + +compose_at() { + location=$1 + shift + docker compose -p endorphin-aster-vm --env-file "$config" \ + --env-file "$location/release.env" -f "$location/compose.yml" "$@" +} +rollback() { + echo "Deployment failed; application data has been retained" >&2 + if [ -n "$previous" ] && [ "$previous" != "$release" ] && [ -f "$previous/compose.yml" ]; then + echo "Restoring the previously healthy release" >&2 + compose_at "$previous" up -d --no-build --wait --wait-timeout 180 || true + fi +} + +if ! compose_at "$release" up -d --no-build --wait --wait-timeout 180; then + rollback + exit 1 +fi +if ! health=$(compose_at "$release" exec -T aster curl --fail --silent http://127.0.0.1:8080/health); then + rollback + exit 1 +fi +printf '%s' "$health" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d == {"status":"ok"}' || { + rollback + exit 1 +} +if ! bootstrap=$(compose_at "$release" exec -T aster cli bootstrap); then + rollback + exit 1 +fi +printf '%s' "$bootstrap" | python3 -c 'import sys; s=sys.stdin.read(); assert "runtime=EndorObject type_system=disabled" in s; assert "released heap_objects=0" in s' || { + rollback + exit 1 +} +# Import legacy basic information once, without changing database records. +# Files are authoritative; cli import only creates records that are missing. +if [ ! -f /var/lib/endorphin-aster-vm/host/.legacy-imported ]; then + if ! has_legacy=$(compose_at "$release" exec -T postgres env PGCLIENTENCODING=UTF8 psql -X -U aster -d aster -A -t -v ON_ERROR_STOP=1 \ + -c "SELECT to_regclass('public.aster_metadata') IS NOT NULL"); then + rollback + exit 1 + fi + if [ "$has_legacy" = t ]; then + legacy_export=$(mktemp) + if ! compose_at "$release" exec -T postgres env PGCLIENTENCODING=UTF8 psql -X -U aster -d aster -A -t -v ON_ERROR_STOP=1 \ + -c "SELECT COALESCE(json_agg(json_build_object('key', key, 'value', value)), '[]'::json) FROM public.aster_metadata" > "$legacy_export"; then + rollback + exit 1 + fi + if ! compose_at "$release" exec -T aster cli metadata import < "$legacy_export"; then + rollback + exit 1 + fi + rm -f "$legacy_export" + legacy_export= + elif [ "$has_legacy" != f ]; then + echo "Cannot determine legacy information schema" >&2 + rollback + exit 1 + fi + if ! touch /var/lib/endorphin-aster-vm/host/.legacy-imported; then + rollback + exit 1 + fi +fi +# File-backed host information; no SQL table or database write. +if ! compose_at "$release" exec -T aster cli metadata put deployment.release "$release_sha"; then + rollback + exit 1 +fi +if ! actual=$(compose_at "$release" exec -T aster cli metadata get deployment.release); then + rollback + exit 1 +fi +if [ "$actual" != "$release_sha" ]; then + rollback + exit 1 +fi +ln -sfn "$release" "$root/current.next" +mv -Tf "$root/current.next" "$root/current" +compose_at "$release" ps +echo "Deployed release=$release_sha postgres=provisioned endpoint=127.0.0.1:18081" +echo "Persistent PostgreSQL data: /var/lib/endorphin-aster-vm/postgres" diff --git a/scripts/smoke.py b/scripts/smoke.py new file mode 100644 index 0000000..f37f727 --- /dev/null +++ b/scripts/smoke.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Remote CI end-to-end smoke test, no external database required.""" +import json +import os +from pathlib import Path +import signal +import socket +import subprocess +import tempfile +import time +import urllib.request + +ROOT = Path(__file__).resolve().parents[1] + + +def run(*args, env): + return subprocess.run(args, cwd=ROOT, env=env, check=True, text=True, capture_output=True).stdout.strip() + + +def main(): + env = os.environ.copy() + run("cargo", "build", "--workspace", "--locked", env=env) + # Cargo may use a configured CARGO_TARGET_DIR; discover the actual binaries. + metadata = json.loads(run("cargo", "metadata", "--no-deps", "--format-version", "1", env=env)) + binary_dir = Path(metadata["target_directory"]) / "debug" + cli = str(binary_dir / "cli") + server = str(binary_dir / "server") + with tempfile.TemporaryDirectory(prefix="aster-smoke-") as directory: + env["ASTER_STATE_DIR"] = str(Path(directory) / "host-info") + env["SQLITE_PATH"] = str(Path(directory) / "nested" / "aster.db") + # Neither executable may use database settings for basic VM information. + env["DATABASE_URL"] = "invalid-database-url" + bootstrap = run(cli, "bootstrap", env=env) + assert "runtime=EndorObject type_system=disabled" in bootstrap, bootstrap + assert "objects_distinct=true heap_objects=2" in bootstrap, bootstrap + assert "released heap_objects=0" in bootstrap, bootstrap + assert "stored (file)" in run(cli, "metadata", "put", "project.note", "日本語・中文・😀", env=env) + assert run(cli, "metadata", "get", "project.note", env=env) == "日本語・中文・😀" + legacy = json.dumps([{"key": "project.note", "value": "old"}, + {"key": "legacy.note", "value": "preserved"}]) + subprocess.run([cli, "metadata", "import"], input=legacy, cwd=ROOT, env=env, + check=True, text=True, capture_output=True) + assert run(cli, "metadata", "get", "project.note", env=env) == "日本語・中文・😀" + assert run(cli, "metadata", "get", "legacy.note", env=env) == "preserved" + assert not Path(env["SQLITE_PATH"]).exists(), "CLI must not open a database" + + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + env["ASTER_LISTEN"] = f"127.0.0.1:{port}" + with tempfile.TemporaryFile(mode="w+") as log: + process = subprocess.Popen([server], cwd=ROOT, env=env, stdout=log, stderr=log) + try: + deadline = time.monotonic() + 15 + while True: + try: + with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=1) as response: + health = json.load(response) + break + except (OSError, ValueError): + if process.poll() is not None or time.monotonic() >= deadline: + log.seek(0) + raise AssertionError(f"server did not become healthy: {log.read()}") + time.sleep(0.1) + assert health == {"status": "ok"}, health + for _ in range(2): + with urllib.request.urlopen(f"http://127.0.0.1:{port}/bootstrap", timeout=2) as response: + info = json.load(response) + assert info == {"object_model": "EndorObject", "type_system": False, "heap_objects": 0}, info + finally: + process.send_signal(signal.SIGTERM) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise AssertionError("server did not shut down gracefully") + assert process.returncode == 0, process.returncode + assert not Path(env["SQLITE_PATH"]).exists(), "Server must not open a database" + assert run(cli, "metadata", "get", "host.version", env=env) == "0.1.0" + record = json.loads((Path(env["ASTER_STATE_DIR"]) / "host.version.json").read_text(encoding="utf-8")) + assert record == {"format_version": 1, "key": "host.version", "value": "0.1.0"} + assert run(cli, "metadata", "get", "project.note", env=env) == "日本語・中文・😀" + print("PASS: no implicit database access; file metadata persistence; HTTP health/bootstrap; SIGTERM shutdown") + + +if __name__ == "__main__": + main() diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 0000000..27a5052 --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,11 @@ +# Aster 引擎依赖 + +[aster](aster/README.md) 是独立 Git 子模块;父仓库 gitlink 固定具体提交,.gitmodules 保存 SSH URL。使用专用 codex-bot 的隔离 SSH 命令初始化: + +```sh +git submodule update --init --recursive +``` + +根 Cargo.toml 通过子模块路径链接库;子模块保留自己的 Cargo workspace。父仓库 Cargo.lock 负责宿主依赖图。构建宿主会自动编译所需引擎库,无需启动 Aster 服务。更新时先取得明确的已发布 Aster commit,再 checkout --detach 并提交父仓库 gitlink;不要提交指向未发布引擎提交的宿主版本。 + +CI 获取私有子模块的 bot SSH secrets 要求见 [.gitea](../.gitea/README.md)。Aster 源码和本目录 README 是本目录直接子项。返回 [项目入口](../README.md)。 diff --git a/vendor/aster b/vendor/aster new file mode 160000 index 0000000..55475a9 --- /dev/null +++ b/vendor/aster @@ -0,0 +1 @@ +Subproject commit 55475a9d4b3948a569bf76c84041ad9cea0ec6ea