WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
const assert = require('node:assert/strict')
const test = require('node:test')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const {cleanupMemoryFixture, memoryTables} = require('./verify_resource_isolation_live.cjs')
const {validateResponse, trackCreatedResource, finishVerification} = require('./verify_access_control.cjs')
const {cleanupMessageFixture} = require('./verify_resource_files_live.cjs')
const prefix = 'resource-isolation-1758297600000'
const fixture = Object.freeze({
id: '5b4af2da-04eb-4e76-b54d-cec380559428',
username: `${prefix}-alice`,
email: `${prefix}-alice@example.test`,
})
test('memory cleanup restricts all six tables to the newly created account and verifies zero remaining rows', () => {
const statements = []
const subject = cleanupMemoryFixture(fixture, prefix, statement => {
statements.push(statement)
return statements.length === 1 ? '' : '0|0|0|0|0|0'
})
assert.equal(subject, `web_user:${fixture.id}`)
assert.equal(statements.length, 2)
assert.match(statements[0], /^BEGIN;/)
assert.match(statements[0], /IF NOT EXISTS \(SELECT 1 FROM users/)
for (const field of ['id', 'username', 'email']) assert.ok(statements[0].includes(`${field}='${fixture[field]}'`))
assert.ok(statements[0].indexOf('RAISE EXCEPTION') < statements[0].indexOf('DELETE FROM'))
assert.match(statements[0], /END; \$fixture_cleanup\$;/)
assert.equal((statements[0].match(/DELETE FROM /g) || []).length, 6)
for (const table of ['memory_item_embeddings', 'memory_items', 'memory_tombstones', 'memory_topic_stats', 'memory_doc_affinity', 'memory_subjects']) {
const scoped = `${table} WHERE tenant_id=10059 AND subject_id='web_user:${fixture.id}'`
assert.ok(statements[0].includes(`DELETE FROM ${scoped};`))
assert.ok(statements[1].includes(`SELECT count(*) FROM ${scoped}`))
}
assert.match(statements[0], /COMMIT;$/)
assert.equal(memoryTables.length, 6)
})
test('memory cleanup rejects business accounts, other runs, malformed IDs and mismatched email before any SQL', () => {
const otherPrefix = 'resource-isolation-1758297500000'
const rejected = [
[{...fixture, username: 'admin'}, prefix],
[{...fixture, email: 'business@example.com'}, prefix],
[{...fixture, id: "x'; DELETE FROM users; --"}, prefix],
[fixture, otherPrefix],
[fixture, 'resource-isolation-%'],
]
for (const [value, runPrefix] of rejected) {
let calls = 0
assert.throws(() => cleanupMemoryFixture(value, runPrefix, () => { calls++ }))
assert.equal(calls, 0)
}
})
test('memory cleanup surfaces owner guard errors and residual rows as failures', () => {
const statements = []
assert.throws(() => cleanupMemoryFixture(fixture, prefix, statement => {
statements.push(statement)
throw new Error('Memory cleanup account does not match this verification run')
}), /does not match/)
assert.equal(statements.length, 1)
let calls = 0
assert.throws(() => cleanupMemoryFixture(fixture, prefix, () => ++calls === 1 ? '' : '0|1|0|0|0|0'), /left rows/)
assert.equal(calls, 2)
})
test('browser shutdown and cleanup failures still allow every cleanup action and a failed report', async () => {
const events = [], failures = [], cleanup = [
async () => { events.push('first') },
async () => { events.push('second'); throw new Error('delete unavailable') },
async () => { events.push('third') },
]
let report
await finishVerification({
browser: {close: async () => { events.push('close'); throw new Error('browser disconnected') }},
cleanup, failures,
writeReport: () => { events.push('report'); report = {passed: failures.length === 0, failures: [...failures]} },
})
assert.deepEqual(events, ['close', 'third', 'second', 'first', 'report'])
assert.equal(cleanup.length, 3)
assert.deepEqual(report, {passed: false, failures: ['browser close: browser disconnected', 'cleanup: delete unavailable']})
})
test('failure before browser startup still cleans fixtures and writes the report', async () => {
const events = [], failures = ['login failed']
await finishVerification({
cleanup: [() => events.push('fixture')], failures,
writeReport: () => events.push(failures.length ? 'failed report' : 'passed report'),
})
assert.deepEqual(events, ['fixture', 'failed report'])
})
test('unexpected successful creation is registered before status assertion, including duplicate and fork envelopes', async () => {
const cases = [
['/access/roles', {id: 'new-role'}, 'new-role', 400],
['/workbench/resources/content-tasks', {id: 'new-task'}, 'new-task', 403],
['/agents', {id: 'new-agent'}, 'new-agent', 403],
['/knowledge-bases', {target_id: 'new-kb'}, 'new-kb', 403],
['/knowledge-bases', {knowledge_base: {id: 'new-nested-kb'}}, 'new-nested-kb', 403],
['/knowledge', {id: 'new-document'}, 'new-document', 403],
['/workbench/resources/flows', {id: 'fork-metadata', personal_id: 'new-flow'}, 'new-flow', 410],
]
for (const [collection, data, expectedID, expectedStatus] of cases) {
const cleanup = [], removed = [], statements = []
const track = trackCreatedResource(collection, ['original-resource'], cleanup, route => removed.push(route), statement => statements.push(statement))
assert.throws(() => validateResponse(collection, 'POST', {data}, 201, expectedStatus, track), /HTTP 201/)
assert.equal(cleanup.length, collection==='/workbench/resources/flows' ? 2 : 1)
await finishVerification({cleanup, failures: [], writeReport: () => {}})
assert.deepEqual(removed, [`${collection}/${expectedID}?detach=true`])
if(collection==='/workbench/resources/flows'){
assert.equal(statements.length, 1)
assert.ok(statements[0].includes("WHERE tenant_id=10059 AND kind='flows' AND id='fork-metadata' AND personal_id='new-flow'"))
assert.ok(statements[0].includes("WHERE tenant_id=10059 AND kind='flows' AND resource_id='new-flow'"))
}else assert.deepEqual(statements, [])
}
})
test('expected denial never registers an existing resource from an error response', () => {
const cleanup = []
validateResponse('/agents/original/copy', 'POST', {data: {id: 'original'}}, 403, [403,404], trackCreatedResource('/agents', [], cleanup))
assert.equal(cleanup.length, 0)
})
test('unexpected creation refuses to delete the original or fork metadata instead of the created resource', () => {
for (const [collection, result] of [
['/agents', {data: {id: 'original'}}],
['/knowledge-bases', {data: {target_id: 'original'}}],
['/workbench/resources/flows', {data: {id: 'fork-metadata'}}],
]) {
const cleanup = []
assert.throws(() => validateResponse(collection, 'POST', result, 200, 403, trackCreatedResource(collection, ['original'], cleanup)), /new resource ID/)
assert.equal(cleanup.length, 0)
}
})
test('resource isolation main cleans both fixture memory scopes after a failure following memory creation', {timeout: 3000}, async () => {
const events = [], userIDs = [fixture.id, 'fcf8b9eb-2af9-4267-9f8c-b288849f8310']
let createdUsers = 0, createdAgents = 0, memoryCreated = false, complete
const reported = new Promise(resolve => { complete = resolve })
const verificationModule = {exports: {}}
const fakeFS = {
readFileSync: () => JSON.stringify({email: 'fake-admin@example.test', password: 'fake-test-password'}),
writeFileSync: (_file, body) => complete(JSON.parse(body)),
}
const fakeRequire = name => {
if(name==='node:fs')return fakeFS
if(name==='node:child_process')return {execFileSync: (_command, args) => {
const statement = args.at(-1)
events.push({type: 'sql', statement})
return statement.startsWith('SELECT ') ? '0|0|0|0|0|0' : ''
}}
return require(name)
}
fakeRequire.main = verificationModule
const context = vm.createContext({
module: verificationModule, require: fakeRequire,
process: {env: {}}, console: {log() {}, error() {}},
FormData, Blob, AbortSignal,
fetch: async (url, request) => {
const route = new URL(url).pathname.replace('/api/v1', '')
const method = request.method, body = request.body instanceof FormData ? {} : JSON.parse(request.body || '{}')
events.push({type: 'http', route, method})
let status = 200, data = {}
if(route==='/auth/login'){
const index = body.email.includes('-alice@') ? 0 : body.email.includes('-bob@') ? 1 : -1
data = {token: 'fake-token', user: {id: index<0 ? 'fake-admin' : userIDs[index]}}
}else if(route==='/access/roles'&&method==='POST')data = {data: {id: 'fake-role', version: 1}}
else if(route==='/access/users'&&method==='POST')data = {data: {...body, id: userIDs[createdUsers++]}}
else if(route==='/knowledge-bases'&&method==='POST')data = {data: {id: 'fake-kb', creator_id: userIDs[0]}}
else if(route==='/knowledge-bases'&&method==='GET')data = {data: []}
else if(route==='/agents'&&method==='POST')data = {data: {id: `fake-agent-${++createdAgents}`}}
else if(route.endsWith('/knowledge/manual'))data = {data: {id: 'fake-doc'}}
else if(route.includes('/suggested-questions'))data = {data: []}
else if(route==='/skills/instructions/import')data = {data: {id: 'fake-skill'}}
else if(route==='/memory/items'&&method==='POST'){
memoryCreated = true
data = {data: {id: 'fake-memory'}}
}else if(route==='/memory/items'&&method==='GET')throw new Error('injected failure after memory creation')
else if(method!=='DELETE'&&!route.startsWith('/access/'))status = 403
return {status, text: async () => JSON.stringify(data)}
},
})
vm.runInContext(fs.readFileSync(path.join(__dirname, 'verify_resource_isolation_live.cjs'), 'utf8'), context)
const report = await reported
assert.ok(memoryCreated)
assert.equal(report.passed, false)
assert.ok(report.failures.includes('injected failure after memory creation'))
assert.deepEqual(report.fixtures.memory_subjects, userIDs.map(id => `web_user:${id}`))
for(const id of userIDs){
const cleanupIndex = events.findIndex(event => event.type==='sql' && event.statement.includes('DELETE FROM memory_items') && event.statement.includes(`subject_id='web_user:${id}'`))
const userDeleteIndex = events.findIndex(event => event.type==='http' && event.route===`/access/users/${id}` && event.method==='DELETE')
assert.ok(cleanupIndex>=0&&userDeleteIndex>cleanupIndex)
assert.ok(report.checks.some(check => check.name===`memory_cleanup_${id}`&&check.passed))
}
assert.ok(!events.some(event => event.type==='sql'&&event.statement.includes("subject_id='web_user:fake-admin'")))
})
const filePrefix = 'file-isolation-1758297600000'
const messageFixture = Object.freeze({
tenant_id: 10059,
session_id: 'a75ddc61-81e2-42f1-a870-656aac0a0931',
user_id: fixture.id,
username: `${filePrefix}-alice`,
email: `${filePrefix}-alice@example.test`,
title: filePrefix,
messages: [
{id: '40c638f6-eafc-4305-85a6-993d4c4250e5', content: 'Disposable attachment fixture.'},
{id: '9a38599e-4d21-485b-8bfc-8bf24fa19b3f', content: 'Unrelated message.'},
],
})
test('message cleanup guards exact tenant, account, session and both synthetic message contents', () => {
const statements = []
cleanupMessageFixture(messageFixture, filePrefix, statement => {
statements.push(statement)
return statements.length === 1 ? '' : '0'
})
assert.equal(statements.length, 2)
const transaction = statements[0]
assert.match(transaction, /^BEGIN;/)
assert.ok(transaction.includes(`SELECT 1 FROM users WHERE id='${messageFixture.user_id}' AND tenant_id=10059 AND username='${messageFixture.username}' AND email='${messageFixture.email}'`))
const sessionScope = `id='${messageFixture.session_id}' AND tenant_id=10059 AND user_id='${messageFixture.user_id}' AND title='${filePrefix}'`
assert.ok(transaction.includes(`IF NOT EXISTS (SELECT 1 FROM sessions WHERE ${sessionScope})`))
for (const message of messageFixture.messages) {
const scope = `id='${message.id}' AND session_id='${messageFixture.session_id}' AND role='assistant' AND content='${message.content}'`
assert.ok(transaction.includes(`IF EXISTS (SELECT 1 FROM messages WHERE id='${message.id}') AND NOT EXISTS (SELECT 1 FROM messages WHERE ${scope})`))
assert.ok(transaction.includes(`DELETE FROM messages WHERE ${scope} AND EXISTS (SELECT 1 FROM sessions WHERE ${sessionScope});`))
assert.ok(statements[1].includes(`'${message.id}'`))
}
assert.ok(transaction.lastIndexOf('RAISE EXCEPTION') < transaction.indexOf('DELETE FROM'))
assert.equal((transaction.match(/DELETE FROM /g) || []).length, 2)
assert.match(transaction, /END; \$message_cleanup\$;/)
assert.match(transaction, /COMMIT;$/)
})
test('message cleanup rejects business or mismatched fixture metadata without SQL', () => {
const rejected = [
[{...messageFixture, tenant_id: 10060}, filePrefix],
[{...messageFixture, username: 'admin'}, filePrefix],
[{...messageFixture, email: 'business@example.com'}, filePrefix],
[{...messageFixture, title: 'Business conversation'}, filePrefix],
[{...messageFixture, session_id: "x'; DELETE FROM sessions; --"}, filePrefix],
[{...messageFixture, user_id: 'admin'}, filePrefix],
[{...messageFixture, messages: [{...messageFixture.messages[0], content: 'Real customer message'}, messageFixture.messages[1]]}, filePrefix],
[{...messageFixture, messages: [messageFixture.messages[0], {...messageFixture.messages[1], id: messageFixture.messages[0].id}]}, filePrefix],
[{...messageFixture, messages: [messageFixture.messages[0]]}, filePrefix],
[messageFixture, 'file-isolation-1758297500000'],
[messageFixture, 'file-isolation-%'],
]
for (const [value, runPrefix] of rejected) {
let calls = 0
assert.throws(() => cleanupMessageFixture(value, runPrefix, () => { calls++ }))
assert.equal(calls, 0)
}
})
test('message cleanup propagates database guard failures and checks residual rows', () => {
let calls = 0
assert.throws(() => cleanupMessageFixture(messageFixture, filePrefix, () => {
calls++
throw new Error('Message cleanup session does not match this verification run')
}), /does not match/)
assert.equal(calls, 1)
calls = 0
assert.throws(() => cleanupMessageFixture(messageFixture, filePrefix, () => ++calls === 1 ? '' : '1'), /left rows/)
assert.equal(calls, 2)
})
for (const failurePoint of ['message insert', 'message file check']) test(`file isolation cleans messages and reports exact fixture IDs after ${failurePoint} failure`, {timeout: 3000}, async () => {
const events = [], userIDs = [fixture.id, 'fcf8b9eb-2af9-4267-9f8c-b288849f8310']
let createdUsers = 0, createdKBs = 0, grant = false, observedPrefix, complete
const reported = new Promise(resolve => { complete = resolve })
const verificationModule = {exports: {}}
const resourceFiles = new Map(), physicalFiles = new Map()
const fakeFS = {
readFileSync: () => JSON.stringify({email: 'fake-admin@example.test', password: 'fake-test-password'}),
mkdirSync() {},
rmSync: file => events.push({type: 'rm', file}),
writeFileSync: (file, body) => {
if (file.endsWith('.json')) complete(JSON.parse(body))
else events.push({type: 'file', file, body})
},
}
const fakeRequire = name => {
if (name === 'node:fs') return fakeFS
if (name === 'node:child_process') return {execFileSync: (_command, _args, options) => {
const statement = options.input
events.push({type: 'sql', statement})
if (statement.startsWith('INSERT INTO messages') && failurePoint === 'message insert') throw new Error('injected message insert failure')
if (statement.startsWith('INSERT INTO resources')) {
const values = statement.match(/VALUES \('[^']+','([^']+)',10059,'local','([^']+)'/)
const label = values[2].split('/').at(-1).replace('.txt', '')
const file = {label, marker: `${observedPrefix} ${label}`}
resourceFiles.set(`resource://${values[1]}`, file)
physicalFiles.set(values[2], file)
}
return statement.startsWith('SELECT count(*)') ? '0' : ''
}}
return require(name)
}
fakeRequire.main = verificationModule
const context = vm.createContext({
module: verificationModule, require: fakeRequire,
process: {env: {}}, console: {log() {}, error() {}}, AbortSignal, Buffer,
fetch: async (url, request) => {
const parsed = new URL(url), route = parsed.pathname.replace('/api/v1', '')
const method = request.method, body = JSON.parse(request.body || '{}')
const identity = request.headers.Authorization?.replace('Bearer ', '')
events.push({type: 'http', route, method})
let status = 200, data = {}, responseText
if (route === '/auth/login') {
const index = body.email.includes('-alice@') ? 0 : body.email.includes('-bob@') ? 1 : -1
data = {token: index < 0 ? 'admin' : index === 0 ? 'alice' : 'bob', user: {id: index < 0 ? 'fake-admin' : userIDs[index]}}
} else if (route === '/access/roles' && method === 'POST') {
observedPrefix = body.name
data = {data: {id: 'fake-role'}}
} else if (route === '/access/users' && method === 'POST') data = {data: {...body, id: userIDs[createdUsers++]}}
else if (route === '/knowledge-bases' && method === 'POST') data = {data: {id: `fake-kb-${++createdKBs}`}}
else if (route.endsWith('/knowledge/manual')) data = {data: {id: 'fake-doc'}}
else if (route === '/access/grants') grant = body.action === 'grant'
else if (route === '/sessions' && method === 'POST') data = {data: {id: messageFixture.session_id}}
else if (parsed.searchParams.has('file_path')) {
const ref = parsed.searchParams.get('file_path')
const file = resourceFiles.get(ref) || physicalFiles.get(ref)
assert.ok(file)
if (file.label === 'message' && failurePoint === 'message file check') throw new Error('injected message file check failure')
const allowed = identity === 'admin' || identity === 'alice' || (identity === 'bob' && grant && file.label === 'knowledge')
status = allowed ? 200 : 403
responseText = allowed ? file.marker : '{}'
} else if (route.startsWith('/sessions') && identity === 'bob') status = 404
else if (method === 'DELETE') { status = 204; responseText = '' }
return {status, text: async () => responseText ?? JSON.stringify(data), headers: {get: () => 'private, no-store'}}
},
})
vm.runInContext(fs.readFileSync(path.join(__dirname, 'verify_resource_files_live.cjs'), 'utf8'), context)
const report = await reported
assert.equal(report.passed, false)
assert.ok(report.failures.includes(`injected ${failurePoint} failure`), JSON.stringify(report.failures))
assert.deepEqual(report.fixtures.users, userIDs)
assert.deepEqual(report.fixtures.sessions, [messageFixture.session_id])
assert.equal(report.fixtures.messages.length, 2)
assert.equal(report.fixtures.message_scopes.length, 1)
assert.equal(report.fixtures.message_scopes[0].session_id, messageFixture.session_id)
assert.equal(report.fixtures.message_scopes[0].user_id, userIDs[0])
assert.deepEqual(report.fixtures.message_scopes[0].messages.map(message => message.id), report.fixtures.messages)
const cleanupIndex = events.findIndex(event => event.type === 'sql' && event.statement.includes('DELETE FROM messages'))
const sessionDeleteIndex = events.findIndex(event => event.type === 'http' && event.method === 'DELETE' && event.route === `/sessions/${messageFixture.session_id}` && events.indexOf(event) > cleanupIndex)
const userDeleteIndex = events.findIndex(event => event.type === 'http' && event.method === 'DELETE' && event.route === `/access/users/${userIDs[0]}`)
assert.ok(cleanupIndex >= 0 && sessionDeleteIndex > cleanupIndex && userDeleteIndex > sessionDeleteIndex)
const statement = events[cleanupIndex].statement
for (const id of report.fixtures.messages) assert.ok(statement.includes(`id='${id}'`))
assert.ok(report.checks.some(check => check.name === `message_cleanup_${messageFixture.session_id}` && check.passed))
})