A chat interface for a Diffuse Enterprise deployment. People in your organisation sign in, pick a model your deployment serves, and chat. Nothing leaves your infrastructure: the interface runs on your machines and talks to your coordinator.
Built on LibreChat, deployed upstream and unmodified, pinned by digest. This repository does not fork LibreChat and does not patch it. Everything it does goes through LibreChat's own configuration or its own HTTP API, so an upstream security fix is a digest change here rather than a merge.
Documentation: Chat interface on the Diffuse Enterprise documentation site. That page is the one to send to somebody who has not deployed this before; this file is the repository's own reference.
Contributions are not accepted. This is a commercial product's deployment tooling, published so customers can read what runs on their own machines. Issues and pull requests are not monitored; support goes through your contract.
Two profiles, one command apart. The difference is what the audit trail on your deployment can tell you afterwards.
| developer | enterprise | |
|---|---|---|
| who it is for | one person, one laptop | an organisation |
| credential | one ordinary API key | one gateway credential, --act-as |
| who the deployment sees | the key | the person who signed in |
| an audit row reads | apikey/V7S80Q12 |
identity/6a8f44f9 via apikey/QQ5WDKKE |
| model catalogue | what the key may call | what that person may call |
| rate limits | shared by everyone | per person |
| registration | open until the first account | closed |
| who may use it | anyone with an account here | people an operator imported |
Take the enterprise profile the moment "who generated this" is a question somebody could be asked. Only that profile puts a person's name on the row.
A Diffuse Enterprise deployment that already works: a coordinator
installed, and at least one model served, so that GET /v1/models answers.
Installing that is a different job and is documented at
Deployment.
Two different programs are involved, and it matters which machine each runs on:
| command | what it is | where it runs |
|---|---|---|
diffuse-coordinator |
the coordinator binary, already installed from diffuse-coordinator_amd64.deb |
on the coordinator host |
./diffuse-chat |
a shell script in this repository, at its root | on the machine that runs the chat interface |
They may be the same machine. Nothing here requires otherwise.
The chat host needs Docker with the compose plugin, and network access to
your coordinator's /v1 port.
./diffuse-chat is not installed by any package and there is no copy of it on
the coordinator. It is the script at the root of this repository:
git clone https://github.com/diffuse-systems/diffuse-chat.git
cd diffuse-chatEvery ./diffuse-chat command below runs from inside that directory.
cp .env.example .envFill in three values:
| value | where it comes from |
|---|---|
the /v1 endpoint |
your coordinator's host and API port, e.g. https://coordinator.internal:8443/v1 |
the path to ca.crt |
copy /etc/diffuse/ca.crt from the coordinator to this machine |
| a credential | created on the coordinator, in step 3 |
./diffuse-chat up writes the rest of .env itself, LibreChat's four secrets
and the service account it uses to own the shared agent, into the same file,
mode 0600. .env is not committed and must not be.
One ordinary API key, created on the coordinator, not here:
# on the coordinator host
diffuse-coordinator apikey create --name chatPut the key it prints into .env, then, in this repository:
./diffuse-chat up
./diffuse-chat doctorOpen http://127.0.0.1:3080, create your account, and chat. Registration closes
by itself: the next up sees a person and shuts the door behind you.
Two commands on the coordinator. The first issues the gateway credential, the only kind allowed to say which person a request is for; the second tells the deployment who those people are:
# on the coordinator host
diffuse-coordinator apikey create --name chat --act-as
diffuse-coordinator identity import users.csvThen, in this repository:
./diffuse-chat up --enterprise
./diffuse-chat doctorRegistration is closed in this profile. Accounts are not created here; they are
imported on the coordinator, and somebody signing in who is not in users.csv
is refused by the endpoint with a message saying so.
The gateway credential is a master key for your organisation's traffic. Read
THREAT_MODEL.md before you deploy it: one page, and the
first section is the one that matters.
All three run in this repository:
./diffuse-chat doctor # what is actually working, and if not, why
./diffuse-chat down # stop it
./diffuse-chat down --volumes # and forget the conversations and accountsdoctor checks the things that break in practice: the images running are the
digests this repository pins, LibreChat answers, the deployment's CA is mounted
where node will look for it, the TLS chain validates from inside the
container, the endpoint answers this credential the way this profile expects,
and the default agent exists and is shared.
APP_TITLE and CUSTOM_FOOTER in .env; images in branding/assets, mounted
over LibreChat's own. See branding/README.md. A
customer's logo never means a rebuilt image and never means a fork.
titleConvo is off by default, and turning it on has two costs rather than
being a preference. Both are set out under
Conversation titles are off, and why.
Both live elsewhere because they are longer than a README should carry, and because an operator needs them before touching this repository rather than while reading it:
- The threat model,
THREAT_MODEL.md: what putting a chat interface in front of the deployment adds. The gateway credential as a master key, what bounds it, what is not mitigated and why. - The limitation to know before a user reports it: a refusal from the
deployment is not shown to the person as a refusal, and the error they see
blames the model rather than the cause. What an operator does about it is one
command on the coordinator, and it is written up under
The limitation to know before a user reports it.
The measurement behind it is in
evidence/refusal-in-the-ui.md.
The rest of this file is for whoever maintains this repository or scripts
against the same stack. Five facts, each of which cost real time to find, all
measured against ghcr.io/danny-avila/librechat@sha256:c5db3331…, the digest
this repository pins, not against a tag and not against the development branch.
There is no /api/ask route. Everything goes through /api/agents/chat and
requires an agent_id; a custom endpoint is reached through an agent that names
it as a provider. Out of the box the composer says "Please select an Agent"
and nothing can be sent.
So ./diffuse-chat up provisions one, called Diffuse, and shares it with
everybody who can sign in. A shipped profile that told each user to build their
own agent first would not be a product.
It is created through POST /api/agents and shared through
PUT /api/permissions/agent/<_id>, which is what the UI does. Seeding the
agents collection in Mongo directly, the obvious shortcut, and what the smoke
test tried first, half-works: the document appears, and then a plain user is
refused their own agent with "Insufficient permissions to access this
agent", because v0.8.7 gates agents on ACL rows in a separate collection that a
hand-written document does not create. Going through the API makes LibreChat
write its own ACL rows, which is also a schema this repository then never has to
track.
Two smaller traps in the same corner: the permissions API is keyed on the Mongo
_id, not on the id string the rest of the agent API uses (passing the wrong
one answers 400 Invalid resource ID), and the agent schema requires a model
even though the person picks one afterwards.
v0.8.7 hides the endpoint picker and the model selector by default. Both configs here re-enable them, so a person can see which deployment they are talking to, but that is a convenience, not what makes the chat work. Re-enabling them was tried during the smoke test and the composer still demanded an agent. If you remove the provisioning step above expecting these two settings to cover it, you get a stack nobody can send a message from.
api/server/middleware/uaParser.js answers {"message":"Illegal request"} to
any request whose user agent is not a browser. It applies to the whole API and
looks nothing like a permission problem: it cost the smoke test more than every
other obstacle combined, because agent creation and every chat attempt failed
identically for a reason that was neither.
So every call ./diffuse-chat makes to LibreChat presents a browser user
agent, with a comment in the source saying why. If you script against this
stack yourself, do the same.
Summarised above and written up in full on the documentation site. The mechanism is that the 403 refuses LibreChat's per-request catalogue fetch before the completion call ever happens, so LibreChat concludes the model is unavailable:
event: error
data: {"error":"{ \"type\": \"illegal_model_request\", \"info\": \"Diffuse|diffuse-demo\" }"}
There is one configuration that changes it, and it is a trade rather than a fix:
with models.fetch: false and the model named literally in default, LibreChat
calls the endpoint anyway and logs the deployment's real sentence verbatim, but
the person then sees an empty reply instead of an error and the catalogue
stops being filtered per person. Shipped as fetch: true for that reason, with
the measurement in evidence/refusal-in-the-ui.md.
They are substituted in apiKey and baseURL and arrive literally inside
models.default, where they would show up in the picker as their own name. And
models.default may not be empty, even with fetch: true: the config schema
requires at least one element. So both config files carry a literal placeholder
there, which is only ever displayed when the deployment cannot be reached.
THREAT_MODEL.md: the façade side of design 021 §6.evidence/smoke-verdict.md: what LibreChat actually sends, captured whole, from two different accounts.evidence/delegation-demo.md: the same stack in front of a real deployment built from the release packages: two people, two audit rows, one credential.evidence/refusal-in-the-ui.md: what a refused person actually sees, read off the stream, and the one configuration that changes it.tests/acceptance.sh: the promise above, measured from a clean machine.compose.smoke.yaml,receiver/: the capture stub those two were measured with. Kept because a version bump re-runs that measurement rather than assuming it still holds.
