From b57c4c075ff2f5c3ab1a74f146a9dd088f27c2a5 Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Mon, 19 Oct 2026 12:03:51 +0200 Subject: [PATCH] Gate local Gemma behind explicit terms acceptance --- AGENTS.md | 2 ++ README.md | 2 +- index.html | 54 +++++++++++++++++++++++++++-- piper-worker.js | 2 ++ scripts/check-frontend.sh | 8 +++++ tests/local-ai-integration.test.cjs | 28 +++++++++++++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/local-ai-integration.test.cjs diff --git a/AGENTS.md b/AGENTS.md index 14d7590..1053a64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,3 +10,5 @@ - Helm charts are published to `harbor.k3s.k0.nu/ai-spel`; Argo CD uses the existing `ai-spel` project and namespace. - Player-facing speech uses the local Piper WebAssembly adapter when the player has activated audio. The separate `ai-spel-static` Nginx service serves pinned Swedish Piper models and ONNX/Piper runtime files from same-origin `/static/piper-wasm-v1/` URLs with immutable caching. Game text is synthesized on-device and must never be sent to a TTS provider. - This pipeline bundles only the Piper worker/client glue. Model/runtime downloads, image builds, and deployment belong to `ai-spel/ai-spel-static` and the existing `large` runner. Never build or run these assets locally. +- Browser local AI loads its client adapter, Wllama runtime, WebAssembly, Gemma model, manifest, YAML prompt, and license resources only from the same-origin `/static/wllama-gemma3-v1/` path owned by `ai-spel/ai-spel-static`. Do not add browser CDN, npm, or model-hub fallbacks. The player must be shown and accept applicable model terms before the model downloads or runs. Acceptance, model failure, or local-AI opt-out must never block play. Incompatible asset changes require a new immutable static path and a coordinated frontend release. +- Local model output is advisory only. Preserve the original player input, send local hints only as untrusted context to Rust, and label offline replies as unsaved. Local diagnostic logging is opt-in, browser-local, exportable/deletable, and expires after 30 days. diff --git a/README.md b/README.md index 774f2a8..ec112be 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ After login, character creation and play use one dialogue history and one text i During play the UI has no persistent location, task, inventory, economy, status, or purchase panels. The player asks for information or writes an action in the same input. Rust validates state changes; the AI can only propose a typed intent. A direct request to travel may establish a new place and an encounter when Rust commits that movement. -The dialogue also offers **Gör om senaste** and **Torrkör senaste**. Both resend the latest player action while leaving the previous dialogue turn out of the AI conversation context. “Gör om” processes a fresh, normal turn. “Torrkör” calls the narrator and evaluates already-approved stored Luau rules, then rolls back the complete game transaction; it does not save the player turn, AI trace, rule, event, ownership, or world progress. A rule that would need to be newly generated is reported as not evaluated, because a dry run never stores newly generated rules. +The dialogue also offers **Gör om senaste** and **Torrkör senaste**. Both resend the latest player action while leaving the previous dialogue turn out of the AI conversation context. “Gör om” processes a fresh, normal turn. “Torrkör” calls the narrator, generates and validates a temporary Luau rule when needed, and evaluates either that temporary rule or an approved stored rule. It rolls back the complete game transaction and does not save the player turn, AI trace, generated rule, event, ownership, or world progress. The reply identifies which rule was evaluated and marks a temporary rule as such. If the player asks to find or approach someone and no NPC is present, the direct dialogue scene may propose one person and an opening line. Rust creates that NPC only for the explicit request and commits the first exchange idempotently; a later turn continues the saved conversation. Clear requests about the current location are answered from its saved facts without asking the player to restate what they want to inspect. diff --git a/index.html b/index.html index e3ee866..7043686 100644 --- a/index.html +++ b/index.html @@ -116,6 +116,7 @@ + ' index.html +grep -q '' index.html grep -q '' index.html grep -q 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0' nginx/default.conf grep -q 'Cross-Origin-Embedder-Policy "require-corp"' nginx/default.conf @@ -46,6 +47,12 @@ grep -q 'WebKit versions before Safari 27' voice.js grep -q "voiceIdentity: speakingNpc.voice_identity" index.html grep -q "role: 'narrator', voiceIdentity: 'neutral'" index.html grep -q 'COPY voice.js /usr/share/nginx/html/voice.js' Dockerfile +grep -Fq 'localGameAI?.start(currentAccount.id)' index.html +grep -q "type === 'terms-required'" index.html +grep -q 'Gemma-villkor' index.html +grep -q 'lokal ai godkänn villkor' index.html +grep -q 'lokal ai avvisa villkor' index.html +grep -q 'client_assistance: clientAssistance' index.html grep -q 'COPY public/piper-runtime.js /usr/share/nginx/html/piper-runtime.js' Dockerfile grep -q 'COPY public/tts/piper-wasm-v1/' Dockerfile grep -q 'build:piper' .gitea/workflows/publish.yaml @@ -55,6 +62,7 @@ grep -q "new Worker(workerUrl, {type: 'module'})" piper-runtime.js grep -q 'worker.js?v=' piper-runtime.js grep -q 'worker.onmessageerror' piper-runtime.js grep -q 'localPiperModelUrl' piper-worker.js +grep -q 'External Piper asset blocked' piper-worker.js grep -q 'trackDownloadProgress' piper-worker.js grep -q 'new ReadableStream' piper-download-progress.mjs grep -q 'createOnnxWasmPaths(self.location.origin)' piper-worker.js diff --git a/tests/local-ai-integration.test.cjs b/tests/local-ai-integration.test.cjs new file mode 100644 index 0000000..3193fad --- /dev/null +++ b/tests/local-ai-integration.test.cjs @@ -0,0 +1,28 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const page = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); + +test('local AI adapter comes from the static service and starts after authenticated game entry', () => { + assert.match(page, /