Your bid documents are not marketing data. Treat them accordingly.
A bid package is a competitor's cost structure, an owner's vulnerabilities and a building's door schedule in one file. This page says exactly where it goes.
Document pipeline runs on your machine Model calls disclosed by vendor and model id No key column in the schema No certification claimed
Four statements. If they answer your question, you are done here.
Written for the person deciding whether to send a package, not for the person who has to sign off on the architecture. The technical record is below and nothing has been removed from it.
Core processing is local-first
The document pipeline runs on the operator's machine: ingest, rule evaluation, validation and report generation are one command line program on your own disk. Your originals are not moved, not copied and not modified.
No silent document uploads
Nothing is sent anywhere without it being disclosed first. There is no upload form on this site and no upload feature in the repository.
External model transmission requires explicit disclosure
Before anything leaves the machine, a block prints naming each vendor, the full model identifier that will receive the material, and each file by name. There is no flag, no environment variable and no yes-to-everything switch that suppresses it.
API credentials are not stored in project artifacts
A key stays in the browser and is sent per request to the endpoint that uses it. The database schema has no column that could hold one, and a test fails the build the day such a column appears.
The record those four statements are drawn from.
Every statement below was checked against the source before it was written. Where something exists in the repository but is not running anywhere, this page says so in the same sentence rather than letting the present tense imply a service. Section 10 is the list of what is not deployed.
The document pipeline is a command line program on your own machine.
Ingest, rule evaluation, validation and report generation all run locally. You point the ingest command at a package directory. It opens the PDFs in place, pulls the text layer with page and coordinate positions, segments the text by CSI MasterFormat section numbering and locates scope markers by pattern matching. Your originals are not moved, not copied and not modified.
- 01 package/*.pdf The drawings, specification sections and addenda, opened in place on your disk.
- 02 extraction Text layer with page and coordinate provenance, CSI section segmentation, scope markers by pattern match.
- 03 document_model.json One JSON file, written where you tell it to write it.
- 04 rules and validators Deterministic evaluation against the published rule packs. This is the part that decides and computes.
- 05 register, BOM, report Written into an output directory you choose. Nothing sweeps them up afterwards.
Nothing on this path opens a network connection. No document is uploaded for any of it.
Reached by one optional command, bre chat, which answers questions
out of the artifacts produced above. That command is the subject of sections 02 and
03: what it sends, and what has to happen before it sends anything. Run sealed it
cannot make a call at all.
What that sentence does and does not cover
The claim above is about the document pipeline: the path your bid package takes from PDF to register. It is not a claim that this project will never run a server for anything. An authentication backend and a chat backend exist in the repository and are described in sections 05, 06 and 10. Neither of them takes a bid package, and neither is deployed. The pipeline that reads your documents is the program on your machine.
The boundary is structural, not a policy
The parts of the system that decide and compute are forbidden from reaching a model client at all. A test walks the import graph and fails the build if a model client becomes reachable from the engine, the validators, the scoring code or the risk code. That test is never weakened and never skipped.
One honest caveat about scope. This page describes what the software does. It does not describe your machine, your backups, your file sync or your own network. A bid package sitting in a synced folder is in that provider's hands regardless of what any tool does with it, and that is your call to make rather than something this program can make for you.
One command can reach the network. Reading the package and running the engine cannot.
Models may extract, classify, locate, transcribe and match. A model may never compute a
quantity, assign a part number, produce a risk score or determine compliance. Reading the
package and running the engine make no outbound call. The one command that can is
bre chat, which answers questions out of the artifacts a run already
produced. It is gated, and the gate has no off switch. Run sealed it cannot make a call
at all: a sealed cache is read-only, refuses to record, and reports a miss by name rather
than quietly reaching the network.
The vendor is a subcontractor in your chain of custody
Whether or not anyone calls it that. Today exactly one vendor is reached, and only through the chat command. The vendor table in the code also names OpenAI so that a second reader can be added later, but no command reaches it and its SDK is not a declared dependency. Adding it would change the disclosed recipient list, which re-asks for consent by design.
Anthropic, in use today
"By default, we will not use your inputs or outputs from our commercial products (e.g. Claude for Work, Anthropic API, Claude Gov, etc.) to train our models."privacy.claude.com, data usage for the commercial API
The same page states an exception: if you explicitly report feedback or bugs, or otherwise choose to allow it, your material may be used for training. Nothing in this tool submits feedback on your behalf.
Quoted as published, checked 10 August 2026.
OpenAI, named but not reached
"data sent to the OpenAI API is not used to train or improve OpenAI models (unless you explicitly opt in to share data with us)."developers.openai.com, API data usage and retention
The same documentation describes abuse monitoring logs retained for up to 30 days unless longer retention is required by law or is reasonably necessary to protect their services or a third party, and separate retention rules that vary by endpoint.
Quoted as published, checked 10 August 2026.
Published policies are not your contract. They change, they differ by account type, and an enterprise agreement, a zero-retention arrangement or a reseller in the middle can all change the answer for your account specifically. Retention is a separate question from training. This project has no contract with either vendor covering your material: the account is yours, the key is yours, and the terms are between you and them. Checking your own account terms before sending a client's package is not a disclaimer to move the risk, it is the only accurate description of who is in a position to know.
If your NDA or your district's terms will not permit it, the answer is straightforward. Do not run the chat command, or run it sealed. Reading the package and running the engine make no outbound call, and that is the majority of what the tool does.
Where a model reads, the run says so, names the vendor, and cannot be told to be quiet.
Before anything is sent for a package, a block prints naming each vendor, the full model identifier that will receive the material, and each file by name. Not a family name like "the latest model", because that does not say what actually ran. Not a generic banner about AI, because a reader cannot act on one. The point is that you can take that list to your counsel or to your district contact and get a yes or a no on the facts.
Transmission disclosure for <package> The following will be sent to model APIs outside this machine: <vendor> (<model identifier>) 2 files engine_output.json document_model.json Responses are cached under out/cache/ on this machine. Nothing is committed to the repository. This is asked once per package, and again if the file set changes or the recipients above change.
The shape of the block, with the package, vendor and model identifier standing in for the real ones. Where two files share a name, the colliding entries take one more folder from the left, so an addendum folder's drawings file and another addendum folder's drawings file are told apart in the terms the district itself issued them.
There is no way to turn the disclosure off
No flag, no environment variable, no yes-to-everything switch. The project's own words for why, from the amendment that requires the disclosure:
There is deliberately no suppression flag, no environment variable, and no --yes. Suppressing the prompt is the first thing anyone scripts, and a disclosure that can be disabled is a disclosure that will be. src/bre/narration/copilot/disclosure.py
That is enforced in three places rather than requested once. The gate lives in the session layer and not only in the command line entry point, because a gate that lives in one caller is a gate the second caller does not have. A boolean argument would itself have been the suppression flag, so what the gate requires instead is the consent record and the current file set digest. And the recipient list is derived from the vendor and model constants rather than accepted as a parameter, so the list compared at the gate is necessarily the party that will receive the material.
What crosses the line, and what is deliberately held back.
The payload is a function of the question and the records retrieved for it. Everything that would let a model assert a fact about your documents rather than quote one is left out on purpose.
What the model receives
The question as typed, plus the specific records that deterministic retrieval selected out of the run artifacts. Each record is serialized as a record id, the record's text, and where one exists the quoted source sentence it carries. There are no sampling parameters, which keeps the payload a function of the question and the records alone.
What it does not receive
Not your PDFs and not page images. Page numbers and document names are deliberately left out of the request, because a model that was shown "p. 236" could type "p. 236". Provenance is looked up afterwards, in code, out of your own artifacts, so a page number in an answer is one the code read rather than one a model wrote.
Nothing here is a data product. Nothing processed is collected, aggregated, resold or used to train anything. The benchmark dataset is specified to hold manifests only: bid number, district, public source link, retrieval date and derived annotations. Source documents are never committed to it, so the measurement can be audited without this project ever holding anyone's package.
The promise about your key is not kept by this page. It is kept by the schema.
If you connect your own model account, the key stays in your browser. It is sent per request to the endpoint that uses it: bound to one local constant, placed in one header on one outbound request, and then the request ends and the binding is gone. It is never logged, never cached, never echoed into a response or an error message, and never interpolated into a string, a template literal or a URL.
That is the kind of sentence every product writes. Here is why this one is checkable. The database schema has no column the key could be written to, and the prohibition is written into the schema file itself rather than into a design document nobody opens:
THERE IS NO API KEY COLUMN IN THIS SCHEMA, AND THERE MUST NEVER BE ONE. The product's published security page states that a user's Anthropic API key is never stored on the server. That key lives in the user's browser and is sent per request to the chat endpoint, which uses it and forgets it. A promise like that is not kept by the security page, it is kept here. The moment a column named api_key, encrypted_key or anything of that shape appears in the users table, the published page becomes false, whatever the code around it does. If a future change appears to need one, that change is wrong. Adding a column is easy and quiet, which is exactly why the prohibition is written in the schema rather than in a design document nobody opens. test/schema.test.js enforces this by reading PRAGMA table_info for every table and failing on any column matching /api[_-]?key/.
The last sentence is the load-bearing one, and it is true. test/schema.test.js
applies the schema, then reads PRAGMA table_info for every table and asserts
that no column name matches /api[_-]?key/. It asserts the same for
/anthropic/. A second test asserts the comment above is still in the file
and still says why. So the day somebody adds a quiet column called
encrypted_key, the build goes red before the page becomes a lie.
The key never enters the frame that could write it down
On the local side there is nothing to store: each vendor's SDK reads its own environment variable when the client is constructed, so the value never passes through this code at all. A response cache entry holds the vendor, the model identifier, the prompt version, a hash of the request and the response. It holds no key.
The trade, stated rather than hidden
The key is held in the browser's localStorage, and
localStorage is readable by any script on the origin. The application
says that on its own page rather than implying the key is safer than it is. The
alternative would be for the browser to call the vendor directly, so the key never
touched this origin at all. It does not, because retrieval, the copy guard and the
citation lookup run on the server and would otherwise have to be trusted to the
browser.
Everything this system writes down, and where each thing is written.
Where model responses land
Model responses are cached so a run can be replayed. A cached response holds verbatim
text out of your documents, which makes the cache a copy of your material and therefore
something whose location has to be decided rather than defaulted. Every real package's
cache lives beside that package, at <package>/out/cache/, outside the
tool's own working tree. That is a written project requirement, not a convention: a cache
recorded against a real bid package and then committed would put a district's
specification text into a source repository, which is the exact thing this project
forbids, arriving through a door nobody was watching.
- One readable JSON file per call. Vendor, model identifier, prompt version, a hash of the request, and the response. You can open it.
- It holds no key. The key never passes through this code: each vendor's SDK reads its own environment variable when the client is constructed, so the value never enters the frame that could write it down.
- Deleting it is safe. It is a replay cache. Removing the directory removes the copies, and the tool asks again before it records again.
- Sealed mode never writes and never calls. A sealed cache is read-only. It refuses to record and reports a miss by name rather than quietly reaching the network.
Passwords: PBKDF2-SHA256 at 210,000 iterations, parameters stored beside the digest
Password hashing runs through WebCrypto. The algorithm, the iteration count and the salt are all stored alongside the hash rather than compiled into the application, so the cost can be raised later without locking out a single existing account.
| Parameter | Value | Why this one |
|---|---|---|
| Algorithm | PBKDF2-SHA256 |
bcrypt, scrypt and argon2 are not available in the Workers runtime without shipping WASM. PBKDF2 is weaker per unit of work than argon2id, so the iteration count carries the load. |
| Iterations | 210000 |
The OWASP figure for PBKDF2-SHA256, treated as a floor rather than a target. |
| Salt | 16 random bytes per password | Stored with the record, so two people with the same password do not share a digest. |
| Derived key | 256 bits | Matches the hash width, so nothing is truncated on the way into storage. |
| Session token | 32 bytes from the runtime CSPRNG | Only its SHA-256 is stored, in a column named token_hash and not
token, so the distinction survives being skim read. |
| Session cookie | HttpOnly; Secure; SameSite=Lax; Path=/, explicit seven day max age |
Script cannot read it, it never crosses plain HTTP, and it is not attached to cross-site posts. The token is never put in browser storage. |
Verification uses the stored count
A user logs in, the iteration count recorded on their row is used to verify, and the
record can then be rewritten at the new cost. Hard coding the cost in the application
would mean a future increase locks out every existing account, so the columns
password_algorithm, password_iterations and
password_salt travel with password_hash.
An unknown address costs the same as a known one
Signing in with an address that has no account runs the same derivation against a fixed decoy salt, so the response time does not answer the question "is this person a customer". The comparison that decides authentication is constant time, because a short circuiting compare leaks how much of a guess was right, one byte at a time.
Counted by address and by email, separately
Counting only by IP lets a botnet spread a password spray across addresses. Counting only by email lets one attacker lock out one victim. Neither alone is a limiter, so both buckets exist and both are enforced.
Signing out revokes exactly the session presented, not every session the account has. Signing out on a shared machine should not sign the same person out on their phone.
What is written down about a request, and what is written down about a visit.
The source is checked too, not just the database
test/secrets.test.js reads the shipped source rather than exercising
it, because a behavioural test cannot prove the absence of a leak: the dangerous line
is always on the branch the test did not take. It fails on any
console call anywhere under lib/ or
functions/, on any secret-shaped identifier passed to a logging call, on
any Error built from a template literal, a concatenation or a variable, on any use of
browser storage in that code, and on anything key-shaped appearing near a database
write.
Why an error message counts as a log entry
A message built from a template literal is the ordinary way a secret reaches a log line, a response body or a support ticket, because the value that made the message useful is the value that should not have travelled. Banning the construction outright is cruder than reviewing each one, and it is the version that survives a deadline.
First-party measurement on this site
This site measures itself, and the whole of that is one file you can open:
/assets/bre.1eea7f485a.js. This is everything it does.
- Named events, same origin only. One page-view event per
route, named for the page rather than emitted as a generic pageview with a path, plus
one interaction event for any element carrying a
data-evattribute when it is activated. Each event is dispatched as a DOM CustomEvent and held in memory for the life of the page. Nothing is transmitted anywhere today. The collector is written and tested but not deployed, so the endpoint constant in the script is null and no request is made. When it is turned on it will post to a path on this site withnavigator.sendBeacon. There is no other recipient, and there is no second setting that could change that: the content security policy below permits requests to this origin only. - A random session identifier that dies with the tab. Nine
random bytes from the browser's own CSPRNG, held in
sessionStorageunder the keybre_s. It is generated in the browser, never derived from anything about the person, does not survive the tab, and is not joinable with anything. Where storage is unavailable it degrades to the constantnostorageand nothing breaks. - Device category, and referrer host only. The device
category is one of mobile, tablet or desktop, computed from the window width. The
referrer is reduced to a hostname, or to the literal
directorinternal. The full referring URL is never sent, because a full referrer can carry a search query or a document title from wherever the visitor came from, and this site has no business holding either. - No cookie, no cross-site profiling, no advertising pixel, no third-party script. Nothing on this site loads from another origin. There is no identifier that outlives the tab and nothing that joins a visit to a person.
- Form field values and document contents are never sent as events. Only short labels are ever attached, truncated to 64 characters. The pilot form reports its submission as an event carrying no payload at all.
- The content security policy allows scripts from this origin
only.
script-src 'self'andconnect-src 'self'are set in_headers, so a beacon has nowhere else it could go. That policy currently blocks the hosting provider's own analytics beacon, which is injected into the response rather than written into these files. The site loses that measurement and keeps the guarantee.
A page that tells estimators their documents are not marketing data cannot be carrying a third-party tracker, and a header that fails loudly is what stops that eroding by accident rather than a promise to remember.
There is no upload form, and that is a decision rather than a gap.
Pilot documents are handled by direct arrangement, and the terms are agreed in writing before anything is sent: what is being sent, how it is transferred, what is kept, and what is deleted. That is the current behaviour stated as it is, rather than a retention schedule written for a service that does not exist. The pilot request asks for the project type and the rough package size. It has no file field.
An upload form is a decision made once, for everyone
Whoever builds it decides, on behalf of every future user, that client material will be held on a server. A pilot is a small number of packages, and each of them deserves an arrangement rather than a default.
The party who knows the terms is you
Your NDA, your district's terms and your own obligations govern what may leave your hands. A form cannot read them. A conversation before the first file moves can.
On your own machine, deletion is yours to do
The artifacts a run writes go to an output directory you chose, and the response
cache sits beside the package at <package>/out/cache/. Removing
either is safe and removes the copies. Nothing sweeps them up and nothing keeps a
second copy somewhere else.
When you are asked again, and what declining does.
Consent is recorded per package, and it is recorded against the material rather than against a filename. Four cases decide whether the disclosure fires a second time.
- When the contents change. The consent record holds a hash over the bytes of every file, not their names. A revised addendum that keeps its filename produces a different hash and re-asks. Consent to send version one is not consent to send version two.
- When the recipients change. Approving one vendor is not approving that vendor plus a second party. Vendors, model identifiers and the file list are disclosed together as one statement, and a gate that watched one third of what it showed would not have re-asked.
- When the record cannot be read. Missing file, unreadable file, invalid JSON, wrong type, missing field, mismatched hash: every one of them means ask again. It fails closed on purpose. A false alarm costs one keystroke. The other kind of error is a client's bid package leaving the machine without anyone being asked.
- Not when the file order changes. The hash is order independent, so a folder listing files in a different order on a different machine does not re-ask. A prompt that fires for no reason is a prompt people learn to click through, and a disclosure that gets clicked through has stopped disclosing anything.
Declining is a correct outcome and not a crash: the estimator who reads the file list and decides a district's package does not leave their machine has used the feature correctly. A caller with no terminal attached is treated as having declined, because no answer is not a yes and defaulting the other way would transmit a package because a pipeline had no input attached.
Sections 05, 06 and 07 describe code that is written and tested, and is not live.
There is no hosted application you can sign into today. Sign-up, login, logout and session handling exist in the repository and have tests. The chat copilot endpoint exists in the repository and has tests. The browser application exists in the repository. None of it is deployed, there is no address that serves it, and no customer account exists.
There is also no upload feature anywhere, in the repository or on this site, and no published pricing.
Why this section exists at all
A security page that quietly implies a running service is the exact failure this page exists to prevent. It is easy to describe a login, a key panel and a chat endpoint in the present tense and let a reader conclude they can use one. Every claim about them on this page is a claim about code and a test, and that distinction is worth more to you than the impression of a finished product.
What changes if it ships
If authentication and the chat backend are deployed, a server exists that holds accounts and sessions. The statement in section 01 is written to stay true through that, because it is a claim about the document pipeline: your bid package is read by the program on your machine. This page changes before anything ships, not after.
The key guarantee in section 05 exists in the schema and in a test now, before anyone can sign in, which is the only order in which a promise like that means anything.
No SOC 2. No ISO certification. No penetration test. No attestation of any kind.
Stated plainly
There is no SOC 2 Type I or Type II report. There is no ISO 27001 or any other ISO certification. No third-party penetration test has been performed and no report exists. There is no compliance attestation and no uptime commitment.
Why saying otherwise would be fatal here
A certification claim is the easiest sentence on a page like this to write and the hardest for a reader to check. It would be the first dishonest thing on the site, and once one sentence here is unreliable a reader has to re-verify all of them, which is exactly the work this page was written to save them.
What you can check instead. Every rule is published with its conditions, rationale and cited authority. The limitations are published. The build status is published, including what is not built. The benchmark method is published before any result exists, so the method cannot be chosen after seeing the numbers. What we publish is measured. What isn't measured is labeled accordingly.
Read the rest of it before you decide.
The pilot is a small number of real packages, run with real estimators, with the document handling agreed in writing first. Nothing is uploaded to start one.