Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4ac99e0
Merge commit 'f335c01' into codex/oct24-skill-log-integration
BigDataDZ Oct 24, 2026
9c4c067
fix(datasources): atomically persist connection test results
BigDataDZ Oct 24, 2026
771671c
test(datasources): normalize integration fixture formatting
BigDataDZ Oct 24, 2026
562915e
fix(mcp): isolate configuration caches and reject stale reads
BigDataDZ Oct 24, 2026
08229bf
refactor(mcp): tighten extracted config values baseline
BigDataDZ Oct 24, 2026
d4b1066
Merge commit '08229bf7fa23a07e3f614aac4d1a1394a0d7d918' into codex/oc…
BigDataDZ Oct 24, 2026
9d2876b
fix(feedback): await asynchronous knowledge gap persistence
BigDataDZ Oct 24, 2026
35a2207
fix(workspace): serialize concurrent process artifact recording
BigDataDZ Oct 24, 2026
0d59918
Merge commit '35a2207' into codex/oct24-feedback-process-integration
BigDataDZ Oct 24, 2026
c02fa8c
fix(mcp): recheck tenant ownership and theme scope for contracts
BigDataDZ Oct 25, 2026
2dc8eaa
fix(mcp): recheck workspace theme authorization before snapshots
BigDataDZ Oct 25, 2026
28a9650
Merge commit '2dc8eaa' into codex/oct25-mcp-authorization-integration
BigDataDZ Oct 25, 2026
60b26c7
fix(mcp): persist and enforce configured Origin allowlists
BigDataDZ Oct 25, 2026
6c82e39
fix(mcp): refresh live tool policies and rate limits
BigDataDZ Oct 25, 2026
9f5fbca
fix(code): await gate audit persistence on async platforms
BigDataDZ Oct 25, 2026
dc9fa40
Merge current main into PR #69 without raising size limits
BigDataDZ Oct 26, 2026
09665d2
Reconcile V1.2.0 main into PR #69 without raising size limits
BigDataDZ Oct 26, 2026
5fbe6f6
Merge branch 'main' into codex/oct25-code-gate-async-audit
xiaour Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,5 @@ jobs:
node-version: '22'
cache: npm
- run: npm ci
- name: Live MySQL concurrent upload and memory admission
run: node --test --test-concurrency=1 tests/uploadAdmission.mysql.integration.test.js tests/userMemoryAdmission.mysql.integration.test.js
- name: Live MySQL persistence concurrency acceptance
run: node --test --test-concurrency=1 tests/uploadAdmission.mysql.integration.test.js tests/userMemoryAdmission.mysql.integration.test.js tests/dataSourceTestAtomic.mysql.integration.test.js
31 changes: 20 additions & 11 deletions config/size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"src/chartIntent.js": 122,
"src/chatAttachments.js": 79,
"src/chatStreamPresenter.js": 123,
"src/codeExecution.js": 697,
"src/codeExecution.js": 692,
"src/columnSemantics.js": 95,
"src/conditionBindings.js": 64,
"src/config.js": 184,
Expand All @@ -82,7 +82,7 @@
"src/datasourceCrypto.js": 55,
"src/datasourceLifecycle.js": 15,
"src/datasourcePool.js": 24,
"src/dataSourceRepository.js": 184,
"src/dataSourceRepository.js": 180,
"src/derivedMetrics.js": 391,
"src/dialects/common.js": 25,
"src/dialects/index.js": 27,
Expand All @@ -92,7 +92,7 @@
"src/distributedLock.js": 121,
"src/executionRecovery.js": 61,
"src/fastQueryPath.js": 116,
"src/feedback.js": 76,
"src/feedback.js": 45,
"src/fileName.js": 96,
"src/fileParsing.js": 320,
"src/gateRegistry.js": 447,
Expand Down Expand Up @@ -129,16 +129,16 @@
"src/localIndicatorValidation.js": 38,
"src/mcp/audit.js": 103,
"src/mcp/auth.js": 54,
"src/mcp/config.js": 73,
"src/mcp/config.js": 57,
"src/mcp/connectorPackage.js": 115,
"src/mcp/index.js": 93,
"src/mcp/index.js": 56,
"src/mcp/modeGate.js": 46,
"src/mcp/rateLimit.js": 17,
"src/mcp/requestContext.js": 22,
"src/mcp/server.js": 30,
"src/mcp/tools/execute_query.js": 119,
"src/mcp/tools/execute_query.js": 93,
"src/mcp/tools/get_policy.js": 33,
"src/mcp/tools/get_snapshot.js": 49,
"src/mcp/tools/get_snapshot.js": 33,
"src/mcp/tools/list_metrics.js": 45,
"src/mcp/tools/resolve_query.js": 115,
"src/mcp/tools/shared.js": 55,
Expand All @@ -160,7 +160,7 @@
"src/platformDatabase.js": 61,
"src/platformLogRepository.js": 215,
"src/presentationFallbacks.js": 63,
"src/processArtifacts.js": 173,
"src/processArtifacts.js": 89,
"src/promptSanitizer.js": 123,
"src/queryContract.js": 358,
"src/queryContractCompiler.js": 2285,
Expand Down Expand Up @@ -203,7 +203,7 @@
"src/turnRouter.js": 143,
"src/uploads.js": 505,
"src/uploadAdmission.js": 94,
"src/userMemory.js": 642,
"src/userMemory.js": 637,
"src/userMemoryDistiller.js": 499,
"src/userMemoryMaintenance.js": 156,
"src/userMemoryModel.js": 138,
Expand All @@ -212,11 +212,20 @@
"src/userRepository.js": 141,
"src/valueClarification.js": 186,
"src/workflow.js": 230,
"src/workspace.js": 464,
"src/workspaceTransforms.js": 189,
"src/workspace.js": 566,
"src/workspaceRepository.js": 287,
"src/xlsx.js": 369,
"src/xlsxReader.js": 480,
"src/skillAudits.js": 119,
"src/platformAsyncLogRepository.js": 168,
"src/skillAudits.js": 119
"src/dataSourceRow.js": 25,
"src/mcp/configValues.js": 34,
"src/feedbackSubmission.js": 69,
"src/processArtifactDraft.js": 119,
"src/mcp/tools/contractExecution.js": 39,
"src/mcp/tools/snapshotAccess.js": 41,
"src/mcp/configDefaults.js": 15,
"src/mcp/runtimePolicy.js": 46,
"src/codeGateAudit.js": 22
}
17 changes: 2 additions & 15 deletions src/codeExecution.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { DEFAULT_PYTHON_BIN } from './config.js';
import { incrementCounter } from './metrics.js';
import { recordCodeGateBlock } from './codeGateAudit.js';

