diff --git a/.memory/worklog.json b/.memory/worklog.json index 5434652..b4f4f62 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -132,6 +132,13 @@ "message": "auto-save 2026-04-19 18:14 (~1)", "hash": "867ca6f", "files_changed": 1 + }, + { + "ts": "2026-04-19T20:27:26+08:00", + "type": "commit", + "message": "auto-save 2026-04-19 20:15 (~1)", + "hash": "eea4230", + "files_changed": 1 } ] } diff --git a/images/base/build.sh b/images/base/build.sh index d66b907..235623e 100644 --- a/images/base/build.sh +++ b/images/base/build.sh @@ -54,8 +54,8 @@ incus exec "$BUILDER" --project "$PROJECT" -- bash -c ' log "Installing uv" incus exec "$BUILDER" --project "$PROJECT" -- bash -c ' - curl -LsSf https://astral.sh/uv/install.sh | env INSTALL_DIR=/usr/local/bin UV_UNMANAGED_INSTALL=1 sh - ls -la /usr/local/bin/uv + curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --no-modify-path --install-dir /usr/local/bin + ls -la /usr/local/bin/uv /usr/local/bin/uvx ' log "Installing bun" diff --git a/images/base/patch-uv.sh b/images/base/patch-uv.sh new file mode 100644 index 0000000..0d3e80a --- /dev/null +++ b/images/base/patch-uv.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Patch existing lobe-sandbox-base image with correctly-installed uv. +# One-shot fix for the UV_INSTALL_DIR mistake in initial build. +set -euo pipefail + +PROJECT="lobe-sandbox" +BUILDER="sb-patch" +IMAGE="lobe-sandbox-base" +log() { echo "==> [$(date +%H:%M:%S)] $*"; } + +incus info "$BUILDER" --project "$PROJECT" >/dev/null 2>&1 && \ + incus delete "$BUILDER" --project "$PROJECT" --force + +log "Launch from base" +incus init "$IMAGE" "$BUILDER" --project "$PROJECT" -p sandbox-default +incus start "$BUILDER" --project "$PROJECT" +sleep 3 + +log "Install uv via UV_INSTALL_DIR (correct env var)" +incus exec "$BUILDER" --project "$PROJECT" -- bash -c ' + rm -f /usr/local/bin/uv /usr/local/bin/uvx /root/.local/bin/uv /root/.local/bin/uvx 2>/dev/null || true + curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh + ls -la /usr/local/bin/uv /usr/local/bin/uvx +' + +log "Verify uv as root AND as sandbox user" +incus exec "$BUILDER" --project "$PROJECT" -- uv --version +incus exec "$BUILDER" --project "$PROJECT" --user 1000 -- uv --version + +log "Realistic LLM flow: create venv, install requests" +incus exec "$BUILDER" --project "$PROJECT" --user 1000 --cwd /workspace -- bash -c ' + uv venv .venv --seed 2>&1 | tail -3 + uv pip install --python .venv/bin/python requests 2>&1 | tail -3 + .venv/bin/python -c "import requests; print(\"requests\", requests.__version__)" +' + +log "Cleanup cache (shrink image)" +incus exec "$BUILDER" --project "$PROJECT" --user 1000 -- rm -rf /home/sandbox/.cache /workspace/.venv +incus exec "$BUILDER" --project "$PROJECT" -- bash -c "rm -rf /root/.cache /tmp/* /var/tmp/*" + +log "Stop builder" +incus stop "$BUILDER" --project "$PROJECT" + +log "Replace image alias" +incus image delete "$IMAGE" --project "$PROJECT" || true +incus publish "$BUILDER" --project "$PROJECT" --alias "$IMAGE" + +log "Cleanup patch container" +incus delete "$BUILDER" --project "$PROJECT" + +log "Final" +incus image list --project "$PROJECT" +log "DONE" diff --git a/orchestrator/src/auth.ts b/orchestrator/src/auth.ts new file mode 100644 index 0000000..0c25bbb --- /dev/null +++ b/orchestrator/src/auth.ts @@ -0,0 +1,18 @@ +import type { MiddlewareHandler } from 'hono'; +import { env } from './env.ts'; + +// 常量时间比较,防 timing attack +const safeEqual = (a: string, b: string): boolean => { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); + return diff === 0; +}; + +export const authMiddleware: MiddlewareHandler = async (c, next) => { + const header = c.req.header('X-Sandbox-Secret'); + if (!header || !safeEqual(header, env.orchSecret)) { + return c.json({ error: 'unauthorized' }, 401); + } + await next(); +}; diff --git a/orchestrator/src/state.ts b/orchestrator/src/state.ts new file mode 100644 index 0000000..0d2d08f --- /dev/null +++ b/orchestrator/src/state.ts @@ -0,0 +1,59 @@ +import { Database } from 'bun:sqlite'; +import { mkdir } from 'node:fs/promises'; +import { dirname } from 'node:path'; +import { env } from './env.ts'; + +await mkdir(dirname(env.stateDbPath), { recursive: true }); +const db = new Database(env.stateDbPath, { create: true }); +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY, + created_at INTEGER NOT NULL, + provisioned_at INTEGER, + deleted_at INTEGER + ); + CREATE TABLE IF NOT EXISTS activity ( + user_id TEXT PRIMARY KEY, + last_used INTEGER NOT NULL + ); +`); + +const now = () => Date.now(); + +export const state = { + recordCreate: (userId: string): void => { + db.prepare( + `INSERT INTO users (user_id, created_at) VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET deleted_at = NULL`, + ).run(userId, now()); + }, + + markProvisioned: (userId: string): void => { + db.prepare(`UPDATE users SET provisioned_at = ? WHERE user_id = ?`).run(now(), userId); + }, + + markDeleted: (userId: string): void => { + db.prepare(`UPDATE users SET deleted_at = ? WHERE user_id = ?`).run(now(), userId); + db.prepare(`DELETE FROM activity WHERE user_id = ?`).run(userId); + }, + + touch: (userId: string): void => { + db.prepare( + `INSERT INTO activity (user_id, last_used) VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET last_used = excluded.last_used`, + ).run(userId, now()); + }, + + // 查找过期:最后使用时间距今 > timeoutMs + findIdle: (timeoutMs: number): string[] => { + const cutoff = now() - timeoutMs; + return db + .prepare(`SELECT user_id FROM activity WHERE last_used < ?`) + .all(cutoff) + .map((r: any) => r.user_id); + }, + + clearActivity: (userId: string): void => { + db.prepare(`DELETE FROM activity WHERE user_id = ?`).run(userId); + }, +};