From a2a31973ab572d5b3b7762ba92283f874943f763 Mon Sep 17 00:00:00 2001 From: Robert Court Date: Wed, 19 Aug 2026 19:07:45 +0000 Subject: [PATCH 1/2] Complete the privacy notice, and stop user text reaching logs Two things the DPIA needs before it can be submitted honestly. The privacy notice was missing four Article 13 essentials: it never identified the controller, gave no lawful basis, listed only access and erasure among the rights, and offered no route to the ICO. The transparency answer in the assessment rests entirely on that page, and it is the first thing an assessor opens. It now names the University of Edinburgh as controller with its ICO registration; states Article 6(1)(e) public task, grounded in the Universities (Scotland) Acts 1858 to 1966 and the Further and Higher Education (Scotland) Act 2005, with consent for an optionally attached transcript; gives the full rights list and how to use the response identifier; and gives the ICO complaint route. Article 6(3) attaches to the task rather than to the individual processing activity, so no statutory power naming this service is needed. Legitimate interests was considered and is not available: Article 6(1) provides that points (ea) and (f) do not apply to processing carried out by a public authority in the performance of its tasks, and the IP address held for abuse prevention exists solely to keep a public-task service available. The notice also now says the service reads from PubMed and bioRxiv and what is sent to them. Not an Article 13 disclosure, since no personal data goes, but the page describes how answers are built and should not leave it out. Then three places user-derived text was reaching somewhere it should not, all found by re-reading the code rather than the documentation. Blocked hostnames were kept by NAME for 26 months. They are extracted from a user's question by regex, so a fragment of what someone wrote sat in the long-retention tier. Now a count. The names remain in the 30-day security and blocked-search logs, which is where an abuse investigation would look. A failing tool logged its arguments by shape and the upstream error verbatim beside them. get_reviewed_page takes a URL whose path is unconstrained - only the host is allow-listed - and the fetcher throws it back in the message, so the redaction was defeated by the value next to it. Errors now go through safeText at all five sites. The argument value test was "contains no whitespace", which a URL and a 70-character lab identifier both pass. It is now identifier-shaped: 40 characters, no path separators. Real VFB vocabulary still prints, so the diagnostic keeps its value. The leak guard caught none of these: it planted its canary only in the question and the term name, and its tool-failure case threw an error containing no user text. Two new cases close both paths. Also: pass NCBI_EMAIL through docker-compose, which was set on the server but never reached the container because Compose forwards only what is listed; and name personal data in the chat's opening guidance, which said only confidential or sensitive. --- app/api/chat/route.js | 6 +-- app/page.js | 2 +- app/privacy/page.js | 69 ++++++++++++++++++++++++++-- docker-compose.yml | 1 + lib/governance.js | 32 ++++++++----- lib/liveHarness.mjs | 10 +++- lib/safeToolArgs.mjs | 10 +++- tests/unit/consoleLeakGuard.test.mjs | 37 +++++++++++++++ 8 files changed, 146 insertions(+), 21 deletions(-) diff --git a/app/api/chat/route.js b/app/api/chat/route.js index df9f31b..412a940 100644 --- a/app/api/chat/route.js +++ b/app/api/chat/route.js @@ -33,7 +33,7 @@ import { stripSupersededFigures } from '../../../lib/countProvenance.mjs' import { isAggregateClassPartner } from '../../../lib/classPartners.mjs' import { planNextAttempt } from '../../../lib/callBudget.mjs' import { detectJailbreakRule } from '../../../lib/jailbreak.mjs' -import { safeToolArgs } from '../../../lib/safeToolArgs.mjs' +import { safeToolArgs, safeText } from '../../../lib/safeToolArgs.mjs' import { createRunSignal, throwIfAborted, isRunAbortedWith, NOBODY_WAITING } from '../../../lib/runSignal.mjs' import { parseScrnaseqClusters, parseClusterExpression, extractRequestedGenes, buildExpressionMatrix, renderExpressionMarkdown } from '../../../lib/scrnaseq.mjs' import { pickSeedIndividuals, parseSimilarityHits, groupSimilarByClass } from '../../../lib/similarNeurons.mjs' @@ -1203,7 +1203,7 @@ async function callMcpToolWithRetry(client, name, args, { retries = VFB_MCP_MAX_ // `spent` and `next` are the two facts that were missing when this was // diagnosed from logs alone: a stall looks exactly like a server refusing // three times unless the line says which budget ran out. - console.error(`[VFBchat] MCP CALL FAILED | tool=${name} | attempt=${attempt + 1}/${retries + 1} | transient=${transient} | spent=${elapsedMs}ms/${budgetMs}ms | next=${next.reason} | args=${safeToolArgs(args)} | error=${error?.message || error}`) + console.error(`[VFBchat] MCP CALL FAILED | tool=${name} | attempt=${attempt + 1}/${retries + 1} | transient=${transient} | spent=${elapsedMs}ms/${budgetMs}ms | next=${next.reason} | args=${safeToolArgs(args)} | error=${safeText(error?.message || error)}`) if (!next.retry) throw error attemptTimeoutMs = next.timeoutMs if (next.waitMs > 0) await new Promise(resolve => setTimeout(resolve, next.waitMs)) @@ -1277,7 +1277,7 @@ async function callMcpToolTextWithForceRefresh(client, name, args, { budget } = if (!isFailedRunQueryPayload(retryText)) return retryText return annotateFailedRunQuery(retryText) } catch (error) { - console.error(`[VFBchat] force_refresh retry for ${name} failed: ${error?.message || error}`) + console.error(`[VFBchat] force_refresh retry for ${name} failed: ${safeText(error?.message || error)}`) return annotateFailedRunQuery(text) } } diff --git a/app/page.js b/app/page.js index 8a3d0e3..92b2ff8 100644 --- a/app/page.js +++ b/app/page.js @@ -1031,7 +1031,7 @@ export default function Home() { - Raw security logs are retained for up to 30 days - We do not store full chat content for routine analytics - If you report a problem, you can optionally attach the visible chat for investigation for up to 30 days -- Do not share confidential or sensitive information +- Do not share personal, confidential or sensitive information - Use this tool to enhance your understanding of neuroscience concepts - See the [Privacy Notice](/privacy) for more information diff --git a/app/privacy/page.js b/app/privacy/page.js index e2aa589..3a10d1e 100644 --- a/app/privacy/page.js +++ b/app/privacy/page.js @@ -19,6 +19,36 @@ export default function PrivacyPage() { VFB Chat is a public-facing AI-assisted interface to Virtual Fly Brain data.

+
+

Who is responsible for your data

+

+ The University of Edinburgh is the data controller for the personal data + described in this notice. The University is registered with the Information + Commissioner’s Office under registration number Z6426984. VFB Chat is operated by the + Virtual Fly Brain project in the School of Informatics. +

+
+ +
+

Our lawful basis

+

+ We process this data in the performance of a task carried out in the public + interest (UK GDPR Article 6(1)(e)). The University’s constitution is + statutory — the Universities (Scotland) Acts 1858 to 1966 — and the Further and + Higher Education (Scotland) Act 2005 provides for the undertaking of research among + fundable bodies, of which the University is one. Operating Virtual Fly Brain as an open + research resource, and making its data reachable, falls within those functions. We process + only the minimum technical data needed to run the service securely and to show how much it + is used; the IP address held for abuse prevention is processed on the same basis, because + it exists solely to keep that service available. +

+

+ Where you choose to attach a conversation to a problem report, we rely on your{' '} + consent for that transcript. You do not have to attach one, and you can + ask us to delete it at any time. +

+
+

What We Collect