Security

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

The short version

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.

01

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.

02

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.

03

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.

04

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.

Technical detail for IT review

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.

01 · Local workflow

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.

bre ingest → bre run the document path
The operator's machine
  1. 01 package/*.pdf The drawings, specification sections and addenda, opened in place on your disk.
  2. 02 extraction Text layer with page and coordinate provenance, CSI section segmentation, scope markers by pattern match.
  3. 03 document_model.json One JSON file, written where you tell it to write it.
  4. 04 rules and validators Deterministic evaluation against the published rule packs. This is the part that decides and computes.
  5. 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.

Outside the machine

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.

02 · Outbound requests

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.

03 · Disclosure mechanism

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.

04 · Data handling

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.

05 · Key handling

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:

schema.sql header comment, verbatim
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.

06 · Storage

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.

  1. One readable JSON file per call. Vendor, model identifier, prompt version, a hash of the request, and the response. You can open it.
  2. 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.
  3. Deleting it is safe. It is a replay cache. Removing the directory removes the copies, and the tool asks again before it records again.
  4. 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.

Password and session parameters, as declared in lib/crypto.js and lib/session.js.
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.
Raising the cost

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.

Timing

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.

Rate limits

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.

07 · Logs and measurement

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.

  1. 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-ev attribute 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 with navigator.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.
  2. A random session identifier that dies with the tab. Nine random bytes from the browser's own CSPRNG, held in sessionStorage under the key bre_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 constant nostorage and nothing breaks.
  3. 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 direct or internal. 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.
  4. 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.
  5. 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.
  6. The content security policy allows scripts from this origin only. script-src 'self' and connect-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.

08 · Retention

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.

01

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.

02

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.

03

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.

10 · What is not deployed yet

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.

11 · What this page does not claim

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.

What Blueprint does not detect

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.