From 81c2f73d57c49a50941e4975b62d9ed9b923d530 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:45:26 +0000 Subject: [PATCH 01/22] fix: require provisioned node key in shipped config (TC-1200, TC-1282) --- README.md | 39 ++++++---------- tinycloud-node-server/src/runtime.rs | 70 ++++++++++++++++++++++++++++ tinycloud.toml | 4 +- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e65dee08..4350ab9a 100644 --- a/README.md +++ b/README.md @@ -150,35 +150,26 @@ With telemetry enabled, backend time-to-first-byte is recorded under the `server ## Quickstart -To run TinyCloud Protocol locally, install [Rustup](https://rustup.rs). The -repository selects the reviewed Rust `1.97.1` toolchain automatically through -`rust-toolchain.toml`; do not substitute a floating `stable` compiler. +For a self-hosted HTTPS deployment with persistent data, unique identity, +backup/restore, and a verified upgrade path, follow the +[personal-node guide](deploy/personal-node/README.md). The source-checkout +example below is for local development only. It deliberately requires a unique +key: the shipped configuration no longer contains a usable key. +Install [Rustup](https://rustup.rs); `rust-toolchain.toml` selects Rust 1.97.1. +Provision a private key file once and retain it across restarts: -You will need to create a directory for TinyCloud Protocol to store data in: ```bash -mkdir data +umask 077 +mkdir -p data/blocks +openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n' > node.key +TINYCLOUD_KEYS__SECRET="$(cat node.key)" cargo run ``` -Within this directory, create one more directories `blocks` and a database file `caps.db`: -```bash -mkdir data/blocks -touch data/caps.db -``` - -To setup local data storage you can also run `./scripts/init-tinycloud-data.sh` - -You will then need to set the environment variables to point to those directories: -```bash -export TINYCLOUD_STORAGE_BLOCKS_PATH="data/blocks" -export TINYCLOUD_STORAGE_DATABASE="data/caps.db" -``` - -Finally you can run TinyCloud Protocol using `cargo`: -```bash -cargo build -cargo run -``` +Keep `node.key` out of git, back it up separately from the data directory, and +do not replace it during an upgrade: derived node identities and sealed data +depend on it. `TINYCLOUD_KEYS__TYPE=Dstack` remains an explicit alternative +only for a properly configured dstack TEE. ## Configuration diff --git a/tinycloud-node-server/src/runtime.rs b/tinycloud-node-server/src/runtime.rs index 757f41a9..2fa0bfca 100644 --- a/tinycloud-node-server/src/runtime.rs +++ b/tinycloud-node-server/src/runtime.rs @@ -704,6 +704,76 @@ mod tests { assert_eq!(cfg.keys, config::Keys::Auto); } + #[::core::prelude::v1::test] + fn shipped_legacy_config_requires_an_installation_key() { + let _lock = env_lock(); + let temp = tempdir().unwrap(); + fs::write( + temp.path().join("tinycloud.toml"), + include_str!("../../tinycloud.toml"), + ) + .unwrap(); + let _cwd = CwdGuard::set(temp.path()); + let _keys_env = clear_keys_env(); + let config = legacy_config_figment().extract::().unwrap(); + match config.keys { + config::Keys::Static(secret) => { + assert!(tinycloud_core::keys::StaticSecret::try_from(secret).is_err()); + } + config::Keys::Auto => {} + other => panic!("shipped configuration must fail closed without a key: {other:?}"), + } + } + #[::core::prelude::v1::test] + fn shipped_config_uses_explicit_key_and_canonical_override_across_restarts() { + let _lock = env_lock(); + let temp = tempdir().unwrap(); + fs::write( + temp.path().join("tinycloud.toml"), + include_str!("../../tinycloud.toml"), + ) + .unwrap(); + let _cwd = CwdGuard::set(temp.path()); + let _keys_env = clear_keys_env(); + let legacy = base64::encode_config([41; 32], base64::URL_SAFE_NO_PAD); + let canonical = base64::encode_config([42; 32], base64::URL_SAFE_NO_PAD); + let _legacy = EnvGuard::set("TINYCLOUD_KEYS_SECRET", legacy); + let _canonical = EnvGuard::set("TINYCLOUD_KEYS__SECRET", canonical); + let expected = tinycloud_core::keys::StaticSecret::new(vec![42; 32]) + .unwrap() + .node_did(); + + for _ in 0..2 { + let config = legacy_config_figment().extract::().unwrap(); + let config::Keys::Static(secret) = config.keys else { + panic!("explicit key must select Static"); + }; + assert_eq!( + tinycloud_core::keys::StaticSecret::try_from(secret) + .unwrap() + .node_did(), + expected + ); + } + } + + #[cfg(feature = "dstack")] + #[::core::prelude::v1::test] + fn shipped_config_allows_explicit_dstack_without_a_static_key() { + let _lock = env_lock(); + let temp = tempdir().unwrap(); + fs::write( + temp.path().join("tinycloud.toml"), + include_str!("../../tinycloud.toml"), + ) + .unwrap(); + let _cwd = CwdGuard::set(temp.path()); + let _keys_env = clear_keys_env(); + let _dstack = EnvGuard::set("TINYCLOUD_KEYS__TYPE", "Dstack"); + let config = legacy_config_figment().extract::().unwrap(); + assert_eq!(config.keys, config::Keys::Dstack); + } + #[cfg(feature = "dstack")] #[::core::prelude::v1::test] fn serve_config_figment_loads_keys_type_from_env() { diff --git a/tinycloud.toml b/tinycloud.toml index 20026212..a0325900 100644 --- a/tinycloud.toml +++ b/tinycloud.toml @@ -44,8 +44,10 @@ cors = true # retry_window_ms = 10000 [global.keys] + # Provision a unique, persistent root before starting this node. + # TINYCLOUD_KEYS__SECRET supplies it; Dstack deployments explicitly + # select TINYCLOUD_KEYS__TYPE=Dstack instead. type = "Static" - secret = "U29tZSBsb25nIHBpZWNlIG9mIGVudHJvcHkgd2hpY2ggaXMgYSBzZWNyZXQgYW5kIG1vcmUgdGhhbiAzMiBieXRlcw" [global.spaces] ## Space allow list api endpoint From feb2019efa91f2bc406cbfd6aac6d310de4e88e3 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:45:37 +0000 Subject: [PATCH 02/22] feat: exercise personal node lifecycle on published images (TC-1282) --- .github/workflows/personal-node.yml | 39 ++++++ deploy/personal-node/Caddyfile | 3 + deploy/personal-node/Caddyfile.test | 4 + deploy/personal-node/README.md | 71 +++++++++++ deploy/personal-node/ci.sh | 58 +++++++++ deploy/personal-node/compose.yaml | 36 ++++++ deploy/personal-node/dstack-smoke.sh | 31 +++++ deploy/personal-node/dstack-smoke.yaml | 8 ++ deploy/personal-node/node.sh | 159 +++++++++++++++++++++++++ deploy/personal-node/tinycloud.toml | 13 ++ test/probe/personal-node-check.ts | 38 ++++++ 11 files changed, 460 insertions(+) create mode 100644 .github/workflows/personal-node.yml create mode 100644 deploy/personal-node/Caddyfile create mode 100644 deploy/personal-node/Caddyfile.test create mode 100644 deploy/personal-node/README.md create mode 100755 deploy/personal-node/ci.sh create mode 100644 deploy/personal-node/compose.yaml create mode 100755 deploy/personal-node/dstack-smoke.sh create mode 100644 deploy/personal-node/dstack-smoke.yaml create mode 100755 deploy/personal-node/node.sh create mode 100644 deploy/personal-node/tinycloud.toml create mode 100644 test/probe/personal-node-check.ts diff --git a/.github/workflows/personal-node.yml b/.github/workflows/personal-node.yml new file mode 100644 index 00000000..a3d30715 --- /dev/null +++ b/.github/workflows/personal-node.yml @@ -0,0 +1,39 @@ +name: Personal node guide +on: + pull_request: + paths: + - 'deploy/personal-node/**' + - 'test/probe/personal-node-check.ts' + - '.github/workflows/personal-node.yml' + - 'tinycloud.toml' + - 'docker-compose.dstack.yaml' + - 'tinycloud-node-server/src/runtime.rs' + push: + branches: [main] + paths: + - 'deploy/personal-node/**' + - 'test/probe/personal-node-check.ts' + - '.github/workflows/personal-node.yml' + - 'docker-compose.dstack.yaml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + published-images: + runs-on: ubuntu-latest + timeout-minutes: 35 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: test/probe/package-lock.json + - name: Install pinned signing client + run: npm ci --prefix test/probe + - name: Exercise the published personal-node guide + run: bash deploy/personal-node/ci.sh + - name: Check dstack-profile local-data persistence + run: bash deploy/personal-node/dstack-smoke.sh diff --git a/deploy/personal-node/Caddyfile b/deploy/personal-node/Caddyfile new file mode 100644 index 00000000..83d47d4f --- /dev/null +++ b/deploy/personal-node/Caddyfile @@ -0,0 +1,3 @@ +{$NODE_DOMAIN} { + reverse_proxy node:8000 +} diff --git a/deploy/personal-node/Caddyfile.test b/deploy/personal-node/Caddyfile.test new file mode 100644 index 00000000..5b5cc127 --- /dev/null +++ b/deploy/personal-node/Caddyfile.test @@ -0,0 +1,4 @@ +{$NODE_DOMAIN} { + tls internal + reverse_proxy node:8000 +} diff --git a/deploy/personal-node/README.md b/deploy/personal-node/README.md new file mode 100644 index 00000000..94824f1c --- /dev/null +++ b/deploy/personal-node/README.md @@ -0,0 +1,71 @@ +# Run a personal TinyCloud node + +This ordinary-server profile runs a single node with local SQLite, local blocks, and Caddy HTTPS. It tracks the version TinyCloud itself runs in production: **1.20.6**, pinned by immutable image digest in [`compose.yaml`](compose.yaml). The prior published ordinary image 1.20.5 is used only to rehearse an upgrade and restore-based rollback. Do not use `latest` or 1.20.7: the latter was rolled back in production (TC-1176). New node-image changes remain unavailable to this published-image guide until a separately approved release ships. + +This is **not** a TEE: the VPS operator and its host can access the filesystem and key. No hardware attestation, dstack KMS isolation, or cross-host account migration is claimed. This guide does not configure registry discovery or hosted-to-owned-node replication (TC-1280/TC-1283). TLS authenticates your domain but does not attest the machine. + +## Preflight + +- Own an ordinary Linux VPS with Docker Engine and Compose v2, `bash`, `curl`, `jq`, `openssl`, `tar`, `sudo`, Node.js 22 and npm. Clone this repository at the revision carrying the guide. Check that ports 80/443 are free, the domain A/AAAA record points to the VPS, and incoming 80/443 are allowed for Caddy certificate issuance and HTTPS. Do not expose the node's unauthenticated metrics port 8001, Docker socket, or its plain HTTP port 8000. +- Store node data on a persistent local filesystem with available disk for both the live data and a full backup. `node.sh init` sets only that dedicated data directory to UID/GID 1000, the non-root image user. Confirm host backups and access controls before admitting users. +- Choose a unique `NODE_HOME` path for this installation. Keep its `node.env` (node identity root) and your owner-held Ethereum private key separate, private and recoverable. Never put either in git, CI logs, or a public volume. Back up the node identity independently of the data. Preserve the original root through upgrades: changing it changes node identity and can make encrypted/sealed state unreadable. +- There is **no** hosted billing/quota URL in this profile. The ordinary node still depends on its chosen wallet/signing client and, for public HTTPS, DNS and the CA. Review the EGPL-1.5 license and host hardening for your deployment. + +All commands below use the same [`node.sh`](node.sh) operations invoked by [the CI rehearsal](ci.sh). Run them from the repository root. No production credentials or hosted services are used by CI. + +## Provision and launch + +```bash +npm ci --prefix test/probe +export NODE_HOME="$HOME/personal-tinycloud" +bash deploy/personal-node/node.sh init node.example.com +bash deploy/personal-node/node.sh check-missing-key +bash deploy/personal-node/node.sh start +bash deploy/personal-node/node.sh check-version 1.20.6 +``` + +Replace `node.example.com` with the domain you own. `init` writes a unique 48-byte random Static root to a `0600` private `node.env`; it is never echoed. The profile mounts a keyless TOML over the bundled image config, so even the older 1.20.6 image **fails** if the installation root is absent. `check-missing-key` must run before `start`; it probes the unprovisioned path without printing keys. Caddy obtains and renews a public certificate; do not bypass its trust verification. + +To prove owner-held EOA operations, place your own wallet's 32-byte hexadecimal private key (without `0x`) in a separate `0600` file outside `NODE_HOME`, then run: + +```bash +bash deploy/personal-node/node.sh seed /secure/path/owner.key +bash deploy/personal-node/node.sh restart +bash deploy/personal-node/node.sh verify /secure/path/owner.key +``` + +The pinned `@tinycloud/node-sdk` signs in, hosts an owned space, writes and reads a KV object and a SQL row. The `verify` operation checks their exact values and the node's public identity after restart. Do not put a key on the command line; only its file path is passed. Your wallet key is **not** included in the node backup and must be retained separately. + +## Offline backup and fresh restore + +Stop writes before backup. `backup` stops the node, archives the entire `/data` tree while SQLite and SQL actors are closed, encrypts `node.env` as a separate artifact, then restarts the node. The passphrase file is not included in either artifact. Retain both the backup and passphrase in independent secure locations. Test recovery regularly. + +```bash +# Create a private passphrase file out of band; it must not be committed or logged. +export BACKUP_PASSPHRASE_FILE=/secure/path/node-backup-passphrase +bash deploy/personal-node/node.sh backup /secure/path/node-backup-2026-10-16 "$BACKUP_PASSPHRASE_FILE" + +# On the destination, copy the encrypted backup, passphrase and owner wallet +# by independent secure channels; use a fresh NODE_HOME and the same domain. +export NODE_HOME="$HOME/personal-tinycloud-restored" +bash deploy/personal-node/node.sh restore /secure/path/node-backup-2026-10-16 "$BACKUP_PASSPHRASE_FILE" +bash deploy/personal-node/node.sh start +bash deploy/personal-node/node.sh check-version 1.20.6 +bash deploy/personal-node/node.sh verify /secure/path/owner.key +``` + +Use a fresh host/directory, not two live nodes serving the same identity and domain at once. Repoint DNS only after verifying the new host; Caddy obtains a new certificate. `verify` compares the public node DID saved before backup and the signed KV/SQL data; a different root fails rather than silently rotating. The backup is a full offline snapshot, **not** continuous replication. TLS ACME state is not in the backup, and a restore needs a reachable domain and valid certificate again. + +## Upgrade and rollback rehearsal + +CI executes [`ci.sh`](ci.sh) on a clean runner: 1.20.5 → 1.20.6, with data checks before and after restart, fresh-volume restores, and rollback by restoring the **pre-upgrade** snapshot into another fresh instance on 1.20.5. An in-place downgrade of a migrated database is not supported. A normal new installation starts directly on 1.20.6; do not downgrade it merely to rehearse. + +```bash +# Run only on a disposable isolated runner with permission to create Docker +# containers. This is exactly the end-to-end job used by GitHub Actions. +bash deploy/personal-node/ci.sh +``` + +Before a future upgrade, take and verify an offline backup as above, stop the node, change the pinned image only to a reviewed released digest, start and verify identity and signed data. If verification fails, stop the candidate and restore that pre-upgrade backup to a fresh instance of the previous image; never blindly start the older binary against migrated data. The next-release upgrade step **requires the next approved production node release** and a new published-image CI run. TC-1176 prohibits promotion of that release until Sam separately clears the hold. Changes to the shipped default key in TC-1200 likewise require that next image; the mounted keyless profile is the safe 1.20.6 workaround, not proof that its bare image fails closed. + +The [personal-node workflow](../../.github/workflows/personal-node.yml) is the acceptance evidence only when its published-image job is green. Record the successful run URL and exact image versions/digests in TC-1282; no CI result here authorizes a production deployment or a stronger sovereignty claim. diff --git a/deploy/personal-node/ci.sh b/deploy/personal-node/ci.sh new file mode 100755 index 00000000..16ec6fa4 --- /dev/null +++ b/deploy/personal-node/ci.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Run only on an isolated GitHub Actions runner, not a shared Docker host. +set -euo pipefail +umask 077 +here="$(cd "$(dirname "$0")" && pwd)" +work="$(mktemp -d)" +owner="$work/owner.key" +passphrase="$work/backup-passphrase" +openssl rand -hex 32 > "$owner" +openssl rand -base64 48 > "$passphrase" + +cleanup() { + for name in tc1282-a tc1282-b tc1282-c tc1282-d; do + if [[ -f "$work/$name/tls-mode" ]]; then + NODE_HOME="$work/$name" "$here/node.sh" destroy-test || true + fi + done +} +trap cleanup EXIT + +export NODE_HOME="$work/tc1282-a" +"$here/node.sh" init localhost test 1.20.5 +"$here/node.sh" check-missing-key +"$here/node.sh" start +"$here/node.sh" check-version 1.20.5 +"$here/node.sh" seed "$owner" +"$here/node.sh" restart +"$here/node.sh" verify "$owner" +"$here/node.sh" backup "$work/pre-upgrade" "$passphrase" +"$here/node.sh" stop + +export NODE_HOME="$work/tc1282-b" +"$here/node.sh" restore "$work/pre-upgrade" "$passphrase" +"$here/node.sh" start +"$here/node.sh" check-version 1.20.5 +"$here/node.sh" verify "$owner" +"$here/node.sh" upgrade +"$here/node.sh" check-version 1.20.6 +"$here/node.sh" verify "$owner" +"$here/node.sh" backup "$work/post-upgrade" "$passphrase" +"$here/node.sh" stop + +export NODE_HOME="$work/tc1282-c" +"$here/node.sh" restore "$work/post-upgrade" "$passphrase" +"$here/node.sh" start +"$here/node.sh" check-version 1.20.6 +"$here/node.sh" verify "$owner" +"$here/node.sh" stop + +# Rollback is a restore from the pre-upgrade snapshot, not an in-place +# downgrade of a migrated database. +export NODE_HOME="$work/tc1282-d" +"$here/node.sh" restore "$work/pre-upgrade" "$passphrase" +"$here/node.sh" start +"$here/node.sh" check-version 1.20.5 +"$here/node.sh" verify "$owner" +"$here/node.sh" stop +printf '%s\n' 'Personal-node lifecycle passed: TLS, signed KV/SQL, restart, fresh restore, upgrade, rollback restore' diff --git a/deploy/personal-node/compose.yaml b/deploy/personal-node/compose.yaml new file mode 100644 index 00000000..60d24f80 --- /dev/null +++ b/deploy/personal-node/compose.yaml @@ -0,0 +1,36 @@ +name: personal-node +services: + node: + image: ${NODE_IMAGE:-ghcr.io/tinycloudlabs/tinycloud-node@sha256:41667d3d133aaa12290a2f3a744afacd838988327552d159c60f04466e66f86f} + restart: unless-stopped + env_file: + - ${NODE_KEY_ENV_FILE:?Set the private node key file} + environment: + TINYCLOUD_STORAGE__DATADIR: /data + ROCKET_ADDRESS: 0.0.0.0 + volumes: + - ${NODE_DATA_DIR:?Set the node data directory}:/data + # 1.20.6 still embeds a sample key in its default TOML. This clean + # config masks the image copy, even when the operator key is absent. + - ./tinycloud.toml:/tinycloud.toml:ro + security_opt: + - no-new-privileges:true + + https: + image: caddy:2.10.2 + restart: unless-stopped + depends_on: + - node + ports: + - "${NODE_HTTP_BIND:-80:80}" + - "${NODE_HTTPS_BIND:-443:443}" + environment: + NODE_DOMAIN: ${NODE_DOMAIN:?Set your domain} + volumes: + - ${NODE_CADDYFILE:-./Caddyfile}:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + +volumes: + caddy-data: + caddy-config: diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh new file mode 100755 index 00000000..ebd8c158 --- /dev/null +++ b/deploy/personal-node/dstack-smoke.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# CI-only smoke of the published dstack compose's local data mount, using +# explicit Static keys instead of requiring a real dstack socket on the runner. +set -euo pipefail +umask 077 +here="$(cd "$(dirname "$0")/../.." && pwd)" +work="$(mktemp -d)" +owner="$work/owner.key" +openssl rand -hex 32 > "$owner" +export TC1282_TEST_KEY="$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')" +export COMPOSE_PROJECT_NAME=tc1282-dstack +compose() { docker compose -f "$here/docker-compose.dstack.yaml" -f "$here/deploy/personal-node/dstack-smoke.yaml" "$@"; } +trap 'compose down -v >/dev/null 2>&1 || true' EXIT + +ready() { + for _ in {1..40}; do + if curl -fsS http://127.0.0.1:8000/healthz -o /dev/null; then return; fi + sleep 2 + done + printf '%s\n' 'Dstack-profile health check failed' >&2 + exit 1 +} + +compose up -d tinycloud +ready +"$here/test/probe/node_modules/.bin/tsx" "$here/test/probe/personal-node-check.ts" seed http://127.0.0.1:8000 "$owner" +compose down +compose up -d tinycloud +ready +"$here/test/probe/node_modules/.bin/tsx" "$here/test/probe/personal-node-check.ts" verify http://127.0.0.1:8000 "$owner" +printf '%s\n' 'Dstack compose local SQLite and blocks survived container recreation' diff --git a/deploy/personal-node/dstack-smoke.yaml b/deploy/personal-node/dstack-smoke.yaml new file mode 100644 index 00000000..0bb54b41 --- /dev/null +++ b/deploy/personal-node/dstack-smoke.yaml @@ -0,0 +1,8 @@ +services: + tinycloud: + environment: + TINYCLOUD_KEYS__TYPE: Static + TINYCLOUD_KEYS__SECRET: ${TC1282_TEST_KEY:?CI test key required} + TINYCLOUD_TEE_MODE: 'off' + labels: + tc1282: 'true' diff --git a/deploy/personal-node/node.sh b/deploy/personal-node/node.sh new file mode 100755 index 00000000..4a74ab15 --- /dev/null +++ b/deploy/personal-node/node.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 + +here="$(cd "$(dirname "$0")" && pwd)" +home="$(realpath -m "${NODE_HOME:-./personal-node}")" +image_1206='ghcr.io/tinycloudlabs/tinycloud-node@sha256:41667d3d133aaa12290a2f3a744afacd838988327552d159c60f04466e66f86f' +image_1205='ghcr.io/tinycloudlabs/tinycloud-node@sha256:eba68094f922b8d377d4439bfa14e7d12b875de0155f03810cf6b5d4cb3b1ab7' + +fail() { printf '%s\n' "$*" >&2; exit 1; } +require_home() { [[ -f "$home/node.env" && -f "$home/image" && -f "$home/domain" ]] || fail 'Initialize or restore the node first'; } + +compose() { + require_home + local domain tls_mode + domain="$(<"$home/domain")" + tls_mode="$(<"$home/tls-mode")" + export NODE_IMAGE="$(<"$home/image")" + export NODE_KEY_ENV_FILE="${NODE_KEY_ENV_FILE:-$home/node.env}" + export NODE_DATA_DIR="$home/data" NODE_DOMAIN="$domain" + export COMPOSE_PROJECT_NAME="$(basename "$home")" + export NODE_CADDYFILE="$here/Caddyfile" + if [[ "$tls_mode" == test ]]; then + export NODE_CADDYFILE="$here/Caddyfile.test" + export NODE_HTTP_BIND='127.0.0.1:8080:80' NODE_HTTPS_BIND='127.0.0.1:8443:443' + fi + docker compose -f "$here/compose.yaml" "$@" +} + +url() { + if [[ "$(<"$home/tls-mode")" == test ]]; then + printf 'https://localhost:8443' + else + printf 'https://%s' "$(<"$home/domain")" + fi +} + +ca_args=() +prepare_tls() { + ca_args=() + if [[ "$(<"$home/tls-mode")" == test ]]; then + for _ in {1..40}; do + if compose cp https:/data/caddy/pki/authorities/local/root.crt "$home/test-root.crt" >/dev/null 2>&1; then break; fi + sleep 2 + done + [[ -s "$home/test-root.crt" ]] || fail 'Test TLS certificate was not issued' + ca_args=(--cacert "$home/test-root.crt") + export NODE_EXTRA_CA_CERTS="$home/test-root.crt" + fi +} + +health() { + prepare_tls + for _ in {1..40}; do + if curl -fsS "${ca_args[@]}" "$(url)/healthz" -o /dev/null; then return; fi + sleep 2 + done + fail 'HTTPS health check failed' +} + +identity() { + health + curl -fsS "${ca_args[@]}" "$(url)/.well-known/tinycloud/node-keys" | jq -er '.nodeDid' > "$home/current-identity" + [[ -s "$home/current-identity" ]] || fail 'Node identity is unavailable' +} + +case "${1:-}" in + init) + [[ $# -ge 2 ]] || fail 'Usage: node.sh init DOMAIN [test] [1.20.5]' + [[ ! -e "$home" ]] || fail 'Choose a fresh NODE_HOME' + [[ "$2" =~ ^[a-zA-Z0-9.-]+$ ]] || fail 'Invalid domain' + [[ "${3:-}" == '' || "${3:-}" == test ]] || fail 'TLS mode must be test or omitted' + [[ "${4:-}" == '' || "${4:-}" == 1.20.5 ]] || fail 'Only 1.20.5 is supported as the previous image' + mkdir -p "$home/data" + printf '%s\n' "$2" > "$home/domain" + printf '%s\n' "${3:-production}" > "$home/tls-mode" + printf '%s\n' "$image_1206" > "$home/image" + if [[ "${4:-}" == 1.20.5 ]]; then printf '%s\n' "$image_1205" > "$home/image"; fi + printf 'TINYCLOUD_KEYS__TYPE=Static\nTINYCLOUD_KEYS__SECRET=%s\n' \ + "$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')" > "$home/node.env" + sudo chown -R 1000:1000 "$home/data" + ;; + check-missing-key) + require_home + blank="$(mktemp)" + trap 'rm -f "$blank"' EXIT + printf 'TINYCLOUD_KEYS__TYPE=Static\n' > "$blank" + status=0 + NODE_KEY_ENV_FILE="$blank" timeout 30 "$0" compose-check >/dev/null 2>&1 || status=$? + [[ "$status" == 1 ]] || fail 'Missing-key check did not fail promptly with a provisioning error' + printf '%s\n' 'Missing-key startup failed as required' + ;; + compose-check) + compose run --rm --no-deps node + ;; + start) + compose up -d + health + ;; + stop) + compose stop + ;; + check-version) + [[ $# == 2 ]] || fail 'Usage: node.sh check-version 1.20.5|1.20.6' + health + curl -fsS "${ca_args[@]}" "$(url)/version" | + jq -e --arg version "$2" '.version == $version and .quotaUrl == null and .inTee == false' >/dev/null || + fail 'Unexpected node version, hosted quota, or TEE mode' + ;; + verify|seed) + [[ $# == 2 && -f "$2" ]] || fail "Usage: node.sh $1 OWNER_KEY_FILE" + identity + if [[ -f "$home/identity" ]]; then + cmp -s "$home/identity" "$home/current-identity" || fail 'Node identity changed' + else + [[ "$1" == seed ]] || fail 'Expected restored identity record' + cp "$home/current-identity" "$home/identity" + fi + [[ -x "$here/../../test/probe/node_modules/.bin/tsx" ]] || fail 'Run npm ci --prefix test/probe first' + "$here/../../test/probe/node_modules/.bin/tsx" "$here/../../test/probe/personal-node-check.ts" "$1" "$(url)" "$2" + ;; + backup) + [[ $# == 3 && -f "$3" ]] || fail 'Usage: node.sh backup BACKUP_DIR PASSPHRASE_FILE' + [[ ! -e "$2" ]] || fail 'Choose a fresh backup directory' + identity + mkdir -m 700 "$2" + compose stop node + tar -C "$home/data" -czf "$2/data.tar.gz" . + openssl enc -aes-256-cbc -pbkdf2 -salt -in "$home/node.env" -out "$2/identity.enc" -pass "file:$3" + cp "$home/identity" "$home/image" "$home/domain" "$home/tls-mode" "$2/" + compose up -d node + health + ;; + restore) + [[ $# == 3 && -f "$3" ]] || fail 'Usage: node.sh restore BACKUP_DIR PASSPHRASE_FILE' + [[ ! -e "$home" ]] || fail 'Restore only into a fresh NODE_HOME' + for file in identity.enc data.tar.gz identity image domain tls-mode; do + [[ -f "$2/$file" ]] || fail "Backup lacks $file" + done + mkdir -p "$home/data" + openssl enc -d -aes-256-cbc -pbkdf2 -in "$2/identity.enc" -out "$home/node.env" -pass "file:$3" || fail 'Identity decryption failed' + tar -C "$home/data" -xzf "$2/data.tar.gz" + cp "$2/identity" "$2/image" "$2/domain" "$2/tls-mode" "$home/" + sudo chown -R 1000:1000 "$home/data" + ;; + upgrade) + require_home + [[ "$(<"$home/image")" == "$image_1205" ]] || fail 'Upgrade expects the previous published image' + compose stop node + printf '%s\n' "$image_1206" > "$home/image" + compose up -d node + health + ;; + destroy-test) + [[ "$(<"$home/tls-mode")" == test && "$(basename "$home")" == tc1282-* ]] || fail 'Only tc1282 test instances may be removed' + compose down -v + ;; + *) fail 'Usage: node.sh {init|check-missing-key|start|check-version|seed|verify|restart|stop|backup|restore|upgrade|destroy-test}' ;; +esac diff --git a/deploy/personal-node/tinycloud.toml b/deploy/personal-node/tinycloud.toml new file mode 100644 index 00000000..d2dfcb8d --- /dev/null +++ b/deploy/personal-node/tinycloud.toml @@ -0,0 +1,13 @@ +[global] +log_level = "normal" +port = 8000 + +[global.storage] +datadir = "/data" + +[global.storage.blocks] +type = "Local" + +[global.keys] +type = "Static" +# The secret is provided only through the installation's private node.env. diff --git a/test/probe/personal-node-check.ts b/test/probe/personal-node-check.ts new file mode 100644 index 00000000..f8374ee2 --- /dev/null +++ b/test/probe/personal-node-check.ts @@ -0,0 +1,38 @@ +import { readFileSync } from "node:fs"; +import { TinyCloudNode } from "@tinycloud/node-sdk"; + +const mode = process.argv[2]; +if (mode !== "seed" && mode !== "verify") throw new Error("Expected seed or verify"); +const host = process.argv[3]; +const ownerKeyFile = process.argv[4]; +if (!host || !ownerKeyFile) throw new Error("Expected HTTPS URL and owner-key file"); + +const privateKey = readFileSync(ownerKeyFile, "utf8").trim(); +const node = new TinyCloudNode({ privateKey, host, prefix: "personal-node-guide" }); +const value = "restored-from-personal-node"; + +function unwrap(result: { ok: true; data: T } | { ok: false; error: unknown }): T { + if (!result.ok) throw result.error; + return result.data; +} + +async function main(): Promise { + unwrap(await node.signIn()); + if (mode === "seed") { + await node.hostOwnedSpace("personal-node-guide"); + unwrap(await node.kv.put("backup-check", value)); + unwrap(await node.sql.execute("CREATE TABLE IF NOT EXISTS backup_check (id INTEGER PRIMARY KEY, value TEXT NOT NULL)")); + unwrap(await node.sql.execute("INSERT OR REPLACE INTO backup_check (id, value) VALUES (1, ?)", [value])); + } + const kv = unwrap(await node.kv.get("backup-check")); + if (kv.data !== value) throw new Error("KV content differs after lifecycle step"); + const sql = unwrap(await node.sql.query("SELECT value FROM backup_check WHERE id = 1")); + if (sql.rows[0]?.[0] !== value) throw new Error("SQL content differs after lifecycle step"); + console.log(`Signed KV and SQL ${mode} succeeded`); +} + +main().catch(() => { + // SDK errors can contain request headers or credentials. Never log raw errors. + console.error(`Signed KV and SQL ${mode} failed`); + process.exitCode = 1; +}); From a16b9676bf773ae7f005f9e667d6fea8d1aac1e6 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:47:30 +0000 Subject: [PATCH 03/22] fix: check published node version response fields (TC-1282) --- deploy/personal-node/dstack-smoke.sh | 3 +++ deploy/personal-node/node.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh index ebd8c158..8907ae17 100755 --- a/deploy/personal-node/dstack-smoke.sh +++ b/deploy/personal-node/dstack-smoke.sh @@ -23,6 +23,9 @@ ready() { compose up -d tinycloud ready +curl -fsS http://127.0.0.1:8000/version | + jq -e '.version == "1.20.6" and (.features | index("tee") != null) and .inTEE == false' >/dev/null || + { printf '%s\n' 'Dstack profile did not run the pinned dstack build' >&2; exit 1; } "$here/test/probe/node_modules/.bin/tsx" "$here/test/probe/personal-node-check.ts" seed http://127.0.0.1:8000 "$owner" compose down compose up -d tinycloud diff --git a/deploy/personal-node/node.sh b/deploy/personal-node/node.sh index 4a74ab15..fc9a7310 100755 --- a/deploy/personal-node/node.sh +++ b/deploy/personal-node/node.sh @@ -104,7 +104,7 @@ case "${1:-}" in [[ $# == 2 ]] || fail 'Usage: node.sh check-version 1.20.5|1.20.6' health curl -fsS "${ca_args[@]}" "$(url)/version" | - jq -e --arg version "$2" '.version == $version and .quotaUrl == null and .inTee == false' >/dev/null || + jq -e --arg version "$2" '.version == $version and .quota_url == null and .inTEE == false' >/dev/null || fail 'Unexpected node version, hosted quota, or TEE mode' ;; verify|seed) From 5bde8346c5e95b4ae62ab19d82c64363410a640d Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:48:45 +0000 Subject: [PATCH 04/22] test: identify failing signed lifecycle operation (TC-1282) --- test/probe/personal-node-check.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/probe/personal-node-check.ts b/test/probe/personal-node-check.ts index f8374ee2..c4ea2df0 100644 --- a/test/probe/personal-node-check.ts +++ b/test/probe/personal-node-check.ts @@ -10,6 +10,7 @@ if (!host || !ownerKeyFile) throw new Error("Expected HTTPS URL and owner-key fi const privateKey = readFileSync(ownerKeyFile, "utf8").trim(); const node = new TinyCloudNode({ privateKey, host, prefix: "personal-node-guide" }); const value = "restored-from-personal-node"; +let stage = "signIn"; function unwrap(result: { ok: true; data: T } | { ok: false; error: unknown }): T { if (!result.ok) throw result.error; @@ -18,21 +19,31 @@ function unwrap(result: { ok: true; data: T } | { ok: false; error: unknown } async function main(): Promise { unwrap(await node.signIn()); + stage = "hostOwnedSpace"; if (mode === "seed") { await node.hostOwnedSpace("personal-node-guide"); + stage = "kv.put"; unwrap(await node.kv.put("backup-check", value)); + stage = "sql.schema"; unwrap(await node.sql.execute("CREATE TABLE IF NOT EXISTS backup_check (id INTEGER PRIMARY KEY, value TEXT NOT NULL)")); + stage = "sql.insert"; unwrap(await node.sql.execute("INSERT OR REPLACE INTO backup_check (id, value) VALUES (1, ?)", [value])); } + stage = "kv.get"; const kv = unwrap(await node.kv.get("backup-check")); if (kv.data !== value) throw new Error("KV content differs after lifecycle step"); + stage = "sql.query"; const sql = unwrap(await node.sql.query("SELECT value FROM backup_check WHERE id = 1")); if (sql.rows[0]?.[0] !== value) throw new Error("SQL content differs after lifecycle step"); console.log(`Signed KV and SQL ${mode} succeeded`); } -main().catch(() => { +main().catch((error: unknown) => { // SDK errors can contain request headers or credentials. Never log raw errors. - console.error(`Signed KV and SQL ${mode} failed`); + const meta = error && typeof error === "object" && "meta" in error ? error.meta : undefined; + const status = meta && typeof meta === "object" && "status" in meta && typeof meta.status === "number" + ? ` HTTP ${meta.status}` + : ""; + console.error(`Signed KV and SQL ${mode} failed at ${stage}${status}`); process.exitCode = 1; }); From 5d54c6129b9d801dbddd245c8fc9469aa5c102a1 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:49:21 +0000 Subject: [PATCH 05/22] fix: await void SDK sign-in before lifecycle calls (TC-1282) --- test/probe/personal-node-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/probe/personal-node-check.ts b/test/probe/personal-node-check.ts index c4ea2df0..c3ca64d2 100644 --- a/test/probe/personal-node-check.ts +++ b/test/probe/personal-node-check.ts @@ -18,7 +18,7 @@ function unwrap(result: { ok: true; data: T } | { ok: false; error: unknown } } async function main(): Promise { - unwrap(await node.signIn()); + await node.signIn(); stage = "hostOwnedSpace"; if (mode === "seed") { await node.hostOwnedSpace("personal-node-guide"); From 48faa281d193c9fba08bcc59f23a7f9b528ff37d Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:49:48 +0000 Subject: [PATCH 06/22] test: typecheck personal-node signing probe (TC-1282) --- .github/workflows/personal-node.yml | 2 ++ test/probe/tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/personal-node.yml b/.github/workflows/personal-node.yml index a3d30715..0dcd7cff 100644 --- a/.github/workflows/personal-node.yml +++ b/.github/workflows/personal-node.yml @@ -33,6 +33,8 @@ jobs: cache-dependency-path: test/probe/package-lock.json - name: Install pinned signing client run: npm ci --prefix test/probe + - name: Typecheck signing probe + run: npm run typecheck --prefix test/probe - name: Exercise the published personal-node guide run: bash deploy/personal-node/ci.sh - name: Check dstack-profile local-data persistence diff --git a/test/probe/tsconfig.json b/test/probe/tsconfig.json index f0d40d33..767fe2cf 100644 --- a/test/probe/tsconfig.json +++ b/test/probe/tsconfig.json @@ -21,6 +21,7 @@ "include": [ "write-probe.ts", "probe-core.ts", - "probe-core.test.ts" + "probe-core.test.ts", + "personal-node-check.ts" ] } From 8236a18b0a3fc01ca10d3bfc43abe031887ec0e5 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:53:25 +0000 Subject: [PATCH 07/22] fix: implement guide restart operation (TC-1282) --- deploy/personal-node/node.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/personal-node/node.sh b/deploy/personal-node/node.sh index fc9a7310..ce1ff676 100755 --- a/deploy/personal-node/node.sh +++ b/deploy/personal-node/node.sh @@ -97,6 +97,10 @@ case "${1:-}" in compose up -d health ;; + restart) + compose restart node + health + ;; stop) compose stop ;; From 18fadbbe6541874bd295dde6822e4570705c526f Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:53:50 +0000 Subject: [PATCH 08/22] test: isolate dstack persistence gate (TC-1282) --- .github/workflows/personal-node.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/personal-node.yml b/.github/workflows/personal-node.yml index 0dcd7cff..07b3e773 100644 --- a/.github/workflows/personal-node.yml +++ b/.github/workflows/personal-node.yml @@ -37,5 +37,18 @@ jobs: run: npm run typecheck --prefix test/probe - name: Exercise the published personal-node guide run: bash deploy/personal-node/ci.sh + + dstack-profile: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: test/probe/package-lock.json + - name: Install pinned signing client + run: npm ci --prefix test/probe - name: Check dstack-profile local-data persistence run: bash deploy/personal-node/dstack-smoke.sh From 9a9bb365e27b67a16be73051b6bcb8b79ef20775 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:55:21 +0000 Subject: [PATCH 09/22] fix: read node-owned data in offline backup (TC-1282) --- deploy/personal-node/node.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/personal-node/node.sh b/deploy/personal-node/node.sh index ce1ff676..13c4e54f 100755 --- a/deploy/personal-node/node.sh +++ b/deploy/personal-node/node.sh @@ -127,9 +127,11 @@ case "${1:-}" in [[ $# == 3 && -f "$3" ]] || fail 'Usage: node.sh backup BACKUP_DIR PASSPHRASE_FILE' [[ ! -e "$2" ]] || fail 'Choose a fresh backup directory' identity + [[ -f "$home/identity" ]] && cmp -s "$home/identity" "$home/current-identity" || + fail 'Back up only after verifying the original node identity' mkdir -m 700 "$2" compose stop node - tar -C "$home/data" -czf "$2/data.tar.gz" . + sudo tar -C "$home/data" -czf - . > "$2/data.tar.gz" openssl enc -aes-256-cbc -pbkdf2 -salt -in "$home/node.env" -out "$2/identity.enc" -pass "file:$3" cp "$home/identity" "$home/image" "$home/domain" "$home/tls-mode" "$2/" compose up -d node From 41de5cd14e7beba701f4d9c4dbc883c117c21808 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:55:21 +0000 Subject: [PATCH 10/22] docs: clarify key continuity and restore cutover (TC-1282) --- README.md | 9 +++++++++ deploy/personal-node/README.md | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4350ab9a..4809f828 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,15 @@ do not replace it during an upgrade: derived node identities and sealed data depend on it. `TINYCLOUD_KEYS__TYPE=Dstack` remains an explicit alternative only for a properly configured dstack TEE. +For an existing node, do **not** generate a new root during upgrade. Recover +the installation's original key from its private backup or its existing +operator configuration, provide that same root explicitly, and compare the +public node identity and encrypted/sealed data before accepting the upgrade. +If the original root is unavailable, stop: a replacement is a different node +identity and does not recover data sealed under the prior root. Changing the +identity also invalidates trust bindings that refer to its DID or public keys. + + ## Configuration diff --git a/deploy/personal-node/README.md b/deploy/personal-node/README.md index 94824f1c..86da6158 100644 --- a/deploy/personal-node/README.md +++ b/deploy/personal-node/README.md @@ -38,15 +38,18 @@ The pinned `@tinycloud/node-sdk` signs in, hosts an owned space, writes and read ## Offline backup and fresh restore -Stop writes before backup. `backup` stops the node, archives the entire `/data` tree while SQLite and SQL actors are closed, encrypts `node.env` as a separate artifact, then restarts the node. The passphrase file is not included in either artifact. Retain both the backup and passphrase in independent secure locations. Test recovery regularly. +Stop writes before backup. `backup` stops the node, archives the entire `/data` tree while SQLite and SQL actors are closed, encrypts `node.env` as a separate artifact, then restarts the node. The data archive itself is **not encrypted**; protect it as sensitive data or encrypt it using your backup system. The passphrase file is not included in either artifact. Retain both the backup and passphrase in independent secure locations. Test recovery regularly. ```bash -# Create a private passphrase file out of band; it must not be committed or logged. +# Generate a strong private passphrase file outside the node and backup tree. export BACKUP_PASSPHRASE_FILE=/secure/path/node-backup-passphrase +umask 077 +openssl rand -base64 48 > "$BACKUP_PASSPHRASE_FILE" bash deploy/personal-node/node.sh backup /secure/path/node-backup-2026-10-16 "$BACKUP_PASSPHRASE_FILE" # On the destination, copy the encrypted backup, passphrase and owner wallet -# by independent secure channels; use a fresh NODE_HOME and the same domain. +# by independent secure channels. Stop the old node, repoint DNS to the new +# host, then start the restored node after Caddy can obtain its certificate. export NODE_HOME="$HOME/personal-tinycloud-restored" bash deploy/personal-node/node.sh restore /secure/path/node-backup-2026-10-16 "$BACKUP_PASSPHRASE_FILE" bash deploy/personal-node/node.sh start @@ -54,7 +57,7 @@ bash deploy/personal-node/node.sh check-version 1.20.6 bash deploy/personal-node/node.sh verify /secure/path/owner.key ``` -Use a fresh host/directory, not two live nodes serving the same identity and domain at once. Repoint DNS only after verifying the new host; Caddy obtains a new certificate. `verify` compares the public node DID saved before backup and the signed KV/SQL data; a different root fails rather than silently rotating. The backup is a full offline snapshot, **not** continuous replication. TLS ACME state is not in the backup, and a restore needs a reachable domain and valid certificate again. +Use a fresh host/directory, not two live nodes serving the same identity and domain at once. Stop the old node before DNS cutover; wait for the new host's Caddy certificate, then verify before admitting clients. If verification fails, stop the destination and revert DNS to the old host. `verify` compares the public node DID saved before backup and the signed KV/SQL data; a different root fails rather than silently rotating. The backup is a full offline snapshot, **not** continuous replication. TLS ACME state is not in the backup, and a restore needs a reachable domain and valid certificate again. ## Upgrade and rollback rehearsal From 39ac258be566d41a689403dc67c9c7ffde071b76 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 17:59:16 +0000 Subject: [PATCH 11/22] fix: pin dstack profile and persist local data at runtime path --- docker-compose.dstack.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docker-compose.dstack.yaml b/docker-compose.dstack.yaml index 437526b1..3ab32aa6 100644 --- a/docker-compose.dstack.yaml +++ b/docker-compose.dstack.yaml @@ -13,7 +13,7 @@ services: tinycloud: - image: ghcr.io/tinycloudlabs/tinycloud-node:latest + image: ghcr.io/tinycloudlabs/tinycloud-node@sha256:4bdae4fc232d886bb56bfbce4ec62ed4b8fb4e1369a01f6afb73d06dd68c188f ports: # Port 8001 (Prometheus /metrics) is intentionally NOT published: the # metrics handler is unauthenticated and serves every series to anyone, @@ -22,12 +22,10 @@ services: - "8000:8000" volumes: - /var/run/dstack.sock:/var/run/dstack.sock - - tinycloud-data:/app/data + - tinycloud-data:/data environment: - TINYCLOUD_KEYS_TYPE: Dstack - TINYCLOUD_STORAGE_DATABASE: "sqlite:./data/caps.db?mode=rwc" - TINYCLOUD_STORAGE_BLOCKS_TYPE: Local - TINYCLOUD_STORAGE_BLOCKS_PATH: ./data/blocks + TINYCLOUD_KEYS__TYPE: Dstack + TINYCLOUD_STORAGE__DATADIR: /data TINYCLOUD_LOG_LEVEL: normal TINYCLOUD_CORS: "true" ROCKET_ADDRESS: "0.0.0.0" From a3dd160917be001ed17d55197a87cae3654947e5 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:01:46 +0000 Subject: [PATCH 12/22] test: prove 1.20.6 legacy key override by node identity --- deploy/personal-node/README.md | 2 ++ deploy/personal-node/dstack-smoke.sh | 12 ++++++++++++ deploy/personal-node/dstack-smoke.yaml | 4 ++-- docker-compose.dstack.yaml | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/deploy/personal-node/README.md b/deploy/personal-node/README.md index 86da6158..3e84c84a 100644 --- a/deploy/personal-node/README.md +++ b/deploy/personal-node/README.md @@ -26,6 +26,8 @@ bash deploy/personal-node/node.sh check-version 1.20.6 Replace `node.example.com` with the domain you own. `init` writes a unique 48-byte random Static root to a `0600` private `node.env`; it is never echoed. The profile mounts a keyless TOML over the bundled image config, so even the older 1.20.6 image **fails** if the installation root is absent. `check-missing-key` must run before `start`; it probes the unprovisioned path without printing keys. Caddy obtains and renews a public certificate; do not bypass its trust verification. +The 1.20.6 config loader accepts both the legacy single-underscore nesting (`TINYCLOUD_KEYS_TYPE`, `TINYCLOUD_STORAGE_DATADIR`) and canonical double-underscore nesting (`TINYCLOUD_KEYS__TYPE`, `TINYCLOUD_STORAGE__DATADIR`). It merges canonical values last when both are set. This profile uses the canonical form; the existing dstack Compose retains its working single-underscore key-type setting. + To prove owner-held EOA operations, place your own wallet's 32-byte hexadecimal private key (without `0x`) in a separate `0600` file outside `NODE_HOME`, then run: ```bash diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh index 8907ae17..a317bb55 100755 --- a/deploy/personal-node/dstack-smoke.sh +++ b/deploy/personal-node/dstack-smoke.sh @@ -26,6 +26,18 @@ ready curl -fsS http://127.0.0.1:8000/version | jq -e '.version == "1.20.6" and (.features | index("tee") != null) and .inTEE == false' >/dev/null || { printf '%s\n' 'Dstack profile did not run the pinned dstack build' >&2; exit 1; } +first_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" +second_key="$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')" +TC1282_TEST_KEY="$second_key" compose up -d --force-recreate tinycloud +ready +second_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" +[[ "$first_did" != "$second_did" ]] || + { printf '%s\n' 'Dstack-profile Static key override did not change node identity' >&2; exit 1; } +compose up -d --force-recreate tinycloud +ready +restored_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" +[[ "$first_did" == "$restored_did" ]] || + { printf '%s\n' 'Dstack-profile original Static key did not restore node identity' >&2; exit 1; } "$here/test/probe/node_modules/.bin/tsx" "$here/test/probe/personal-node-check.ts" seed http://127.0.0.1:8000 "$owner" compose down compose up -d tinycloud diff --git a/deploy/personal-node/dstack-smoke.yaml b/deploy/personal-node/dstack-smoke.yaml index 0bb54b41..605baed6 100644 --- a/deploy/personal-node/dstack-smoke.yaml +++ b/deploy/personal-node/dstack-smoke.yaml @@ -1,8 +1,8 @@ services: tinycloud: environment: - TINYCLOUD_KEYS__TYPE: Static - TINYCLOUD_KEYS__SECRET: ${TC1282_TEST_KEY:?CI test key required} + TINYCLOUD_KEYS_TYPE: Static + TINYCLOUD_KEYS_SECRET: ${TC1282_TEST_KEY:?CI test key required} TINYCLOUD_TEE_MODE: 'off' labels: tc1282: 'true' diff --git a/docker-compose.dstack.yaml b/docker-compose.dstack.yaml index 3ab32aa6..cbf1cc7f 100644 --- a/docker-compose.dstack.yaml +++ b/docker-compose.dstack.yaml @@ -24,7 +24,7 @@ services: - /var/run/dstack.sock:/var/run/dstack.sock - tinycloud-data:/data environment: - TINYCLOUD_KEYS__TYPE: Dstack + TINYCLOUD_KEYS_TYPE: Dstack TINYCLOUD_STORAGE__DATADIR: /data TINYCLOUD_LOG_LEVEL: normal TINYCLOUD_CORS: "true" From 154e065a949cde808607d9831b0414af1575ca09 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:03:16 +0000 Subject: [PATCH 13/22] test: distinguish key-type selection from direct static env secret --- deploy/personal-node/dstack-smoke.sh | 10 ++++++++++ deploy/personal-node/dstack-smoke.yaml | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh index a317bb55..a636b43a 100755 --- a/deploy/personal-node/dstack-smoke.sh +++ b/deploy/personal-node/dstack-smoke.sh @@ -27,6 +27,16 @@ curl -fsS http://127.0.0.1:8000/version | jq -e '.version == "1.20.6" and (.features | index("tee") != null) and .inTEE == false' >/dev/null || { printf '%s\n' 'Dstack profile did not run the pinned dstack build' >&2; exit 1; } first_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" +TC1282_TEST_KEY_TYPE=Provider compose up -d --force-recreate tinycloud +ready +provider_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" +[[ "$first_did" != "$provider_did" ]] || + { printf '%s\n' 'Legacy single-underscore key-type override did not select Provider' >&2; exit 1; } +compose up -d --force-recreate tinycloud +ready +static_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" +[[ "$first_did" == "$static_did" ]] || + { printf '%s\n' 'Legacy single-underscore key-type override did not restore Static' >&2; exit 1; } second_key="$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')" TC1282_TEST_KEY="$second_key" compose up -d --force-recreate tinycloud ready diff --git a/deploy/personal-node/dstack-smoke.yaml b/deploy/personal-node/dstack-smoke.yaml index 605baed6..629cb68d 100644 --- a/deploy/personal-node/dstack-smoke.yaml +++ b/deploy/personal-node/dstack-smoke.yaml @@ -1,8 +1,8 @@ services: tinycloud: environment: - TINYCLOUD_KEYS_TYPE: Static - TINYCLOUD_KEYS_SECRET: ${TC1282_TEST_KEY:?CI test key required} + TINYCLOUD_KEYS_TYPE: ${TC1282_TEST_KEY_TYPE:-Static} + TINYCLOUD_KEYS__SECRET: ${TC1282_TEST_KEY:?CI test key required} TINYCLOUD_TEE_MODE: 'off' labels: tc1282: 'true' From b25e4be72793fedc54f5587fa1cd5b6dfb8b6853 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:08:56 +0000 Subject: [PATCH 14/22] test: show redacted dstack startup errors on CI failure --- deploy/personal-node/dstack-smoke.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh index a636b43a..3c90b593 100755 --- a/deploy/personal-node/dstack-smoke.sh +++ b/deploy/personal-node/dstack-smoke.sh @@ -14,10 +14,18 @@ trap 'compose down -v >/dev/null 2>&1 || true' EXIT ready() { for _ in {1..40}; do - if curl -fsS http://127.0.0.1:8000/healthz -o /dev/null; then return; fi + if curl -fsS http://127.0.0.1:8000/healthz -o /dev/null 2>/dev/null; then return; fi sleep 2 done - printf '%s\n' 'Dstack-profile health check failed' >&2 + printf '%s\n' 'Dstack-profile health check failed; container state and redacted errors:' >&2 + compose ps --all >&2 + compose logs --no-color --tail=100 tinycloud 2>&1 | + while IFS= read -r line; do + case "$line" in + *[Ee]rror*|*failed*|*denied*|*panic*) + printf '%s\n' "${line//"$TC1282_TEST_KEY"/[REDACTED]}" >&2 ;; + esac + done exit 1 } From 6674fb1ba3be8dd5b7ec569856de01906cddbf74 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:13:40 +0000 Subject: [PATCH 15/22] fix: use 1.20.6 TEE mode enum in dstack smoke --- deploy/personal-node/dstack-smoke.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/personal-node/dstack-smoke.yaml b/deploy/personal-node/dstack-smoke.yaml index 629cb68d..bbc19eee 100644 --- a/deploy/personal-node/dstack-smoke.yaml +++ b/deploy/personal-node/dstack-smoke.yaml @@ -3,6 +3,6 @@ services: environment: TINYCLOUD_KEYS_TYPE: ${TC1282_TEST_KEY_TYPE:-Static} TINYCLOUD_KEYS__SECRET: ${TC1282_TEST_KEY:?CI test key required} - TINYCLOUD_TEE_MODE: 'off' + TINYCLOUD_TEE__MODE: 'Off' labels: tc1282: 'true' From 20ee2ca58acddcc7d720f14c5a18d313ce8279c9 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:18:40 +0000 Subject: [PATCH 16/22] test: retain redacted dstack startup cause lines --- deploy/personal-node/dstack-smoke.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh index 3c90b593..4670c83b 100755 --- a/deploy/personal-node/dstack-smoke.sh +++ b/deploy/personal-node/dstack-smoke.sh @@ -17,14 +17,11 @@ ready() { if curl -fsS http://127.0.0.1:8000/healthz -o /dev/null 2>/dev/null; then return; fi sleep 2 done - printf '%s\n' 'Dstack-profile health check failed; container state and redacted errors:' >&2 + printf '%s\n' 'Dstack-profile health check failed; container state and redacted startup log:' >&2 compose ps --all >&2 - compose logs --no-color --tail=100 tinycloud 2>&1 | + compose logs --no-color --tail=35 tinycloud 2>&1 | while IFS= read -r line; do - case "$line" in - *[Ee]rror*|*failed*|*denied*|*panic*) - printf '%s\n' "${line//"$TC1282_TEST_KEY"/[REDACTED]}" >&2 ;; - esac + printf '%s\n' "${line//"$TC1282_TEST_KEY"/[REDACTED]}" >&2 done exit 1 } From c59c67c233303f42d03edbaec284f9cf6ba7d84c Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:23:41 +0000 Subject: [PATCH 17/22] test: prove legacy key type selects KMS without a socket --- deploy/personal-node/dstack-smoke.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/deploy/personal-node/dstack-smoke.sh b/deploy/personal-node/dstack-smoke.sh index 4670c83b..13943821 100755 --- a/deploy/personal-node/dstack-smoke.sh +++ b/deploy/personal-node/dstack-smoke.sh @@ -32,16 +32,21 @@ curl -fsS http://127.0.0.1:8000/version | jq -e '.version == "1.20.6" and (.features | index("tee") != null) and .inTEE == false' >/dev/null || { printf '%s\n' 'Dstack profile did not run the pinned dstack build' >&2; exit 1; } first_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" -TC1282_TEST_KEY_TYPE=Provider compose up -d --force-recreate tinycloud -ready -provider_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" -[[ "$first_did" != "$provider_did" ]] || - { printf '%s\n' 'Legacy single-underscore key-type override did not select Provider' >&2; exit 1; } -compose up -d --force-recreate tinycloud +compose stop tinycloud +# With a valid Static secret still present, selecting Dstack via the legacy +# key-type name must try KMS. The runner has no dstack daemon. +dstack_status=0 +dstack_output="$(TC1282_TEST_KEY_TYPE=Dstack timeout 30 docker compose \ + -f "$here/docker-compose.dstack.yaml" \ + -f "$here/deploy/personal-node/dstack-smoke.yaml" \ + run --rm --no-deps tinycloud 2>&1)" || dstack_status=$? +[[ "$dstack_status" == 1 && "$dstack_output" == *"connecting to dstack socket"* ]] || + { printf '%s\n' 'Legacy single-underscore key-type did not select Dstack KMS' >&2; exit 1; } +compose up -d tinycloud ready static_did="$(curl -fsS http://127.0.0.1:8000/.well-known/tinycloud/node-keys | jq -er '.nodeDid')" [[ "$first_did" == "$static_did" ]] || - { printf '%s\n' 'Legacy single-underscore key-type override did not restore Static' >&2; exit 1; } + { printf '%s\n' 'Static key did not restore the original node identity' >&2; exit 1; } second_key="$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')" TC1282_TEST_KEY="$second_key" compose up -d --force-recreate tinycloud ready From 5254b0d1f24f596f3dc3784f037c7e28cda51be8 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:36:11 +0000 Subject: [PATCH 18/22] test: cover fresh 1.20.6 and reject unrelated key-check failure --- deploy/personal-node/ci.sh | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/deploy/personal-node/ci.sh b/deploy/personal-node/ci.sh index 16ec6fa4..c2a8157e 100755 --- a/deploy/personal-node/ci.sh +++ b/deploy/personal-node/ci.sh @@ -10,7 +10,7 @@ openssl rand -hex 32 > "$owner" openssl rand -base64 48 > "$passphrase" cleanup() { - for name in tc1282-a tc1282-b tc1282-c tc1282-d; do + for name in tc1282-a tc1282-b tc1282-c tc1282-d tc1282-e; do if [[ -f "$work/$name/tls-mode" ]]; then NODE_HOME="$work/$name" "$here/node.sh" destroy-test || true fi @@ -18,6 +18,27 @@ cleanup() { } trap cleanup EXIT +export NODE_HOME="$work/tc1282-e" +"$here/node.sh" init localhost test +"$here/node.sh" check-missing-key +saved_image="$(<"$NODE_HOME/image")" +printf '%s\n' 'not a valid image reference' > "$NODE_HOME/image" +unrelated_status=0 +"$here/node.sh" compose-check >/dev/null 2>&1 || unrelated_status=$? +[[ "$unrelated_status" == 1 ]] || + { printf '%s\n' 'Negative control did not produce an unrelated exit 1' >&2; exit 1; } +if "$here/node.sh" check-missing-key; then + printf '%s\n' 'Missing-key guard accepted an unrelated exit 1' >&2 + exit 1 +fi +printf '%s\n' "$saved_image" > "$NODE_HOME/image" +"$here/node.sh" start +"$here/node.sh" check-version 1.20.6 +"$here/node.sh" seed "$owner" +"$here/node.sh" restart +"$here/node.sh" verify "$owner" +"$here/node.sh" stop + export NODE_HOME="$work/tc1282-a" "$here/node.sh" init localhost test 1.20.5 "$here/node.sh" check-missing-key From d8bc9507d95719358770128c3394e5edd6c26658 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:36:11 +0000 Subject: [PATCH 19/22] fix: require exact missing-secret startup diagnostic --- deploy/personal-node/node.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/deploy/personal-node/node.sh b/deploy/personal-node/node.sh index 13c4e54f..d38f0222 100755 --- a/deploy/personal-node/node.sh +++ b/deploy/personal-node/node.sh @@ -83,11 +83,17 @@ case "${1:-}" in check-missing-key) require_home blank="$(mktemp)" - trap 'rm -f "$blank"' EXIT + diagnostic="$(mktemp)" + trap 'rm -f "$blank" "$diagnostic"' EXIT printf 'TINYCLOUD_KEYS__TYPE=Static\n' > "$blank" status=0 - NODE_KEY_ENV_FILE="$blank" timeout 30 "$0" compose-check >/dev/null 2>&1 || status=$? - [[ "$status" == 1 ]] || fail 'Missing-key check did not fail promptly with a provisioning error' + NODE_KEY_ENV_FILE="$blank" timeout 30 "$0" compose-check >"$diagnostic" 2>&1 || status=$? + missing=0 + while IFS= read -r line; do + if [[ "$line" == ' Missing secret' ]]; then missing=1; fi + done < "$diagnostic" + [[ "$status" == 1 && "$missing" == 1 ]] || + fail 'Missing-key check did not report the exact missing-secret error' printf '%s\n' 'Missing-key startup failed as required' ;; compose-check) From 4f08122a2300d0e4f2917ca420e9fb83402adecd Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:36:11 +0000 Subject: [PATCH 20/22] fix: pin Caddy index digest and document updates --- deploy/personal-node/README.md | 8 +++++--- deploy/personal-node/compose.yaml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/deploy/personal-node/README.md b/deploy/personal-node/README.md index 3e84c84a..645671c1 100644 --- a/deploy/personal-node/README.md +++ b/deploy/personal-node/README.md @@ -1,6 +1,6 @@ # Run a personal TinyCloud node -This ordinary-server profile runs a single node with local SQLite, local blocks, and Caddy HTTPS. It tracks the version TinyCloud itself runs in production: **1.20.6**, pinned by immutable image digest in [`compose.yaml`](compose.yaml). The prior published ordinary image 1.20.5 is used only to rehearse an upgrade and restore-based rollback. Do not use `latest` or 1.20.7: the latter was rolled back in production (TC-1176). New node-image changes remain unavailable to this published-image guide until a separately approved release ships. +This ordinary-server profile runs a single node with local SQLite, local blocks, and Caddy HTTPS. It tracks the version TinyCloud itself runs in production: **1.20.6**, pinned by immutable image digest in [`compose.yaml`](compose.yaml). Caddy **2.10.2** is pinned by its multi-platform image digest there too. The prior published ordinary image 1.20.5 is used only to rehearse an upgrade and restore-based rollback. Do not use `latest` or 1.20.7: the latter was rolled back in production (TC-1176). New node-image changes remain unavailable to this published-image guide until a separately approved release ships. This is **not** a TEE: the VPS operator and its host can access the filesystem and key. No hardware attestation, dstack KMS isolation, or cross-host account migration is claimed. This guide does not configure registry discovery or hosted-to-owned-node replication (TC-1280/TC-1283). TLS authenticates your domain but does not attest the machine. @@ -24,7 +24,7 @@ bash deploy/personal-node/node.sh start bash deploy/personal-node/node.sh check-version 1.20.6 ``` -Replace `node.example.com` with the domain you own. `init` writes a unique 48-byte random Static root to a `0600` private `node.env`; it is never echoed. The profile mounts a keyless TOML over the bundled image config, so even the older 1.20.6 image **fails** if the installation root is absent. `check-missing-key` must run before `start`; it probes the unprovisioned path without printing keys. Caddy obtains and renews a public certificate; do not bypass its trust verification. +Replace `node.example.com` with the domain you own. `init` writes a unique 48-byte random Static root to a `0600` private `node.env`; it is never echoed. The profile mounts a keyless TOML over the bundled image config, so even the older 1.20.6 image **fails** if the installation root is absent. `check-missing-key` must run before `start`; it requires the node's exact `Missing secret` diagnostic and rejects unrelated failures without printing their logs. Caddy obtains and renews a public certificate; do not bypass its trust verification. The 1.20.6 config loader accepts both the legacy single-underscore nesting (`TINYCLOUD_KEYS_TYPE`, `TINYCLOUD_STORAGE_DATADIR`) and canonical double-underscore nesting (`TINYCLOUD_KEYS__TYPE`, `TINYCLOUD_STORAGE__DATADIR`). It merges canonical values last when both are set. This profile uses the canonical form; the existing dstack Compose retains its working single-underscore key-type setting. @@ -63,7 +63,7 @@ Use a fresh host/directory, not two live nodes serving the same identity and dom ## Upgrade and rollback rehearsal -CI executes [`ci.sh`](ci.sh) on a clean runner: 1.20.5 → 1.20.6, with data checks before and after restart, fresh-volume restores, and rollback by restoring the **pre-upgrade** snapshot into another fresh instance on 1.20.5. An in-place downgrade of a migrated database is not supported. A normal new installation starts directly on 1.20.6; do not downgrade it merely to rehearse. +CI executes [`ci.sh`](ci.sh) on a clean runner: a fresh 1.20.6 installation with missing-key denial, an unrelated-failure negative control, signed data and restart; then 1.20.5 → 1.20.6, with data checks before and after restart, fresh-volume restores, and rollback by restoring the **pre-upgrade** snapshot into another fresh instance on 1.20.5. An in-place downgrade of a migrated database is not supported. A normal new installation starts directly on 1.20.6; do not downgrade it merely to rehearse. ```bash # Run only on a disposable isolated runner with permission to create Docker @@ -73,4 +73,6 @@ bash deploy/personal-node/ci.sh Before a future upgrade, take and verify an offline backup as above, stop the node, change the pinned image only to a reviewed released digest, start and verify identity and signed data. If verification fails, stop the candidate and restore that pre-upgrade backup to a fresh instance of the previous image; never blindly start the older binary against migrated data. The next-release upgrade step **requires the next approved production node release** and a new published-image CI run. TC-1176 prohibits promotion of that release until Sam separately clears the hold. Changes to the shipped default key in TC-1200 likewise require that next image; the mounted keyless profile is the safe 1.20.6 workaround, not proof that its bare image fails closed. +For a Caddy update, choose a reviewed release and resolve its published multi-platform index digest (for example, from the [Docker Hub tag metadata](https://hub.docker.com/v2/repositories/library/caddy/tags/2.10.2)). Replace only the `caddy@sha256:…` reference in `compose.yaml`, run the published-image CI job and verify HTTPS certificate issuance and renewal on a staging domain before upgrading a live proxy. Retain the Caddy `/data` and `/config` volumes; do not switch to a mutable tag. + The [personal-node workflow](../../.github/workflows/personal-node.yml) is the acceptance evidence only when its published-image job is green. Record the successful run URL and exact image versions/digests in TC-1282; no CI result here authorizes a production deployment or a stronger sovereignty claim. diff --git a/deploy/personal-node/compose.yaml b/deploy/personal-node/compose.yaml index 60d24f80..f5634001 100644 --- a/deploy/personal-node/compose.yaml +++ b/deploy/personal-node/compose.yaml @@ -17,7 +17,7 @@ services: - no-new-privileges:true https: - image: caddy:2.10.2 + image: caddy@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb restart: unless-stopped depends_on: - node From ac8beca61bf95cd40ecfe050b1bb1b45cb0a0655 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:44:08 +0000 Subject: [PATCH 21/22] test: prove legacy Dstack override with shipped keyless config --- tinycloud-node-server/src/runtime.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tinycloud-node-server/src/runtime.rs b/tinycloud-node-server/src/runtime.rs index 2fa0bfca..7f3ecc0c 100644 --- a/tinycloud-node-server/src/runtime.rs +++ b/tinycloud-node-server/src/runtime.rs @@ -774,6 +774,23 @@ mod tests { assert_eq!(config.keys, config::Keys::Dstack); } + #[cfg(feature = "dstack")] + #[::core::prelude::v1::test] + fn shipped_config_allows_legacy_dstack_without_a_static_key() { + let _lock = env_lock(); + let temp = tempdir().unwrap(); + fs::write( + temp.path().join("tinycloud.toml"), + include_str!("../../tinycloud.toml"), + ) + .unwrap(); + let _cwd = CwdGuard::set(temp.path()); + let _keys_env = clear_keys_env(); + let _dstack = EnvGuard::set("TINYCLOUD_KEYS_TYPE", "Dstack"); + let config = legacy_config_figment().extract::().unwrap(); + assert_eq!(config.keys, config::Keys::Dstack); + } + #[cfg(feature = "dstack")] #[::core::prelude::v1::test] fn serve_config_figment_loads_keys_type_from_env() { From 1746224a0ffcf5f481ecc11394ce826f1661aa16 Mon Sep 17 00:00:00 2001 From: Sam Gbafa Date: Fri, 16 Oct 2026 18:45:47 +0000 Subject: [PATCH 22/22] ci: verify shipped Dstack env in source-built tests --- .github/workflows/personal-node.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/personal-node.yml b/.github/workflows/personal-node.yml index 07b3e773..655c0e35 100644 --- a/.github/workflows/personal-node.yml +++ b/.github/workflows/personal-node.yml @@ -52,3 +52,16 @@ jobs: run: npm ci --prefix test/probe - name: Check dstack-profile local-data persistence run: bash deploy/personal-node/dstack-smoke.sh + + dstack-config-source: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.97.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 + with: + shared-key: rust-1.97.1-dstack-config + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Check shipped keyless config with both Dstack environment forms + run: cargo test --locked -p tinycloud-node --features dstack shipped_config_allows_ --lib