Everything in this section is Brobridge, used unchanged. Broapp does not implement any of it and does not weaken any of it. Brobridge's own THREAT-MODEL.md is the authoritative document; this page says how a Broapp application sits inside it.
Over one loop: the tab redeems the one-time launch token at the trust fence and
gets a session cookie; it fetches the single embedded document; it calls a typed
operation, which the fence admits and the host validates and runs. A request
from any other origin, including another port on 127.0.0.1, meets the same
fence and is refused before routing.
Loopback binding. The host binds 127.0.0.1 on an ephemeral port. Broapp
does not forward Brobridge's allowNonLoopback option at all, so a Broapp
application cannot grow LAN exposure through a configuration change. There is a
test asserting the bound host is loopback.
A one-time launch token. The URL the host prints carries a token that is
valid once, for two minutes. Redeeming it sets an HMAC-signed session cookie and
redirects to / without the token, so the token-bearing URL leaves browser
history. A second use of the same token is refused. There is a test.
A trust fence. Every request is checked for a Host and Origin the host
recognises before anything else runs — before routing, before a body is read,
before an allocation proportional to the request. A page on another origin
cannot reach the bridge. There is a test with a foreign Origin.
A session cookie. HttpOnly; SameSite=Strict; Path=/. Without it, every
route answers 403.
A closed route table. /, /ws, /rpc, and nothing else. No path reaches
the filesystem, so there is no traversal to defend against — an unrouted path is
a 404 that never touched a disk.
Refusal of framing and other cross-document loads. The fence allows only
Sec-Fetch-Site: same-origin or none. A page on another origin that puts the
application in an <iframe> sends cross-site (or same-site), so the request
is refused with a 403 and no frame is rendered. Browsers set Sec-Fetch-Site
themselves and a page cannot forge it. This — not a CSP directive — is what
stops the application being framed.
Rate-limited authentication failures. Twenty per minute per remote address.
A restrictive Content-Security-Policy. Brobridge sets Referrer-Policy,
Cache-Control: no-store and X-Content-Type-Options but no CSP, and only lets
a host application supply a body — so Broapp puts the policy in a
<meta http-equiv> at the top of the generated document:
default-src 'none';
script-src 'sha256-…';
style-src 'sha256-…'; (or 'none' when the application ships no CSS)
img-src 'self' data:;
font-src 'self' data:;
connect-src 'self' ws://127.0.0.1:*;
base-uri 'none'; form-action 'none'; object-src 'none'
default-src 'none' means a directive nobody thought about fails closed. The
inline script is pinned by hash, not permitted by 'unsafe-inline', and the
hash is computed from the exact bytes the browser will execute — there is a test
that recomputes it from the served document. connect-src names the one host
the bridge can bind rather than allowing a bare ws:, which would allow any
host on the network.
What is deliberately not in it. No frame-ancestors. It is one of three
directives — with report-uri and sandbox — that the CSP specification
requires user agents to ignore when a policy arrives in a <meta> element,
and a <meta> element is the only way a host application can express a policy
here, because Brobridge writes the response headers and offers no hook for
adding one. Declaring it would put an inert directive in the document and invite
a reader to count it as protection.
Framing is refused anyway, one layer down, and by something that does work: see below.
An off-origin build check. broapp build fails if the produced document
loads anything from another origin — a src, an href, a CSS url(), an
@import. A local application that pulls a font from a CDN stops working
offline and tells a third party when the user runs it. (A URL that is only a
string in JavaScript is allowed: React embeds documentation URLs in its error
messages, and a string fetches nothing. The CSP is what enforces this at
runtime; the build check is so the failure happens where a developer can see
it.)
Host-side validation of every operation. Input is checked against the contract before a handler runs. The browser checks too, but only as a convenience — a test bypasses the client entirely and proves the host still refuses.
A public/internal error boundary. A PublicError a handler raises
deliberately keeps its message. Anything else is logged on the host with its
stack and reaches the browser as a fixed sentence. There is a test that throws
an error containing a filesystem path and a credential-shaped string and asserts
neither reaches the browser.
Launch credentials stay out of files. The launch URL is written to the
terminal, deliberately, because a user whose browser did not open needs it.
Nothing in Broapp writes it to a log file, and Brobridge's no-referrer and
no-store headers keep the browser from persisting it.
Say this plainly to your users, because it is easy to assume otherwise.
Loopback HTTP is not encryption. Traffic does not leave the machine, but it is not TLS. Anything with permission to inspect local network traffic on the machine can read it.
Authentication is not a sandbox. The host process runs with the invoking user's permissions and can do anything that user can do. The session cookie decides who may call your operations; it does not constrain what those operations are allowed to do once called.
A compromised frontend has your session. Code running in the page can invoke every operation the page can invoke. That is why the CSP is hash-pinned and why the build refuses off-origin scripts — but it means the security of your application is bounded by the security of the operations you expose. Do not expose an operation you would be uncomfortable with arbitrary page code calling.
Local malware is out of scope. Another process running as the same user can read the data directory, read the process's memory, and attach a debugger. No local application defends against this, and one that claims to is lying.
AI providers see what you send them. The AI layer is off until a user
configures it, and a local provider keeps everything on the machine. A remote
one is given the message, the conversation history, the full text of every
document the application resolved for that turn, search snippets, the tool
descriptions, and each tool call's input and output — that is what generating an
answer requires. <AiSettings/> states this on screen, worded for the provider
chosen; an application must not hide the notice. Whether a provider counts as
local is decided by its address, so a loopback proxy that forwards elsewhere
would be reported as local.
A provider that is off is sent nothing. Settings keep more than one
provider, but only the one in use and the ones a person turned on may be sent
anything — not a request for a model list, not a turn. A model reference
(<provider>:<model>) in a stored conversation, a task or a tier file that
names a provider that is off is refused before anything is sent; only that
provider's own Test button contacts it while it is off. Enabling is what stands
between a line in a file and a person's work leaving their computer.
A model reference can move a conversation off this computer. Pinning a
conversation, a task or a tier to a model of a hosted provider sends what that
turn carries to that provider, while the rest stay where they were. So the
person is told wherever a model is chosen or named — the conversation's picker
and its heading, the conversation list, every tier and task select, and the
Overview's running task — in words: on this computer or sent to <provider>.
A choice that moves a conversation across that line also puts one sentence
under the picker until the next message is sent. Nothing falls back from a
failed provider to another.
Keys are per provider, under one switch. Each provider's key is stored under its own name; "Remember key on this computer" covers every one of them, and turning it off moves all of them off the disk.
Rendered markdown is narrowed, not trusted. broapp-ai-elements turns
assistant text into React elements — never raw HTML, and never through
dangerouslySetInnerHTML. Links and images are removed before rendering: their
words survive, their addresses do not. A document the model was shown that asks
it to emit a link therefore produces text and no way out of the page. The
plain-text panel in broapp/ai/react renders no markup at all.
An image is sent to the provider with its message. Attaching or pasting one
into the chat panel sends it, once, with the turn it arrives on — so the answer
to "does this leave my computer" is the one above: a local provider keeps it on
the machine, a remote one is given it. Later turns carry a [image: name]
placeholder rather than the picture again.
The API key is a file, not a vault. It is written to
<dataDir>/ai/secrets.json with mode 0600 — the posture of
~/.aws/credentials, not of a keychain. That rules out another user on the
machine and a world-readable backup. It does not rule out another process
running as the same user, for the reason in the paragraph above. A user who
does not want the key on disk can turn "Remember key on this computer" off, and
it is held in memory for the life of the process instead. See
the AI layer.
Neither Broapp nor Brobridge has been independently audited. Brobridge's threat model is written down and its invariants have tests. That is not the same as an audit, and this project does not claim it is.
These are the ones that matter.
Never expose a shell. An operation that runs a command string turns a local application into remote code execution the moment anything else goes wrong. No amount of escaping fixes this; do not add the operation.
Never expose unrestricted filesystem access. An operation taking an
arbitrary path can read the user's SSH keys and browser profile. If your
application needs files, give the host an explicit root and let the browser
name files relative to it — see the
file-processor example, which enforces
containment on the resolved path so that .., absolute paths, and symlinks are
all caught by the same check.
Validate at the boundary, then again for your own rules. The contract enforces types and bounds. It does not know that a title of only whitespace is not a title, or that this identifier must belong to the current user.
Keep paths and driver messages out of PublicError. Its message is shown to
the browser verbatim. That is the point of it, and the reason to be careful.
Bound everything. Array lengths, string lengths, file sizes, listing counts. The contract's schemas are where to do it.
Do not retry a mutating operation automatically. A call that was in flight when the socket dropped may have run. Broapp does not retry it, and neither should you without idempotency you can point at.
Security issues in the transport, the trust fence, or the authentication flow belong to Brobridge. Issues in the generator, the build, the contract layer, or the lifecycle belong here — open a private security advisory on this repository rather than a public issue.