Add Gemma license gate and publish local model assets
Build and publish static assets / publish (push) Failing after 2s

This commit is contained in:
Ein Anderssono
2026-10-19 12:02:45 +02:00
parent 440652214c
commit 73c0c0b8ba
16 changed files with 699 additions and 7 deletions
+39
View File
@@ -1,5 +1,7 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
test('Piper asset manifest pins every Swedish voice and required ONNX loader files', async () => {
const {assetPrefix, voiceFiles, requiredOnnxFiles} = await import('../scripts/assets.mjs');
@@ -9,3 +11,40 @@ test('Piper asset manifest pins every Swedish voice and required ONNX loader fil
assert.ok(requiredOnnxFiles.includes('ort-wasm-simd-threaded.mjs'));
assert.ok(requiredOnnxFiles.includes('ort-wasm-simd-threaded.wasm'));
});
test('browser local AI prompt declares non-authoritative, bounded assistance tasks', async () => {
const prompt = fs.readFileSync(path.join(__dirname, '..', 'prompts', 'browser-assistance.yaml'), 'utf8');
for (const required of ['id: browser-assistance', 'version: 1', 'task:', 'trust: untrusted-data', 'intent_hint', 'preflight', 'continuity', 'offline_help', 'excerpts_max_items: 3']) {
assert.ok(prompt.includes(required), `missing ${required}`);
}
});
test('local AI runtime and model are published only under the owned static origin', async () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'build-assets.mjs'), 'utf8');
assert.ok(source.includes('wllama-gemma3-v1'));
assert.ok(source.includes('wllama-3.8.1.tgz'));
assert.ok(source.includes('gemma-3-1b-it-Q4_K_M.gguf'));
assert.ok(source.includes('manifest.json'));
});
test('Gemma terms, prohibited-use policy, notice, provenance, and runtime license are shipped', () => {
const root = path.join(__dirname, '..', 'licenses');
for (const name of ['GEMMA_USER_TERMS.html', 'GEMMA_TERMS.html', 'GEMMA_PROHIBITED_USE_POLICY.html', 'GEMMA_NOTICE.txt', 'GEMMA_SOURCE.txt', 'LLAMA_CPP_LICENSE.txt']) {
assert.ok(fs.statSync(path.join(root, name)).size > 0, `missing ${name}`);
}
const script = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'build-assets.mjs'), 'utf8');
for (const name of ['GEMMA_USER_TERMS.html', 'GEMMA_TERMS.html', 'GEMMA_PROHIBITED_USE_POLICY.html', 'GEMMA_NOTICE.txt', 'GEMMA_SOURCE.txt', 'WLLAMA_LICENSE.txt', 'LLAMA_CPP_LICENSE.txt']) {
assert.ok(script.includes(name), `build does not publish ${name}`);
}
assert.match(script, /acceptance_id: acceptanceId/);
assert.match(script, /terms_sha256: termsSha256/);
assert.match(script, /policy_sha256: policySha256/);
assert.match(script, /user_terms_sha256: userTermsSha256/);
const terms = fs.readFileSync(path.join(root, 'GEMMA_TERMS.html'), 'utf8');
assert.match(terms, /3\.1 Distribution and Redistribution/);
assert.match(terms, /must include the use restrictions/);
assert.doesNotMatch(terms, /<script|google-signin|google-analytics/);
const nginx = fs.readFileSync(path.join(__dirname, '..', 'nginx', 'default.conf'), 'utf8');
assert.match(nginx, /location = \/static\/wllama-gemma3-v1\/manifest\.json[\s\S]*?Cache-Control "no-cache"/);
assert.match(nginx, /location = \/static\/wllama-gemma3-v1\/local-ai\.js[\s\S]*?Cache-Control "no-cache"/);
});
+41
View File
@@ -0,0 +1,41 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const source = fs.readFileSync(path.join(__dirname, '..', 'client', 'local-ai.js'), 'utf8');
test('local AI runtime rejects assets outside the same-origin static service', () => {
assert.match(source, /parsed\.origin !== location\.origin/);
assert.match(source, /parsed\.pathname\.startsWith\('\/static\/'\)/);
assert.match(source, /\/static\/wllama-gemma3-v1\//);
assert.doesNotMatch(source, /huggingface\.co|registry\.npmjs\.org/);
});
test('local assistance includes preflight, bounded continuity, and unsaved offline guidance', () => {
assert.match(source, /async function assist\(/);
assert.match(source, /async function offline\(/);
assert.match(source, /slice\(-12\)/);
assert.match(source, /Lokalt, osparat förslag/);
});
test('local diagnostics are opt-in, stored in IndexedDB, exportable, and deletable', () => {
assert.match(source, /ai-spel-local-ai-logs/);
assert.match(source, /indexedDB\.open/);
assert.match(source, /lokal ai exportera loggar/);
assert.match(source, /lokal ai radera loggar/);
assert.match(source, /30 \* 24 \* 60 \* 60 \* 1000/);
});
test('Gemma model use is blocked until per-account acceptance of the exact terms revision', () => {
assert.match(source, /async function start\(currentAccountId\)/);
assert.match(source, /localStorage\.getItem\(key\) !== manifest\.license\.acceptance_id/);
assert.match(source, /lokal ai godkänn villkor/);
assert.match(source, /lokal ai avvisa villkor/);
assert.match(source, /rättslig behörighet att ingå avtalet/);
assert.match(source, /termsPresentedForAccount !== accountId/);
assert.match(source, /localStorage\.setItem\(acceptanceKey\(manifest\), manifest\.license\.acceptance_id\)/);
assert.match(source, /async function assist\([\s\S]*?!await isAccepted\(\)/);
assert.match(source, /async function offline\([\s\S]*?!await isAccepted\(\)/);
assert.match(source, /function stop\(\)/);
});