const BLOCKED_IMPORTS = [
'ctypes',
Expand Down Expand Up @@ -595,20 +595,7 @@ export class CodeExecutionService {
validateCode(code);
} catch (error) {
// CODE-001 is a hard, auditable gate: record the block before surfacing it.
incrementCounter('gate_block_total', {
gateId: 'CODE-001',
phase: 'EXECUTE',
blocking: 'true',
});
this.database?.addAuditLog?.({
userId,
action: 'CODE_GATE_BLOCK',
detail: {
sessionId: sessionId ?? null,
code: error.code ?? 'CODE_DATASOURCE_ACCESS_FORBIDDEN',
message: error.message,
},
});
await recordCodeGateBlock(this.database, { userId, sessionId, error });
throw error;
}
const runId = crypto.randomUUID();
Expand Down
21 changes: 21 additions & 0 deletions src/codeGateAudit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { incrementCounter } from './metrics.js';
import { callPlatformDatabase } from './platformDatabaseAccess.js';

export async function recordCodeGateBlock(database, { userId, sessionId, error }) {
incrementCounter('gate_block_total', {
gateId: 'CODE-001', phase: 'EXECUTE', blocking: 'true',
});
const entry = {
userId,
action: 'CODE_GATE_BLOCK',
detail: {
sessionId: sessionId ?? null,
code: error.code ?? 'CODE_DATASOURCE_ACCESS_FORBIDDEN',
message: error.message,
},
};
if (database?.isAsyncPlatformStore || typeof database?.addAuditLogAsync === 'function') {
return await callPlatformDatabase(database, 'addAuditLogAsync', 'addAuditLog', entry);
}
return database?.addAuditLog?.(entry);
}
46 changes: 11 additions & 35 deletions src/dataSourceRepository.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,22 @@
import { mapRow } from './dataSourceRow.js';
import { normalizeDataSource } from './dialects/index.js';
import { dataSourceTestState } from './datasourceLifecycle.js';

function nowIso() {
return new Date().toISOString();
}

function parseJson(value, fallback) {
if (value === null || value === undefined || value === '') {
return fallback;
}
try {
return JSON.parse(value);
} catch {
return fallback;
}
}

function asJson(value) {
return JSON.stringify(value ?? {});
}

function asBool(value) {
return value === 1 || value === true;
}

export class DataSourceRepository {
constructor(db, asyncStore = db) {
this.db = db;
this.asyncStore = asyncStore;
}

mapRow(row) {
return row ? {
...row,
options: parseJson(row.optionsJson, {}),
lastTestOk: row.lastTestOk === null || row.lastTestOk === undefined
? null
: asBool(row.lastTestOk),
} : null;
}
mapRow(row) { return mapRow(row); }

async listDataSourcesAsync() {
const rows = await this.asyncStore.query(`SELECT id, code, name, db_type AS dbType,
Expand Down Expand Up @@ -84,17 +62,15 @@ export class DataSourceRepository {
async updateDataSourceTestAsync(id, { ok, message }, source = null) {
const current = source ?? await this.getDataSourceAsync(id);
if (!current) return null;
const unchanged = (await this.asyncStore.query(`SELECT COUNT(*) AS count FROM data_sources
WHERE id=? AND code=? AND db_type=? AND host=? AND port=? AND database_name=?
AND username=? AND encrypted_password=? AND options_json=?`, [
Number(id), current.code, current.dbType, current.host, Number(current.port),
current.databaseName, current.username, current.encryptedPassword ?? '',
JSON.stringify(current.options ?? {}),
]))[0]?.count;
if (Number(unchanged) > 0) {
await this.asyncStore.execute(`UPDATE data_sources SET last_test_at=?, last_test_ok=?,
last_test_message=? WHERE id=?`, [nowIso(), ok ? 1 : 0, String(message ?? ''), Number(id)]);
}
const fields = ['code', 'db_type', 'host', 'database_name', 'username', 'encrypted_password', 'options_json'];
const match = fields.map(field => this.asyncStore.backend === 'mysql'
? `BINARY ${field} = BINARY ?` : `${field} = ?`).join(' AND ');
await this.asyncStore.execute(`UPDATE data_sources SET last_test_at=?, last_test_ok=?,
last_test_message=? WHERE id=? AND port=? AND ${match}`, [
nowIso(), ok ? 1 : 0, String(message ?? ''), Number(id), Number(current.port),
current.code, current.dbType, current.host, current.databaseName, current.username,
current.encryptedPassword ?? '', JSON.stringify(current.options ?? {}),
]);
return this.getDataSourceAsync(id);
}

Expand Down
24 changes: 24 additions & 0 deletions src/dataSourceRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function parseJson(value, fallback) {
if (value === null || value === undefined || value === '') {
return fallback;
}
try {
return JSON.parse(value);
} catch {
return fallback;
}
}

function asBool(value) {
return value === 1 || value === true;
}

export function mapRow(row) {
return row ? {
...row,
options: parseJson(row.optionsJson, {}),
lastTestOk: row.lastTestOk === null || row.lastTestOk === undefined
? null
: asBool(row.lastTestOk),
} : null;
}
71 changes: 4 additions & 67 deletions src/feedback.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,14 @@
import { submitFeedback, submitFeedbackAsync } from './feedbackSubmission.js';

export class FeedbackService {
constructor(database, growth = null) {
this.database = database;
this.growth = growth;
}

submit({
userId,
themeId,
sessionId,
messageId,
question,
correct,
comment,
corrected,
}) {
const message = messageId
? this.database.getChatMessage(messageId, userId)
: null;
if (messageId && !message) {
throw new Error('assistant message not found');
}
const resolvedThemeId = themeId
?? message?.result?.theme?.id
?? this.database.getChatSession(sessionId, userId)?.themeId;
if (!userId || !resolvedThemeId) {
throw new Error('user and theme are required');
}
const id = this.database.saveFeedback({
userId,
themeId: resolvedThemeId,
sessionId: sessionId ?? message?.sessionId,
messageId,
question: question || message?.result?.question || '',
correct: Boolean(correct),
comment,
corrected,
});
if (!correct) {
this.growth?.record({
term: question || message?.result?.question || `message-${messageId}`,
kind: 'FEEDBACK',
source: 'USER_FEEDBACK',
context: {
userId,
themeId: resolvedThemeId,
sessionId: sessionId ?? message?.sessionId,
messageId,
comment,
corrected,
},
});
}
return {
id,
correct: Boolean(correct),
message: '反馈已记录',
};
}
submit(args) { return submitFeedback(this, args); }

async submitAsync(args) {
if (!this.database.isAsyncPlatformStore) return this.submit(args);
const { userId, themeId, sessionId, messageId, question, correct, comment, corrected } = args;
const message = messageId ? await this.database.getChatMessageAsync(messageId, userId) : null;
if (messageId && !message) throw new Error('assistant message not found');
const session = !themeId && !message ? await this.database.getChatSessionAsync(sessionId, userId) : null;
const resolvedThemeId = themeId ?? message?.result?.theme?.id ?? session?.themeId;
if (!userId || !resolvedThemeId) throw new Error('user and theme are required');
const id = await this.database.saveFeedbackAsync({ userId, themeId: resolvedThemeId, sessionId: sessionId ?? message?.sessionId,
messageId, question: question || message?.result?.question || '', correct: Boolean(correct), comment, corrected });
if (!correct) this.growth?.record({ term: question || message?.result?.question || `message-${messageId}`, kind: 'FEEDBACK', source: 'USER_FEEDBACK',
context: { userId, themeId: resolvedThemeId, sessionId: sessionId ?? message?.sessionId, messageId, comment, corrected } });
return { id, correct: Boolean(correct), message: '反馈已记录' };
}
submitAsync(args) { return submitFeedbackAsync(this, args); }

list({ themeId = null, correct = null, limit = 100 } = {}) {
return this.database.listFeedback({ themeId, correct, limit });
Expand Down
68 changes: 68 additions & 0 deletions src/feedbackSubmission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export function submitFeedback(service, {
userId,
themeId,
sessionId,
messageId,
question,
correct,
comment,
corrected,
}) {
const message = messageId
? service.database.getChatMessage(messageId, userId)
: null;
if (messageId && !message) {
throw new Error('assistant message not found');
}
const resolvedThemeId = themeId
?? message?.result?.theme?.id
?? service.database.getChatSession(sessionId, userId)?.themeId;
if (!userId || !resolvedThemeId) {
throw new Error('user and theme are required');
}
const id = service.database.saveFeedback({
userId,
themeId: resolvedThemeId,
sessionId: sessionId ?? message?.sessionId,
messageId,
question: question || message?.result?.question || '',
correct: Boolean(correct),
comment,
corrected,
});
if (!correct) {
service.growth?.record({
term: question || message?.result?.question || `message-${messageId}`,
kind: 'FEEDBACK',
source: 'USER_FEEDBACK',
context: {
userId,
themeId: resolvedThemeId,
sessionId: sessionId ?? message?.sessionId,
messageId,
comment,
corrected,
},
});
}
return {
id,
correct: Boolean(correct),
message: '反馈已记录',
};
}

export async function submitFeedbackAsync(service, args) {
if (!service.database.isAsyncPlatformStore) return submitFeedback(service, args);
const { userId, themeId, sessionId, messageId, question, correct, comment, corrected } = args;
const message = messageId ? await service.database.getChatMessageAsync(messageId, userId) : null;
if (messageId && !message) throw new Error('assistant message not found');
const session = !themeId && !message ? await service.database.getChatSessionAsync(sessionId, userId) : null;
const resolvedThemeId = themeId ?? message?.result?.theme?.id ?? session?.themeId;
if (!userId || !resolvedThemeId) throw new Error('user and theme are required');
const id = await service.database.saveFeedbackAsync({ userId, themeId: resolvedThemeId, sessionId: sessionId ?? message?.sessionId,
messageId, question: question || message?.result?.question || '', correct: Boolean(correct), comment, corrected });
if (!correct) await service.growth?.record({ term: question || message?.result?.question || `message-${messageId}`, kind: 'FEEDBACK', source: 'USER_FEEDBACK',
context: { userId, themeId: resolvedThemeId, sessionId: sessionId ?? message?.sessionId, messageId, comment, corrected } });
return { id, correct: Boolean(correct), message: '反馈已记录' };
}
Loading
Loading