Security, as far as we can say
Publication snapshot: web-70ce3b8735785f14
Revision 13 · Production review pending.
Your agent needs to know what it would send to labbel, what we keep, and what prevents another organisation from accessing it. This page describes those boundaries, the security reviews we commissioned, the corrections that followed and the limitations we have accepted.
The reviews described here were performed by AI agents with access to source code and local test environments. They were not independent third-party security audits. Using an OpenAI model for a review does not constitute an audit or endorsement by OpenAI. The findings below are dated observations and reported corrections; they do not certify the security of the running service.
Release status, 16 September 2026. The September 7 corrections have a recorded deployment, and the S+ security changes described below were deployed on 16 September 2026: database migrations 049 to 053 and the MCP server. Post-deployment checks were run and are described where they bear on a claim. The website is published separately. This account describes the server deployment and checks recorded on that date; it is not a full security assessment of production.
Release, 24 September 2026. The follow-up release was deployed: database migrations 054 to 067 and the MCP server. Before it, a further Daybreak source review of the follow-up work and a review of each of the corrections that followed preceded the deployment; the final combined review was accepted with recorded qualifications. The deployment checks were limited, and the cold-session verification described below was never made. The earlier operational observations on this page retain their original dates.
Release, 8 October 2026. The build this page now describes removes the trial door, the trial key and its rotation, the MCP session and the two Durable Object classes that held it. What remains is one address and one door: OAuth, with the account's row read on every request for its organisation, role and standing, and no session held between requests. Call ceilings are counted at the edge, per signed-in user and per connecting address, and nothing is written to the database for them. New client registrations are also capped at a fixed number a day for the whole service, counted in the database as one number per day, with no client, name or address. Each row of a report an agent stores carries three facts the server observed as it wrote the row, in place of a session class; the guide defines them and what each does not establish. An organisation is created and its owner invited by the operator at the bench, and a signed-in person can see the clients connected to their account at the bench and revoke one; both write an audit row that names the action, the actor and the organisation, never the client. The count of how many tries a document needed before it passed the structural checks moved from session storage to the database, read in the account's current organisation and capped. These changes live in database migrations 068 to 074 and in the MCP server, and have been verified locally in the Workers runtime and against a local database. On 4 October 2026 OpenAI's Daybreak model read the difference from the 24 September release as a source review; its seven findings were addressed in the source before this revision, and it did not retest. On 8 October 2026, before the deployment, Daybreak read the build a second time: it found those seven closed in the source and raised two new findings — that client registration had no ceiling over time, and that the consent page said where a client's description was fetched from when only the host in its client ID had been checked. Both were corrected in the source before the deployment, by the daily registration cap above and by a consent page that says only where the client ID points; Daybreak did not read the corrections. These changes were deployed on 8 October 2026, and the sentences below that name the 24 September release describe the release before it. The checks after that deployment were limited: a request to the one address without a token was answered 401 with the OAuth challenge, the earlier organisation address answered 410, and our own clients — Claude Code, Claude Desktop and claude.ai, Codex and the ChatGPT plugin — signed in through OAuth with a client ID metadata document; a profile read through the one address succeeded, and the bench listed the connected clients and revoked one. They did not test the behaviour described here path by path; the rest rests on the local tests.
Current release, 12 October 2026. A new client names itself only by a client ID metadata document: the build gives the authorization server no registration endpoint, its metadata advertises none, and a request to the old registration address is answered as one the service does not have. The daily cap on new client registrations, which stood only in front of that endpoint, goes with it, and so does its count in the database. The database tables and functions that served the trial key, its rotation and the claim of a trial identity — empty in production since 9 October 2026 — are removed, and the three functions that write a participant's runs in steps and progress no longer consult them; a table of aggregated insights that nothing in the service read is removed with them. The bench lists a client named by a metadata document by its address, host and path, never decoded, so two clients from one host can be told apart. These changes live in database migrations 075 to 076, in the MCP server and in the website, and have been verified locally in the Workers runtime and against a local database. They were deployed on 12 October 2026. No check made after that deployment is recorded on this page yet, so what this paragraph says rests on the local tests; the paragraph above describes the release before it, its daily registration cap included.
Next release, stage EA. The account below is prepared for this build, which is not yet deployed. Code review is the one built-in domain; every other domain is authored by an organisation, with work handed in once as a run in one step, or delivered to a participant over time as a run in steps. The learning profile adds requirements and delivery guidance for work in steps whose purpose is learning. A run in steps belongs to the participant's own account: only that participant sees it in the list of due work and reports its follow-up. At the first saved step, the run binds to its edition and step documents; later edits do not change it. Both forms keep their follow-up contract, and an organisation-authored domain is answered per criterion in either form. The build also makes the database functions that write take the organisation's row before other rows, with one documented exception that only inserts a draft, so work and erasure meet in the same lock order. These changes live in database migrations 077 to 094 and the service built with them. The engineering record reports local verification and a Daybreak source reading of the lock order; the deployment requires a stop on writes while the database and service are updated. These are records of local checks and source review, not checks of this build in production.
The 7 September review
On 7 September 2026, David Eberlen commissioned a separate review by ChatGPT 6, working as Astra. It examined source commit 7179101, following authentication, organisation boundaries, database permissions, the human review bench and changes since the previous production baseline.
The reviewer ran the unit tests and source and test type checks, applied migrations 001–047 to a separate PostgreSQL database, and used synthetic data to investigate suspected failures. The Supabase integration suite was skipped because its test environment was unavailable. The separate database probes did not replace that suite.
The review did not test production, real OAuth sign-ins, platform log collection or concurrent trial-transfer requests under load. It was a source review with targeted local experiments, not a penetration test of the deployed service.
In the cases tried, all six organisation-checked write functions refused a foreign organisation's record. Three further probes checked that returning an existing stamp did not bypass that boundary: nine probes passed in total. A database policy probe also prevented a user in one organisation from reading another's domain. These results describe the cases tested, not every possible request.
What the review found, and what changed
The review found four issues. Two could lose saved work through ordinary use, and the reviewer recommended withholding deployment until those two were corrected.
| Finding | Correction |
|---|---|
| S1: transferring trial work into an organisation could delete authored domains and their bench records. | Migration 048 made the transfer move those records within the transaction, check domain-name collisions and refuse deletion if dependent work remains. |
| S2: cleanup of old unused trials did not count authored domains as saved work. | Migration 048 made domain editions and bench cases protect a trial from that cleanup. |
| S3: an administrator demoted to member could retain administrative powers in an open MCP session. | Each tool call now uses the account's current role for permission checks and the handler. |
| S4: suspending an account did not stop organisation-level reads through the database's authenticated-user policies. | The shared policy helpers now require an active account. Personal-record exceptions are described below. |
S1, S2 and S4 were corrected in migration 048. S3 was corrected in the MCP server. Our engineering record reports a passing strict gate, including the Supabase integration suite, after the corrections. Our deployment record dated 8 September 2026 reports that the corrected server and database migrations were deployed. These are the project's verification and deployment records; the original reviewer did not independently retest production. The transfer and the nightly cleanup belonged to the trial door: the build described at the top of this page has neither; the corrected functions remain in its database as history, called by no code path, and the build after it removes them.
The 15 September review and the S+ corrections
After the work on organisation-authored domains, David commissioned a further review using OpenAI's Daybreak Blue model. Astra prepared the source context; Daybreak conducted the security assessment. It examined authentication and session boundaries, database relationships and permissions, error logging and a reachable dependency. The work combined source review, local request tests and database probes with synthetic data. It was not a penetration test of production.
Daybreak reported five findings, two of which blocked its deployment recommendation. We addressed all five in the S+ security stage. The corrections are summarised here at the level relevant to a customer; reproduction scripts and detailed attack sequences remain internal.
Sessions and authorisation. The implementation deployed on 16 September separated trial and organisation session handling and checked organisation authorisation on each request; local regression tests at the time covered revoked or expired access, use of another principal's session and session termination. The post-deployment check confirmed that the organisation endpoint rejected an unauthenticated request with 401. That check did not exercise an authenticated session or verify session isolation in production. The build described at the top of this page has no session to isolate: authorisation is checked on each request from the account's row, and the tests that covered sessions went with them.
Input handling. A dependency update and a regression test address excessive processing from a crafted document. The dependency audit reported no known production-dependency vulnerabilities at the 16 September closing check. That result describes the audit's database and dependency versions at that time, not the absence of undiscovered vulnerabilities.
Database integrity and permissions. New database constraints keep a run's step and recipe connected correctly. Permission changes remove unnecessary table privileges from anonymous and authenticated roles on the affected tables. Integration tests check both invalid relationships and the resulting privilege sets. These changes do not remove the service's responsibility for organisation-scoped reads.
Error logging. The MCP error path now uses a fixed failure classification, an operation from a closed vocabulary and an error code. Local tests check that submitted values do not enter that log line. This correction applies to the MCP repository-error log line, not every logging surface. The platform settings and remaining bench limitation are described below.
Astra reviewed the S+ implementation and subsequent corrections, including additional findings about session termination and the vocabulary used in logs. The engineering record for 16 September reports that the source and test type checks and the full test gate, including the local Supabase integration suite, passed at closure. These tests verified the corrected code and local behaviour. The deployment record separately confirms the database and MCP server release and limited checks against the running service. Those checks did not repeat the full regression suite in production.
How we handle findings
The work above followed a repeatable sequence: examine the change, reproduce suspected failures with synthetic data, record findings, implement corrections, add regression coverage and review the result. Findings that affect deployment are kept visible until corrected or explicitly accepted. An accepted limitation remains a limitation.
Daybreak is a review tool in this process. The record describes a dedicated assessment after a major development stage, not continuous Daybreak scanning or a standing external audit. Local tests, reviewer judgement and production checks provide different evidence; we keep their scope and dates separate. Public summaries describe material risks and the state of corrections without publishing exploit instructions for unresolved issues.
How organisation boundaries are enforced
The MCP service uses a privileged database role that bypasses row-level security (RLS). On this path, saying that the database has RLS policies would not explain what protects your organisation's records. The controls have separate responsibilities.
Identity and reads. The service resolves the organisation from the authenticated connection. Application queries must restrict reads to that organisation. The repository has a shared suite of negative isolation cases for methods that accept an organisation ID, run against both the in-memory implementation and PostgreSQL through the same privileged role used by the service. A coverage test checks the repository interface so that a new organisation-scoped method without a case fails the suite. The database portion requires the integration environment; a run with those tests skipped is not equivalent.
These tests check application isolation. They do not add an RLS boundary to MCP reads. A defective read query remains a risk on this path.
Writes. Migration 047 added database-side organisation checks to six ID-based write functions. Each compared the supplied organisation with the record's organisation before writing or returning an existing stamp. Stage EA removes two of those functions that no code calls and preserves database-side organisation checks on the write paths in use. This gives those operations a second check beneath the application. It still relies on the service supplying the authenticated organisation correctly; the database does not independently authenticate the end user behind the service role.
Evidence records. Database grants restrict direct changes to protected records such as signals and records of runs in one step. The service role has no general table-level update or delete permission on the append-only set. This is a restriction on specified roles and operations, not a claim that database owners, privileged functions or cascading operations cannot affect records. The trial-transfer findings above illustrate why those other paths also need review.
The bench. The bench server also uses the privileged service role for its repository access. The public application key is used in the sign-in flow; it does not put the bench's subsequent repository queries under user-token RLS. The bench verifies a signed session cookie, reloads the current account, checks organisation and role, and checks the origin of write requests. Its boundary therefore also depends on server-side authorisation and scoped repository operations.
Direct authenticated database access. This is a separate path, governed by RLS. Organisation-level policies built on the shared helpers require an active account. Suspension deliberately leaves policies for a person's own account row, runs in steps, progress and personal signals available; it does not mean every possible database read is blocked.
What data is kept
labbel stores method documents, including recipes, steps and organisation-authored domain editions, together with account information, permitted progress records, run records and reported signals. Those documents are content you submit for the service to retain.
The code-review path receives and processes the submitted review but does not retain its body as a document or in its run record. It keeps structural checks, versions, selected declarations and derived counts. For work handed in as a run in one step under an organisation-authored domain, the run record does not copy the work's text or extract declarations from it. Your organisation needs to keep the original material if it wants to examine it later.
Reading a step for a run in steps also records which stored step version that account was given and when it was first given that version. Repeated reads of the same version do not create a frequency log. This account-linked record determines the material a finished run in steps is later assessed against; it is separate from the three facts a stored report carries, which the guide defines.
The service does not collect your agent's conversation history. It receives the arguments sent to its tools. Pasting conversation content into a stored document would still send that content to us: the retention rules depend on the tool and field, not on where the text originated. See the guide for the distinction between material sent, processed and stored.
Signal reflections have a 2,000-character limit per field; an overlong field is rejected rather than silently shortened. Before storage, the sanitiser removes recognised email addresses, URLs, phone numbers and @-handles. It does not recognise personal names and does not guarantee anonymity. Do not put personal information in signals. The organisation and its agent remain responsible for what they submit.
Follow-up work status. An assessor can record what is happening to the work on one assessable unit of a run: waiting, with a reason and a date to look again no more than thirty days ahead; giving up on it, with a reason; or opening the unit for another attempt. Those decisions are stored as an append-only log, one row per decision, addressed to the run and to the unit it concerns. Every word in a row comes from a closed list held by a database constraint: the log has no free-text field, and it has no column for who made the decision. The account is checked at the moment of writing — that it exists, belongs to the organisation and is not suspended — and nothing about it is stored. A status is never a result. It is not an outcome, it changes none of the facts stamped on a later report, and no counting path reads it as one; it is shown separately from the outcomes. Rows leave only when their run does.
Work references. An organisation's agent may file an optional locator saying where the work of a run, in either form, can be found: a git reference (host, repository, full revision, optional base) or a record reference (system, workspace, an opaque object ID, optional version), each marked as the subject of the work, its result or its deployment. Those are typed locator fields with lengths and refused characters, not descriptions: there is no free-text field, nothing is fetched by us from the address, and nothing is extracted from a submission. The work itself is never stored. A reference is held apart from the outcomes, is never a result and is counted as none; it is read back on the assessment brief for a chosen run, on a chosen run's record and on the bench card, and it appears in no due-work list, no profile, no signal and no aggregated insight. At most ten references stand on a subject at one time, and at most a hundred additions ever succeed on it; a removal is allowed even at the limit. An owner or administrator of the organisation may add or remove a reference on a run in one step; only the participant may do so for their own run in steps, and an administrator gets no access to another person's references on runs in steps. Removing a reference is a tombstone: the moment is stamped and every column that could carry the address goes to NULL in the same statement, so nothing of what it said remains, while the row stays with its ordinal, its timestamps and a digest of the retry key the addition was sent with. References are removed with the run, the participant or the organisation they hang on; an anonymised history of runs in steps keeps no locator. Removing a reference does not remove copies already received by another client, and does not promise immediate removal from existing backups.
Keys and rotations, in the 24 September release. The build described at the top of this page issues no key, rotates none and looks none up: nothing in the server reads the rows a trial key or its rotation left. In the 24 September release a trial identity could replace its own key in two steps, and the record of that operation held digests, timestamps and a state — never a key in readable form. The last key rows were deleted from the production database on 9 October 2026, and no rotation record remained.
Identifier-free is not anonymous. Two separate things stand between a reported value and the person who wrote it, and neither is a guarantee. The first is what the sanitiser takes out before storage — the recognised email addresses, URLs, phone numbers and @-handles described above; personal names are not among the patterns it recognises, so a name typed into a field is stored with the row, which is why the instruction above is to keep personal information out of signals in the first place. The second is the row itself, which carries no field naming its author — and is still not anonymous. Every signal is stored against your organisation and against the run or declaration it concerns, and is read back at that address by an agent authorised to read it. For a run in steps, that is the participant's own agent, not another member's or an administrator's. What is aggregated is the reading: our own counting surface counts rows and opens the text in none of them, while a chosen run's record quotes what a person wrote in their own words. Separately, records that are bound to an account do name the person: membership in an organisation, the account that resolved a judgement at the bench, and a work reference on a participant's own run in steps, which hangs on that participant. We do not offer a general guarantee of anonymity, and no part of this service should be read as one.
Database location. Our deployment records identify the Supabase database as Frankfurt, Germany (eu-central-1). The service and website run on Cloudflare Workers. In the 24 September release, MCP session state was held in Durable Objects addressed with an EU jurisdiction, so a session's stored state was kept in the European Union; the build described at the top of this page keeps no session state, so there is no stored session to locate. Our platform logs have no configured EU-only storage guarantee, according to the operational check on 16 September 2026. The content controls described below limit what specified application log lines contain; they do not provide a regional guarantee for logs or for all request processing.
Platform logs. Automatic invocation logging is disabled in the MCP server configuration, and stays disabled in the build described at the top of this page: an invocation log retains every request URL for seven days, and what persists instead is the server's own log lines, described below. In the 24 September release a trial key travelled in the request path, which is why the setting was chosen. In the build described at the top of this page no credential travels in a request URL: a saved trial link that still arrives with its old key in the path is answered with a pointer to the one address, and the key opens nothing. Console output and errors can still be collected. The recorded post-deployment checks included unauthenticated requests to both endpoints; no retained log entries or errors were observed in the checked window. This observation does not establish that all request paths are log-free.
Application error content. The MCP repository-error log line contains a failure class, an operation from a closed vocabulary and an error code. It excludes the database's details and hint fields, call arguments and raw error objects. The corresponding error response uses the failure class and fixed wording. A local test checks that a row value supplied in an error does not enter this line. This is a control on that application message, not a claim about every field in a platform log event.
Retention and access. The operational check on 16 September recorded seven days of retention for MCP Workers Logs on our plan. The Cloudflare account had one member, with two-factor authentication enabled. These are dated observations about that log collection and account membership, not a retention guarantee for every diagnostic surface.
Bench logs. Persistent Workers Logs collection is not enabled in the website Worker configuration. The bench still emits error messages that may contain submitted values and can be exposed during live debugging. Those messages need sanitisation before persistent collection is enabled; access to live diagnostics also needs to account for their potentially sensitive content.
Backups. The operational record on 16 September reports daily database backups, seven days of history and storage in the project's region. Point-in-time recovery is not enabled. Restoring a daily backup loses writes made after that backup: the interval can approach twenty-four hours if daily backups succeed, or be longer if a backup fails. The checks described here did not include a recovery drill.
Accepted limitations
The project maintains a register of accepted limitations, with tests that pin the described behaviour. Acceptance is a recorded engineering decision, not evidence that a limitation is harmless.
- A report can be partly accepted. Signals are checked individually for duplicates. Resending an expanded report can store its new signals while recognising earlier ones as already present; the response reports each result. This is intentional so that a retry or addition does not require rejecting the whole report. Without an idempotency key, a repeated signal can become another stored row where that signal format allows it. A criterion status in an organisation-authored domain has a stricter rule: only one status per criterion per run is accepted; a changed judgement belongs in a reflection.
- Sanitisation does not remove personal names. The accepted scope is pattern-based identifier removal. A guessed list of names would miss names and remove ordinary words while suggesting a protection it could not provide.
The register's earlier, time-limited write-isolation entry was closed after migration 047 added the checks described above. That closure does not remove the application responsibility for MCP reads.
The register's first entry — that revoking a trial key did not end trial sessions already open — was closed in the source on 21 September 2026 and deployed on 24 September: a trial session recorded the access key record it was established with, and a continuation that carried no credential was refused when that record had been revoked. Local regression tests covered this in the Workers runtime and against a local database until the build described at the top of this page removed the session and the key, and those tests with them; there, the entry is closed by construction.
The register's fifth entry — that a newly issued trial key stayed in plaintext in the issuing session's storage until the first tool response handed it over — is closed by construction: the release of 8 October 2026 issues no key and keeps no session storage, so there is no window left, and the database only ever held the key's digest. The tests that pinned the window went with the key and the session; the register names the tests that now pin its absence.
Everything on this page that postdates the 16 September release and precedes the release at the top of this page — the first entry's closure, the follow-up work status log, work references and key rotation — came with database migrations 054 to 067 and the MCP server built with them, was verified locally in the Workers runtime and against a local database, and was deployed on 24 September 2026. One verification was never made for that release: resuming a genuinely cold Workers Durable Object after key rotation. The cold path was tested in a Node stub only; since 8 October 2026 there is no Durable Object to resume. The checks after that deployment were limited: the closed trial door refused to issue a new identity, and one read-only tool call through the organisation door succeeded. They did not test the behaviour described here path by path, and the rest rests on the local tests. What the release of 8 October 2026 removed and added is stated at the top of this page.
What you can verify, and how to contact us
The architecture page explains the limits of external verification: the implementation and deployment configuration are not public, and service responses do not prove the internal controls. This page adds specific review results and known limitations to that account. No third-party audit certification is claimed.
To report a suspected vulnerability or ask about a boundary relevant to your intended use, contact hello@labbel.ai. Describe the affected operation and, where possible, reproduction steps using synthetic data. Do not include credentials or another organisation's data in the message.
Changes in this document
- Revision 13: prepares the data and access account for stage EA without claiming deployment: one built-in domain, two forms of run, binding at opening and participant ownership of runs in steps. Updates retained step records, work references and report access to that vocabulary and boundary; records the write lock order, retirement of two unused write functions and the single status per criterion per run. Keeps the 12 October deployment and earlier reviews as dated history.
- Revision 12: records the 8 October 2026 deployment of the release described at the top of this page — database migrations 068 to 074 and the MCP server — Daybreak's second reading before it, and the limited checks made after it; closes the register's fifth entry, the plaintext-key window, by construction, and removes it from the list above; says that the last trial key rows were deleted on 9 October 2026; and puts the sentences about the 24 September release, including its never-made cold-session verification, in the past tense; and describes the release after it, deployed on 12 October 2026 — client ID metadata documents only, the registration cap and the trial door's tables and functions removed, the bench's client address — as verified locally, with no check after the deployment recorded yet.
- Revision 11: describes the build after the 24 September release — one door, OAuth, no key, no session, no Durable Object; ceilings counted at the edge; three observed facts on each stored row of a report; the operator's path, connected clients and the friction count in the database — as verified locally, read on 4 October, and not yet deployed; and rewrites the sentences about the trial transfer and cleanup, session authorisation, work references, key rotation, the location of session state, invocation logging, the plaintext-key window and the register's first entry so that each says which build it describes.
- Revision 10: adds the current-release entry for 24 September, names the outstanding cold-session verification beside the local checks, and describes the record of which course step version an account was given and when.
- Revision 9: records the 24 September deployment of database migrations 054 to 067 and the MCP server, and the limited checks made after it.
- Revision 8: describes what the follow-up work status log, work references and key rotation keep, and the difference between an identifier-free value and an account-bound record, including what the sanitiser does not recognise. States that the new migrations and the server changes above are verified locally and not yet deployed.
- Revision 7: records the closure of the register's first accepted limitation in the source, and removes it from the list above. States that the change is local and not yet deployed.
- Revision 6: distinguishes the limited post-deployment checks from local session-isolation tests; separates persistent log collection from live diagnostics; narrows the region, retention and backup claims to the recorded evidence. Removes stale pending-deployment wording.
- Revision 5: added the 16 September deployment record and operational observations about logs, account access, location and backups. Revision 6 corrects the scope of conclusions drawn from those observations.
- Revision 4: adds the dated Daybreak review, all five S+ correction areas and subsequent Astra review; separates local verification from pending deployment and production checks. Describes the findings process without claiming continuous scanning. Narrows the logging claim to the MCP path and keeps outstanding operational verification visible.
- Revision 3: rewrites the logs paragraph after the 15 September correction (finding A4): the server's log line carries the failure class, the operation and the error code, never row values. Platform log settings remain unverified.
- Revision 2: removes a stale pending-review statement from the revision history. The current publication review status appears at the top; the specific unverified settings remain identified in the text.
- Revision 1: describes the 7 September review, the reported corrections and deployment, organisation isolation, retained data and accepted limitations.
This page is also served as markdown at labbel.ai/security.md — agents read documents.