Compare commits
130
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2422432b85 | ||
|
|
5402b9e758 | ||
|
|
96b1201f72 | ||
|
|
852e52dc35 | ||
|
|
ac75c504f6 | ||
|
|
c6d03c8f85 | ||
|
|
fb14577626 | ||
|
|
9119e6726a | ||
|
|
70c87e47c2 | ||
|
|
342c353ebc | ||
|
|
0786d97286 | ||
|
|
af4f96ad13 | ||
|
|
b263e125c4 | ||
|
|
fa4ba8d989 | ||
|
|
2a89e6a19d | ||
|
|
5eacc1c116 | ||
|
|
df722f81b0 | ||
|
|
278c1fda1a | ||
|
|
869d16cc65 | ||
|
|
07d5a60377 | ||
|
|
33b5cf73e8 | ||
|
|
98cd517b8a | ||
|
|
c5fd1ba96f | ||
|
|
9b5eff63fb | ||
|
|
08ad0c42ee | ||
|
|
4f07c2d3ac | ||
|
|
492a64fa43 | ||
|
|
e0b9c0cd7e | ||
|
|
c95bae161b | ||
|
|
a81249eda2 | ||
|
|
1c863ddb95 | ||
|
|
49ce027c5b | ||
|
|
4e44793aaf | ||
|
|
bf356cd9a3 | ||
|
|
9b66409343 | ||
|
|
4069ee12f6 | ||
|
|
3cc32708d9 | ||
|
|
f1be37a725 | ||
|
|
5b432079b7 | ||
|
|
8b1f6d4a65 | ||
|
|
387269e245 | ||
|
|
2d3291d493 | ||
|
|
fa1f38ced2 | ||
|
|
44f040c27f | ||
|
|
62158c16cc | ||
|
|
cdb62fc247 | ||
|
|
7c9c8ef578 | ||
|
|
1fec2b16eb | ||
|
|
f0ed33371c | ||
|
|
a0e7c8d405 | ||
|
|
6b41382bef | ||
|
|
deb53eabc8 | ||
|
|
27415161f9 | ||
|
|
ddbbe01801 | ||
|
|
2fdda5a201 | ||
|
|
be2ebd1bc4 | ||
|
|
fbfca9624e | ||
|
|
e0732af1fd | ||
|
|
6cf50e6a5d | ||
|
|
f6dcde3cbb | ||
|
|
3ee59e2dcc | ||
|
|
95c6258594 | ||
|
|
682b5530be | ||
|
|
f27621d4de | ||
|
|
29f0d2674a | ||
|
|
cdb705d89e | ||
|
|
87e0a085f4 | ||
|
|
5605f32527 | ||
|
|
3cd441999b | ||
|
|
d14395e4ec | ||
|
|
64c2cbcc1e | ||
|
|
ab96402d15 | ||
|
|
0a0a66ae28 | ||
|
|
33a9e3e2a4 | ||
|
|
1e91cfcd1e | ||
|
|
f23513a41f | ||
|
|
a3fabdd53c | ||
|
|
c9a6e23154 | ||
|
|
12da5c1268 | ||
|
|
1f3f0ce9b8 | ||
|
|
fde5d143c5 | ||
|
|
0e708a99c6 | ||
|
|
467d19357d | ||
|
|
6a0d092e53 | ||
|
|
bac352f50e | ||
|
|
34a23a34f3 | ||
|
|
fa979a5ed9 | ||
|
|
5bb99b8592 | ||
|
|
e7c86b16ba | ||
|
|
d3c3c8f8f7 | ||
|
|
884340427f | ||
|
|
9699ff08cc | ||
|
|
0d4a62a2da | ||
|
|
f3a757c9a5 | ||
|
|
196a3387ad | ||
|
|
7ea5e1a4a1 | ||
|
|
1e57fd8f00 | ||
|
|
af2b6eacd0 | ||
|
|
83942f7bd0 | ||
|
|
fd2cf4089a | ||
|
|
67a69dc8b6 | ||
|
|
d047ced4c6 | ||
|
|
d0ec489af7 | ||
|
|
ccebba45af | ||
|
|
b9953ed086 | ||
|
|
4f995be9f1 | ||
|
|
fed58a3437 | ||
|
|
0f657b7cf4 | ||
|
|
a3defe1bed | ||
|
|
e175f07cb5 | ||
|
|
a5e33b73dd | ||
|
|
21b31b3a0a | ||
|
|
9d520130f4 | ||
|
|
7b0b13a54a | ||
|
|
bed53d6cbd | ||
|
|
121afa101a | ||
|
|
d865501fbf | ||
|
|
adabc8bccd | ||
|
|
3c346e420f | ||
|
|
7313f821e3 | ||
|
|
d8bd033f05 | ||
|
|
c022a0b52e | ||
|
|
85f5723357 | ||
|
|
a647284170 | ||
|
|
888ac8c299 | ||
|
|
dcdde204b0 | ||
|
|
0a79c80483 | ||
|
|
d2e90c6dc3 | ||
|
|
7e2fa8c0a3 | ||
|
|
85a6045389 |
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
# Generated by the documentation build (see publishing/build.sh)
|
||||||
|
.site-src/
|
||||||
|
.site-build/
|
||||||
|
.venv-docs/
|
||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Woodpecker CI pipeline for co-op/docs — THIN on purpose.
|
||||||
|
#
|
||||||
|
# It does NOT re-implement the build. The repository's publishing layer is the
|
||||||
|
# single source of truth and this pipeline only wires it up:
|
||||||
|
#
|
||||||
|
# publishing/requirements.txt pinned MkDocs + Material toolchain
|
||||||
|
# publishing/build.sh stage Markdown -> .site-src/, render .site-build/
|
||||||
|
# publishing/deploy.sh push .site-build/ into the Cloudron Surfer app
|
||||||
|
#
|
||||||
|
# Any change to how the site is built belongs in publishing/, never here.
|
||||||
|
#
|
||||||
|
# Run model: MANUAL ONLY for now. The Gitea webhook that fires this on push is
|
||||||
|
# deliberately NOT wired — the poll watcher (docs-deploy-poll.service on the
|
||||||
|
# agent host) remains the live auto-deploy path until a manual run has proven
|
||||||
|
# the pipeline. Once it has, add `push` to the `when` block below and register
|
||||||
|
# the webhook in Gitea.
|
||||||
|
#
|
||||||
|
# Backend note (IMPORTANT — this is why there are no container images here):
|
||||||
|
# The co-op's Woodpecker agent runs as a Cloudron app, which has no Docker
|
||||||
|
# socket, so it uses Woodpecker's `local` backend. Under `local` each step is
|
||||||
|
# run as a shell command INSIDE the agent container, and the step's `image:`
|
||||||
|
# field selects the SHELL, not a container image. Every tool the steps need is
|
||||||
|
# therefore baked into the agent image (co-op/woodpecker-agent):
|
||||||
|
# python3 + the pinned MkDocs toolchain (venv at /opt/docs-venv, mirroring
|
||||||
|
# publishing/requirements.txt), node + the cloudron CLI, bash, git, tar.
|
||||||
|
# If the toolchain pin changes, rebuild that image — see its Dockerfile.
|
||||||
|
#
|
||||||
|
# Secrets required (Woodpecker repo secret, name `cloudron_token`):
|
||||||
|
# cloudron_token = the Cloudron API token used by publishing/deploy.sh
|
||||||
|
# Do not put credentials in this file.
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: manual
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: bash
|
||||||
|
commands:
|
||||||
|
# Reuses the toolchain pre-installed in the agent image; build.sh only
|
||||||
|
# creates/installs a venv if the one given is missing, which it is not.
|
||||||
|
- publishing/build.sh /opt/docs-venv
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
image: bash
|
||||||
|
commands:
|
||||||
|
# deploy.sh drives the Cloudron `cloudron` CLI (a node program) via
|
||||||
|
# `cloudron exec`; node, the CLI, bash, git and tar are all in the agent
|
||||||
|
# image. The build it publishes is the shared workspace .site-build/.
|
||||||
|
- publishing/deploy.sh
|
||||||
|
environment:
|
||||||
|
CLOUDRON_TOKEN:
|
||||||
|
from_secret: cloudron_token
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Inference Cooperative
|
# Inference Cooperative docs
|
||||||
|
|
||||||
**AI, governed together.**
|
**AI, governed together.**
|
||||||
|
|
||||||
The Inference Cooperative is a member-governed project providing private AI
|
The Inference Cooperative is a member-governed project providing more ethical AI
|
||||||
inference. Members pay dues on a sliding scale and, in return, get private AI
|
inference. Members pay dues on a sliding scale and, in return, get AI
|
||||||
chat and API access through shared, cooperatively governed infrastructure. We
|
chat and API access through shared, cooperatively governed infrastructure. We
|
||||||
are fiscally sponsored by [Metagov](https://metagov.org), a nonprofit, and
|
are fiscally sponsored by [Metagov](https://metagov.org), a nonprofit, and
|
||||||
funded through our [Open Collective](https://opencollective.com/inference-cooperative).
|
funded through our [Open Collective](https://opencollective.com/inference-cooperative).
|
||||||
@@ -11,19 +11,24 @@ funded through our [Open Collective](https://opencollective.com/inference-cooper
|
|||||||
## Start here
|
## Start here
|
||||||
|
|
||||||
- **New here?** → [Getting started](getting-started.md) — join, use the chat, and get involved in governance.
|
- **New here?** → [Getting started](getting-started.md) — join, use the chat, and get involved in governance.
|
||||||
|
- **Why does this exist?** → [Background](background.md) — a one-page summary of the mission and history.
|
||||||
- **Which models can I use?** → [Models](models.md) — the full catalog of providers and models.
|
- **Which models can I use?** → [Models](models.md) — the full catalog of providers and models.
|
||||||
- **Want API access?** → [API access](api-access.md) — use our models programmatically.
|
- **Want API access?** → [API access](api-access.md) — use our models programmatically.
|
||||||
|
- **Want community chat?** → [Getting started · Matrix](getting-started.md) — our federated Matrix server, including how to connect from any client and how to use an existing Matrix account.
|
||||||
- **Curious how it works?** → [Infrastructure](infrastructure.md) — a map of the stack and the codebase.
|
- **Curious how it works?** → [Infrastructure](infrastructure.md) — a map of the stack and the codebase.
|
||||||
|
- **Wondering about the bot?** → [Working with the agent](working-with-the-agent.md) — what the co-op's agents do, who can instruct them, and what always needs a human.
|
||||||
- **Need help or have an idea?** → [Support & feature requests](https://git.inference.coop/co-op/support) — open an issue.
|
- **Need help or have an idea?** → [Support & feature requests](https://git.inference.coop/co-op/support) — open an issue.
|
||||||
|
|
||||||
## Key links
|
## Key links
|
||||||
|
|
||||||
- **Website:** https://inference.coop
|
- **Website:** [inference.coop](https://inference.coop)
|
||||||
- **AI Chat:** https://chat.inference.coop
|
- **AI Chat:** [chat.inference.coop](https://chat.inference.coop)
|
||||||
- **Member Dashboard:** https://dashboard.inference.coop
|
- **Matrix chat:** [matrix.inference.coop](https://matrix.inference.coop)
|
||||||
- **Governance forum:** https://forum.inference.coop
|
- **Member Dashboard:** [dashboard.inference.coop](https://dashboard.inference.coop)
|
||||||
- **Source code:** https://git.inference.coop
|
- **Governance forum:** [forum.inference.coop](https://forum.inference.coop)
|
||||||
- **Contact:** info@inference.coop
|
- **Co-op calendar:** [cloud.inference.coop](https://cloud.inference.coop/apps/calendar/p/Ltb6LAbt7NWHH7Td)
|
||||||
|
- **Source code:** [git.inference.coop](https://git.inference.coop)
|
||||||
|
- **Contact:** [info@inference.coop](mailto:info@inference.coop)
|
||||||
|
|
||||||
## Governance & legal
|
## Governance & legal
|
||||||
|
|
||||||
@@ -31,7 +36,3 @@ funded through our [Open Collective](https://opencollective.com/inference-cooper
|
|||||||
- [Open questions](open-questions.md) — decisions currently up for member discussion.
|
- [Open questions](open-questions.md) — decisions currently up for member discussion.
|
||||||
- [Terms of Service](terms-of-service.md)
|
- [Terms of Service](terms-of-service.md)
|
||||||
- [Privacy Policy](privacy-policy.md)
|
- [Privacy Policy](privacy-policy.md)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This documentation lives in a public git repository. Contributions welcome.*
|
|
||||||
+44
-4
@@ -10,10 +10,32 @@ client works.
|
|||||||
- **Auth:** a bearer API key (create one in the [Member Dashboard](https://dashboard.inference.coop))
|
- **Auth:** a bearer API key (create one in the [Member Dashboard](https://dashboard.inference.coop))
|
||||||
- **Format:** OpenAI chat completions — `POST https://gateway.inference.coop/v1/chat/completions`
|
- **Format:** OpenAI chat completions — `POST https://gateway.inference.coop/v1/chat/completions`
|
||||||
- **Models:** see the [model list](models.md).
|
- **Models:** see the [model list](models.md).
|
||||||
|
- **Not only chat:** the gateway also serves a decision endpoint,
|
||||||
|
`POST https://gateway.inference.coop/v1/systemone` — see
|
||||||
|
[Decisions](models.md#decisions). It takes a different request shape, so
|
||||||
|
it is not reachable through an OpenAI SDK's `chat.completions` call.
|
||||||
|
|
||||||
API usage draws from the **same monthly allowance as chat** — there's no separate
|
API usage draws from the **same monthly allowance as chat** — there's no separate
|
||||||
quota.
|
quota.
|
||||||
|
|
||||||
|
## What the co-op stores about your requests
|
||||||
|
|
||||||
|
Usage records, and nothing else: the model, token counts, cost, and which key made
|
||||||
|
the request — that is how your monthly metering works. **Your prompts and the
|
||||||
|
responses are not stored on our side.** The API path keeps no conversation, so there
|
||||||
|
is no transcript to delete later.
|
||||||
|
|
||||||
|
Two things worth being precise about:
|
||||||
|
|
||||||
|
- The **chat interface** is different — it saves your conversations so you can revisit
|
||||||
|
them. If you want the API's behaviour there instead, use a **temporary chat**.
|
||||||
|
- The **provider** is a separate question. Whether a model's operator retains anything
|
||||||
|
is set by that provider, not by us, and it varies by model — the
|
||||||
|
[model list](models.md) distinguishes policy-based zero retention from
|
||||||
|
enclave-based protection.
|
||||||
|
|
||||||
|
Full detail, including what we can and cannot see: [privacy policy](privacy-policy.md).
|
||||||
|
|
||||||
## Get a key
|
## Get a key
|
||||||
|
|
||||||
Log in to the [Member Dashboard](https://dashboard.inference.coop) and create an
|
Log in to the [Member Dashboard](https://dashboard.inference.coop) and create an
|
||||||
@@ -48,6 +70,25 @@ resp = client.chat.completions.create(
|
|||||||
Any OpenAI-compatible client works the same way: set the base URL to
|
Any OpenAI-compatible client works the same way: set the base URL to
|
||||||
`https://gateway.inference.coop/v1` and the API key to your member key.
|
`https://gateway.inference.coop/v1` and the API key to your member key.
|
||||||
|
|
||||||
|
## Decisions (Metis)
|
||||||
|
|
||||||
|
Alongside the chat models, the gateway exposes GreenPT's **Metis** decision model
|
||||||
|
at `POST https://gateway.inference.coop/v1/systemone`. It is the same key and the
|
||||||
|
same base URL — only the path and the request body differ. For what it is, the
|
||||||
|
three question types, and a full `curl` example, see
|
||||||
|
[Decisions in the model list](models.md#decisions).
|
||||||
|
|
||||||
|
Two practical notes:
|
||||||
|
|
||||||
|
- **It is not an OpenAI chat endpoint.** You send a `state` plus typed `questions`
|
||||||
|
and receive probabilities; there is nothing for an OpenAI SDK's
|
||||||
|
`chat.completions` to parse. Post the body directly (`curl`, `requests`,
|
||||||
|
`httpx`, or an SDK's generic POST).
|
||||||
|
- **Its usage is metered like any other call.** Each decision is billed at
|
||||||
|
**$0.12 per 1M input tokens**, figured from the exact token count GreenPT
|
||||||
|
returns (Metis generates no prose, so there is no output cost). Decisions draw
|
||||||
|
on the same monthly allowance as chat.
|
||||||
|
|
||||||
## Coding agents and other OpenAI-compatible clients
|
## Coding agents and other OpenAI-compatible clients
|
||||||
|
|
||||||
Any tool that speaks the OpenAI API works. Where a tool asks for provider or
|
Any tool that speaks the OpenAI API works. Where a tool asks for provider or
|
||||||
@@ -57,7 +98,7 @@ endpoint configuration:
|
|||||||
- **API type / protocol:** "OpenAI compatible" (or `openai-completions` where a
|
- **API type / protocol:** "OpenAI compatible" (or `openai-completions` where a
|
||||||
protocol string must be named) — not "Ollama", "Anthropic", or "Responses"
|
protocol string must be named) — not "Ollama", "Anthropic", or "Responses"
|
||||||
- **API key:** your member key (the `sk-…` string)
|
- **API key:** your member key (the `sk-…` string)
|
||||||
- **Model:** one from the [model list](models.md), e.g. `tinfoil/deepseek-v4-1-flash` or `greenpt/green-r`
|
- **Model:** one from the [model list](models.md), e.g. `tinfoil/deepseek-v4-1-flash` or `greenpt/glm-5.3-flash`
|
||||||
|
|
||||||
A typical provider entry looks like:
|
A typical provider entry looks like:
|
||||||
|
|
||||||
@@ -71,9 +112,8 @@ A typical provider entry looks like:
|
|||||||
"models": [
|
"models": [
|
||||||
{ "id": "tinfoil/deepseek-v4-1-flash" },
|
{ "id": "tinfoil/deepseek-v4-1-flash" },
|
||||||
{ "id": "tinfoil/gpt-oss-120b" },
|
{ "id": "tinfoil/gpt-oss-120b" },
|
||||||
{ "id": "tinfoil/glm-5-3-flash" },
|
{ "id": "tinfoil/glm-5.3" },
|
||||||
{ "id": "greenpt/green-r" },
|
{ "id": "greenpt/glm-5.3-flash" }
|
||||||
{ "id": "greenpt/green-l" }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Background
|
||||||
|
|
||||||
|
The Inference Cooperative exists to give people a real alternative for AI: a
|
||||||
|
service owned and directed by the people who use it, rather than rented from
|
||||||
|
an extractive industry. Members pay dues on a sliding scale and receive
|
||||||
|
private AI chat and API access through infrastructure the members govern
|
||||||
|
together.
|
||||||
|
|
||||||
|
We are a small cooperative by design, fiscally sponsored by the nonprofit
|
||||||
|
[Metagov](https://metagov.org) and managing member dues on
|
||||||
|
[Open Collective](https://opencollective.com/inference-cooperative).
|
||||||
|
For how to join and use it, see [Getting started](getting-started.md);
|
||||||
|
for our governance model, see the [Charter](charter.md).
|
||||||
|
|
||||||
|
## How we got here
|
||||||
|
|
||||||
|
The cooperative launched in September 2026, growing out of research on democratic
|
||||||
|
digital infrastructure through Metagov's [Collective Governance for AI:
|
||||||
|
Points of Intervention](https://metagov.org/cg-ai/). It was founded by
|
||||||
|
[Nathan Schneider](https://nathanschneider.info), who formerly co-founded
|
||||||
|
Social.coop, Start.coop, and the Media Economies Design Lab at the University
|
||||||
|
of Colorado Boulder.
|
||||||
|
|
||||||
|
It began with a single privacy-first provider and a deliberately small
|
||||||
|
toolset: a chat, an API gateway, and the basics for membership and payments.
|
||||||
|
From there it grew the way co-ops do: in response to what members asked for.
|
||||||
|
Green-energy providers were added, along with more diverse models.
|
||||||
|
A member dashboard made usage and spending transparent. A federated
|
||||||
|
[Matrix](https://matrix.inference.coop) server, a
|
||||||
|
[forum](https://forum.inference.coop), and a newsletter gave members places to
|
||||||
|
talk to each other and to the project. The
|
||||||
|
code that runs the member-facing services is available in public
|
||||||
|
repositories.
|
||||||
|
|
||||||
|
In October 2026, members adopted the co-op's first
|
||||||
|
[charter](charter.md) — a democratic constitution setting out values, roles,
|
||||||
|
and decision-making, with a commitment to redesign governance as the
|
||||||
|
community grows past 250 members.
|
||||||
|
|
||||||
|
## Where we're going
|
||||||
|
|
||||||
|
The near-term road: more model choices with strong privacy and environmental
|
||||||
|
records, infrastructure hosted with values-aligned providers, and the
|
||||||
|
working-group structure the charter describes. The longer arc: proving that
|
||||||
|
people can govern their AI the way they govern a credit union or a land trust
|
||||||
|
— and sharing what we learn with the co-ops that come after us.
|
||||||
+33
-16
@@ -1,35 +1,52 @@
|
|||||||
# Inference Cooperative Charter
|
# Inference Cooperative Charter
|
||||||
|
|
||||||
A cooperative for AI resources, governed by members.
|
## Preamble
|
||||||
|
|
||||||
|
The Inference Cooperative exists to enable the people who use its AI infrastructure to co-govern it. We came together to build and run an alternative to extractive, profit-driven AI—a service owned and directed by its members, committed to pursuing ethical practices, and governed cooperatively. This charter is our core governing document. It sets out how we make decisions together.
|
||||||
|
|
||||||
## Values
|
## Values
|
||||||
|
|
||||||
The Inference Cooperative seeks to advance accountable AI grounded in democracy, privacy, and sustainability.
|
In addition to the international [cooperative principles](https://www.ica.coop/en/cooperatives/cooperative-identity/), we share these basic commitments:
|
||||||
|
|
||||||
## Code of Conduct
|
- **Participation.** Our service is accountable to the people who use it, and they both can and should have an active role in shaping it.
|
||||||
|
- **Experimentation.** In order to learn together, we will maintain a spirit of exploration and experimentation, and be respectful of all members' levels of knowledge and expertise.
|
||||||
|
- **Sustainability.** We commit, wherever possible, to minimizing harm to natural ecologies and to promoting environmental justice.
|
||||||
|
- **Trustworthiness.** We seek to provide reliable service to one another and protect each other's privacy, in contrast to an industry widely characterized by surveillance.
|
||||||
|
- **Transparency.** The co-op should make its operations and decisions as available as possible to members, and to share our learnings with the wider world.
|
||||||
|
|
||||||
Members must adhere to the [Contributor Covenant](https://www.contributor-covenant.org/).
|
## Roles
|
||||||
|
|
||||||
## Membership
|
- **Members** are the heart of the co-op. All account holders currently [contributing monthly dues](https://opencollective.com/inference-cooperative) are considered members and may participate in decision-making and working groups. Members may be sanctioned or removed for violating co-op rules by the GM or a designated working group.
|
||||||
|
- A **general manager** oversees day-to-day operations and implements member decisions, through their own actions or additional roles that they delegate. The GM is accountable to members and may be replaced by them through a policy decision.
|
||||||
|
- **Working groups** make and carry out operational decisions within their own purview. They should be formally established, modified, and disbanded through a policy decision, and they are obligated, at minimum, to publish to members their membership, meeting schedule, and major activities.
|
||||||
|
- A **spokescouncil** coordinates across working groups, and all working groups are responsible for ensuring that at least one member consistently participates. The spokescouncil meets monthly, and meetings are open to all members.
|
||||||
|
|
||||||
Members are dues-paying users of the service. Join at [opencollective.com/inference-cooperative](https://opencollective.com/inference-cooperative).
|
## Decisions
|
||||||
|
|
||||||
### Deliberation
|
The co-op operates through the practice of modified [policy governance](https://en.wikipedia.org/wiki/Policy_Governance). Member decisions should seek to instill responsibility and trust in role-holders, while guiding them only through goal-setting and necessary constraints. Role-holders should act on the basis of their expertise and the co-op's values, as well as through consultation with members.
|
||||||
|
|
||||||
Members can participate in deliberation about co-op business on Loomio at [forum.inference.coop](https://forum.inference.coop). Decisions are not currently binding, but in the future they will be.
|
**Policy.** Any member may propose a policy in the co-op's [forum](https://forum.inference.coop/). A proposal requires at least one vote from another member and stays open for 7 days. It passes by consent as long as it receives support from at least 60% of voting members. If a member raises a block—a reasoned objection that the proposal would seriously harm the cooperative—it requires 75% support. The proposal's author is always encouraged to revise the proposal based on feedback from other members and restart the process.
|
||||||
|
|
||||||
### Removal
|
**Charter.** Changes to this charter require a proposal open for **14 days**, along with approval by 75% of voting members, regardless of blocks. Policy decisions must not contradict the charter.
|
||||||
|
|
||||||
Members may be removed by the General Manager for violations of the Code of Conduct or failure to contribute dues.
|
Proposals may be adjusted with modest changes during the open discussion period by the proposer, as long as changes are announced in the discussion.
|
||||||
|
|
||||||
## General Manager
|
This governance design is oriented around a relatively small community. Upon reaching 250 members, we commit to revising this charter with a larger community in mind.
|
||||||
|
|
||||||
The General Manager, initially Nathan Schneider, temporarily has full governance responsibilities over the cooperative.
|
## Disputes
|
||||||
|
|
||||||
## Evolution
|
We regard disputes as an opportunity to learn and grow. Members should try to work things out directly with each other first. When necessary, disputes either among members or between a member and the co-op should be brought to the disputes working group—or, if one does not exist, the GM. Disputes that cannot be adequately addressed within the co-op may be escalated to the parent legal entity, which may in turn forward them to the relevant legal jurisdiction.
|
||||||
|
|
||||||
The current governance model will evolve into a true member-governed cooperative upon or before reaching 50 members. The General Manager is responsible for ensuring the transition occurs within two weeks of that milestone. If it does not, the General Manager must resign and transfer the role to someone who does commit to that deadline, or the co-op is dissolved.
|
## Agreements
|
||||||
|
|
||||||
---
|
This charter is the core governing document. Members are also bound by the following agreements:
|
||||||
|
|
||||||
*Created by the [Inference Cooperative](https://inference.coop/).*
|
* [Code of Conduct](code-of-conduct.md)
|
||||||
|
* [Terms of Service](terms-of-service.md)
|
||||||
|
* [Privacy Policy](privacy-policy.md)
|
||||||
|
|
||||||
|
## Legal entity
|
||||||
|
|
||||||
|
The Inference Cooperative operates under the fiscal sponsorship of [Metagov](https://metagov.org/), and our governance is subject to [Metagov's fiscal sponsorship policy](https://metagov.pubpub.org/pub/7bj5ia5g/release/1). We commit to honor that agreement and to operate in a spirit that avoids causing harm to our fiscal host. Metagov may rescind a decision it deems at odds with its policies.
|
||||||
|
|
||||||
|
If that commitment is ever at odds with the cooperative's evolution, the cooperative may move to a different fiscal host. Changing fiscal hosts should occur through a revision of this Charter.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
# Contributor Covenant 3.0 Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We pledge to make our community welcoming, safe, and equitable for all.
|
||||||
|
|
||||||
|
We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
|
||||||
|
|
||||||
|
## Encouraged Behaviors
|
||||||
|
|
||||||
|
While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
|
||||||
|
|
||||||
|
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
|
||||||
|
|
||||||
|
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
||||||
|
2. Engaging **kindly and honestly** with others.
|
||||||
|
3. Respecting **different viewpoints** and experiences.
|
||||||
|
4. **Taking responsibility** for our actions and contributions.
|
||||||
|
5. Gracefully giving and accepting **constructive feedback**.
|
||||||
|
6. Committing to **repairing harm** when it occurs.
|
||||||
|
7. Behaving in other ways that promote and sustain the **well-being of our community**.
|
||||||
|
|
||||||
|
|
||||||
|
## Restricted Behaviors
|
||||||
|
|
||||||
|
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
|
||||||
|
|
||||||
|
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
|
||||||
|
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
|
||||||
|
3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits.
|
||||||
|
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
|
||||||
|
5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
|
||||||
|
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
|
||||||
|
7. Behaving in other ways that **threaten the well-being** of our community.
|
||||||
|
|
||||||
|
### Other Restrictions
|
||||||
|
|
||||||
|
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
|
||||||
|
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
||||||
|
3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
|
||||||
|
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
|
||||||
|
|
||||||
|
|
||||||
|
## Reporting an Issue
|
||||||
|
|
||||||
|
Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
|
||||||
|
|
||||||
|
When an incident does occur, it is important to report it promptly. To report a possible violation, **contact the administrative team at info@inference.coop.**
|
||||||
|
|
||||||
|
Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.
|
||||||
|
|
||||||
|
|
||||||
|
## Addressing and Repairing Harm
|
||||||
|
|
||||||
|
**The Community Moderators consist of the disputes working group or, if it is not yet formed, the General Manager.**
|
||||||
|
|
||||||
|
If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped.
|
||||||
|
|
||||||
|
1) Warning
|
||||||
|
1) Event: A violation involving a single incident or series of incidents.
|
||||||
|
2) Consequence: A private, written warning from the Community Moderators.
|
||||||
|
3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations.
|
||||||
|
2) Temporarily Limited Activities
|
||||||
|
1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation.
|
||||||
|
2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members.
|
||||||
|
3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over.
|
||||||
|
3) Temporary Suspension
|
||||||
|
1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation.
|
||||||
|
2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions.
|
||||||
|
3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted.
|
||||||
|
4) Permanent Ban
|
||||||
|
1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member.
|
||||||
|
2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior.
|
||||||
|
3) Repair: There is no possible repair in cases of this severity.
|
||||||
|
|
||||||
|
This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community.
|
||||||
|
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||||
|
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
|
||||||
|
|
||||||
|
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||||
|
|
||||||
|
For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion).
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Energy and carbon
|
||||||
|
|
||||||
|
The co-op shows members how much energy its inference uses, and what carbon that
|
||||||
|
represents. This page says where those numbers come from and where they are weak.
|
||||||
|
The [model list](models.md) covers which providers we use and their energy
|
||||||
|
sources; this covers how we count.
|
||||||
|
|
||||||
|
## What we measure
|
||||||
|
|
||||||
|
**Output tokens, per model, per day** — read from our own gateway logs. Nothing
|
||||||
|
is required of your client, and no per-member figure is published: the dashboards
|
||||||
|
show co-op totals.
|
||||||
|
|
||||||
|
We count **output tokens only**, deliberately. That is the unit
|
||||||
|
[GreenPT's published metric](https://greenpt.com/blog/introducing-a-new-ai-metric-to-drive-sustainability)
|
||||||
|
uses ("mWh per 100 tokens"); input tokens are excluded so comparisons hold across
|
||||||
|
prompts, workloads and models. It also means these figures are **not** total
|
||||||
|
compute.
|
||||||
|
|
||||||
|
## How tokens become energy and carbon
|
||||||
|
|
||||||
|
```
|
||||||
|
energy = output tokens × (Wh per 1,000 output tokens)
|
||||||
|
carbon = energy × grid intensity of the electricity (gCO2e per kWh)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both factors are held per provider in one table, with the source recorded for
|
||||||
|
each row, in the portal's
|
||||||
|
[`metrics_snapshot.py`](https://git.inference.coop/code/member-portal/src/branch/main/metrics_snapshot.py).
|
||||||
|
|
||||||
|
### Measured: GreenPT
|
||||||
|
|
||||||
|
GreenPT returns an `impact` object with every call — energy in watt-milliseconds,
|
||||||
|
emissions in µgCO2e — so these are fitted from their own numbers rather than
|
||||||
|
imported from a third party. Measured 2026-10-28 by regressing energy against
|
||||||
|
output tokens across prompt sizes (R² ≥ 0.999; there is a real per-request
|
||||||
|
overhead, because their energy derives from inference *time*):
|
||||||
|
|
||||||
|
| Model | Wh per 1,000 output tokens | Implied grid intensity |
|
||||||
|
|---|---|---|
|
||||||
|
| `greenpt/deepseek-v4.1-flash` | 0.36 | ~193 gCO2e/kWh |
|
||||||
|
| `greenpt/glm-5.3-flash` | 1.08 | ~193 gCO2e/kWh |
|
||||||
|
| `greenpt/green-r` | 0.55 | ~24 gCO2e/kWh |
|
||||||
|
|
||||||
|
`green-r` is roughly an order of magnitude cleaner per unit of energy, which is
|
||||||
|
where GreenPT's claim of up to 40% lower CO2 per query comes from.
|
||||||
|
|
||||||
|
### Estimated: everyone else
|
||||||
|
|
||||||
|
Providers that do not disclose impact — Tinfoil, Regolo, PublicAI — use a
|
||||||
|
**documented order-of-magnitude proxy**: 0.5 Wh per 1,000 output tokens at an EU
|
||||||
|
grid average of 250 gCO2e/kWh. It exists so a day's total is never quietly
|
||||||
|
missing models. It is an estimate, and it is labelled as one everywhere it
|
||||||
|
appears. Refining a factor is a data edit, not a code change.
|
||||||
|
|
||||||
|
## What this does not cover
|
||||||
|
|
||||||
|
- **It is a floor, not a total.** Embedding, audio and image requests carry no
|
||||||
|
completion tokens, so their compute is not counted at all. This is the largest
|
||||||
|
known gap.
|
||||||
|
- **Estimates for non-disclosing providers.** The proxy is an order-of-magnitude
|
||||||
|
figure, not a measurement of what a given provider actually draws.
|
||||||
|
- **GreenPT's energy is itself estimated** — from time and documented hardware
|
||||||
|
power — so our per-token figures are an efficiency ratio, not a per-call meter
|
||||||
|
reading.
|
||||||
|
- **Training emissions are not included**, for any model we serve. Nobody in this
|
||||||
|
market can verify them yet.
|
||||||
|
|
||||||
|
## Where to see the numbers
|
||||||
|
|
||||||
|
The [member dashboard](https://dashboard.inference.coop) has them under
|
||||||
|
"Co-op usage" — by day, alongside membership, requests and spend. Operators see
|
||||||
|
the same series on the admin panel's Trends tab.
|
||||||
|
|
||||||
|
## Units, for the curious
|
||||||
|
|
||||||
|
`Wh` is a watt-hour (a 100 W bulb for one hour is 100 Wh). `kWh` is 1,000 Wh.
|
||||||
|
`gCO2e` is grams of carbon-dioxide equivalent. Converting between them is where
|
||||||
|
mistakes hide: µgCO2e per mWh and gCO2e per kWh are the same ratio, and Wh to kWh
|
||||||
|
is a factor of 1,000.
|
||||||
+117
-15
@@ -7,13 +7,18 @@ private AI chat and API access in return.
|
|||||||
This guide covers the three things most new members want: becoming a member,
|
This guide covers the three things most new members want: becoming a member,
|
||||||
using the chat, and getting involved in governance.
|
using the chat, and getting involved in governance.
|
||||||
|
|
||||||
|
The co-op's meetings — including the regular member meetings — are all on the
|
||||||
|
shared [co-op calendar](https://cloud.inference.coop/apps/calendar/p/Ltb6LAbt7NWHH7Td),
|
||||||
|
where we share dates and details. Check it for what's coming up.
|
||||||
|
|
||||||
## 1. Become a member
|
## 1. Become a member
|
||||||
|
|
||||||
Membership is a **sliding-scale contribution of $10–20/month** through Open
|
Membership is a **sliding-scale contribution starting at $5/month** through
|
||||||
Collective. What you pay is up to you.
|
Open Collective (the suggested level is $15 — pay what fits your
|
||||||
|
circumstances).
|
||||||
|
|
||||||
1. Go to [opencollective.com/inference-cooperative](https://opencollective.com/inference-cooperative).
|
1. Go to [opencollective.com/inference-cooperative](https://opencollective.com/inference-cooperative).
|
||||||
2. Choose a contribution amount in the $10–20 range and subscribe.
|
2. Choose a contribution amount ($5 minimum) and subscribe.
|
||||||
3. After you contribute, you'll land on a "check your inbox" page.
|
3. After you contribute, you'll land on a "check your inbox" page.
|
||||||
|
|
||||||
**Within a few minutes, you'll get a welcome email** with a link to set up your
|
**Within a few minutes, you'll get a welcome email** with a link to set up your
|
||||||
@@ -25,12 +30,21 @@ account. Click it and create your login.
|
|||||||
> as a member. (Contributed without an Open Collective account? You're recognized
|
> as a member. (Contributed without an Open Collective account? You're recognized
|
||||||
> by the email you entered at checkout.)
|
> by the email you entered at checkout.)
|
||||||
|
|
||||||
|
**On payment timing:** Open Collective charges your contribution on the
|
||||||
|
collective's billing day, not the day you joined — so your second charge can
|
||||||
|
arrive sooner than a month after the first. When that leaves a paid month
|
||||||
|
shorter than a month, we credit the difference automatically as non-expiring
|
||||||
|
credits, pro-rated to the days lost. It's automatic: no need to ask, claim it,
|
||||||
|
or contact anyone.
|
||||||
|
|
||||||
### Your membership includes
|
### Your membership includes
|
||||||
|
|
||||||
- **Private AI chat** through the co-op's shared gateway.
|
- **Private AI chat** through the co-op's shared gateway.
|
||||||
- **API access** for your own tools and projects (see [API access](api-access.md)).
|
- **API access** for your own tools and projects (see [API access](api-access.md)).
|
||||||
- **A monthly usage allowance** of $15 of credit, shared between chat and API.
|
- **A monthly usage allowance** of $15 of credit, shared between chat and API.
|
||||||
- **Credit packs** — buy extra non-expiring credits when you need more.
|
- **Credit packs** — buy extra non-expiring credits when you need more.
|
||||||
|
- **Guest access** — give a friend $2 of credit and 60 days of the co-op.
|
||||||
|
See [Guest access](guest-access.md).
|
||||||
- **A voice in governance** — how the project is run and where it goes next.
|
- **A voice in governance** — how the project is run and where it goes next.
|
||||||
|
|
||||||
## 2. Use the chat
|
## 2. Use the chat
|
||||||
@@ -38,29 +52,116 @@ account. Click it and create your login.
|
|||||||
The chat lives at **[chat.inference.coop](https://chat.inference.coop)**. Log in
|
The chat lives at **[chat.inference.coop](https://chat.inference.coop)**. Log in
|
||||||
with the account you created in step 1.
|
with the account you created in step 1.
|
||||||
|
|
||||||
- **Choose your model.** You can pick not just *which* model but *where* it runs:
|
- **Choose your model.** You can pick not just *which* model but *where* it
|
||||||
**private** (Tinfoil, runs in encrypted enclaves) or **green** (GreenPT, 100%
|
runs: **private** (Tinfoil, runs in encrypted enclaves) or **green**
|
||||||
renewable energy). See the [model list](models.md) for the full list.
|
(GreenPT and Regolo, 100% renewable energy — Regolo adds a written
|
||||||
- **Web search** is enabled by default (self-hosted, no external dependency).
|
zero-data-retention policy). See the [model list](models.md) for the full
|
||||||
- **File uploads** work out of the box — ask the chat about a document you attach.
|
list.
|
||||||
- Your usage draws from your monthly allowance; you can see it on the
|
- **Web search** — click the **globe** next to the message box to ground
|
||||||
[Member Dashboard](https://dashboard.inference.coop).
|
answers in current web results (self-hosted search, no external dependency).
|
||||||
|
- **File uploads** — attach a document with the **paperclip** and ask the
|
||||||
|
chat about it.
|
||||||
|
- **Tools** — the **Tools menu** next to the message box lists everything the
|
||||||
|
chat can do: generate images, search the co-op documentation, and more. If
|
||||||
|
there's a tool you'd like available, ask in #general.
|
||||||
|
- **Voice** works in both directions: speak to the chat with the microphone
|
||||||
|
icon, and have replies read aloud.
|
||||||
|
- **Temporary chats** — for conversations you don't want kept on our servers
|
||||||
|
at all. Click the **Temporary Chat** toggle (in the chat header) and the
|
||||||
|
conversation is never saved: it disappears when you leave the page, and
|
||||||
|
nothing lands in your history or our database. To make this your default,
|
||||||
|
open **Settings → Conversations** and turn on **Default Temporary Chat**.
|
||||||
|
For anything sensitive, we recommend a temporary chat — ideally paired with
|
||||||
|
a 🔒 enclave model. See [Privacy](privacy-policy.md#4-what-we-can-see-an-honest-caveat).
|
||||||
|
|
||||||
|
Your usage draws from your monthly allowance; you can see it on the
|
||||||
|
[Member Dashboard](https://dashboard.inference.coop).
|
||||||
|
|
||||||
If the chat ever says your balance is out, that's the allowance, not an error —
|
If the chat ever says your balance is out, that's the allowance, not an error —
|
||||||
see "Your allowance" below.
|
see "Your allowance" below.
|
||||||
|
|
||||||
|
> **Moved from the old chat?** The previous Open WebUI instance remains
|
||||||
|
> readable at [chat-legacy.inference.coop](https://chat-legacy.inference.coop)
|
||||||
|
> — your full history is here in the new chat, but files you attached in the
|
||||||
|
> old one are still accessible only there.
|
||||||
|
|
||||||
|
### Add the chat to your phone
|
||||||
|
|
||||||
|
The chat works like an app on your phone — no app store needed. Open
|
||||||
|
[chat.inference.coop](https://chat.inference.coop) in your phone's browser and
|
||||||
|
log in, then:
|
||||||
|
|
||||||
|
- **iPhone (Safari):** tap the **Share** button (the square with an arrow),
|
||||||
|
then **Add to Home Screen**.
|
||||||
|
- **Android (Chrome):** tap the **⋮** menu, then **Add to Home Screen**
|
||||||
|
(or **Install app**).
|
||||||
|
|
||||||
|
You'll get an icon on your home screen that opens the chat full-screen, like
|
||||||
|
any other app. Your login stays active — just tap and chat.
|
||||||
|
|
||||||
## 3. Get involved in governance
|
## 3. Get involved in governance
|
||||||
|
|
||||||
You don't just use the co-op; you help run it. Decisions about models, pricing,
|
You don't just use the co-op; you help run it. Decisions about models, pricing,
|
||||||
privacy, and direction happen on our **Loomio forum**:
|
privacy, and direction happen on our **Loomio forum**:
|
||||||
|
|
||||||
- **[forum.inference.coop](https://forum.inference.coop)** — where members discuss and decide.
|
- **[forum.inference.coop](https://forum.inference.coop)** — where members discuss and decide.
|
||||||
|
- **[matrix.inference.coop](https://matrix.inference.coop)** — our Matrix chat server. Log in
|
||||||
|
with your co-op account (Cloudron SSO); your Matrix ID is `@username:inference.coop`.
|
||||||
|
Every member is invited to the **Inference Cooperative** space automatically — check your
|
||||||
|
invites on first login.
|
||||||
|
|
||||||
|
**Using it from any Matrix client** — the server speaks the standard Matrix protocol,
|
||||||
|
so you can connect from any client:
|
||||||
|
|
||||||
|
- **Element** (web, desktop, iOS, Android): on the sign-in screen, click *Edit* next to
|
||||||
|
"Homeserver" and enter `https://matrix.inference.coop`, then sign in with your co-op
|
||||||
|
account. (On [app.element.io](https://app.element.io) or the desktop/mobile apps alike.)
|
||||||
|
**Note:** the new **Element X** apps (iOS/Android/macOS) are *not* currently supported —
|
||||||
|
they only work with the newer Matrix Authentication Service, which this server doesn't
|
||||||
|
run yet. Use the classic Element apps (Element Web / Element Desktop / "Element" on the
|
||||||
|
app stores) or another client below.
|
||||||
|
- **FluffyChat, Cinny, SchildiChat** or any other client: look for the "custom homeserver"
|
||||||
|
/ "other server" option in the client's login screen and enter the same address.
|
||||||
|
- Your co-op account and your existing Matrix account on another server (e.g.
|
||||||
|
matrix.org or a personal homeserver) are separate identities. **Pick your
|
||||||
|
identity (optional):** by default, invitations go to your co-op identity
|
||||||
|
(`@username:inference.coop`). To use an account you already have instead:
|
||||||
|
|
||||||
|
1. Open your [dashboard](https://dashboard.inference.coop) and find the
|
||||||
|
**Matrix chat** card. It shows the Matrix ID currently registered for
|
||||||
|
you — your co-op identity, or `pending` if your co-op Matrix account
|
||||||
|
hasn't been created yet (it is created the first time you log in to
|
||||||
|
Matrix).
|
||||||
|
2. Click **Use external account**, enter your existing Matrix ID in the
|
||||||
|
format `@username:server.org`, and click **Save**.
|
||||||
|
3. Accept the space invitation that arrives in that account — usually
|
||||||
|
instantly. If the account can't be reached right away, the invitation
|
||||||
|
is retried automatically each night.
|
||||||
|
|
||||||
|
That's all it takes: invitations (and the space) follow whichever identity
|
||||||
|
you've chosen. Switching back is one click — **Use
|
||||||
|
@username:inference.coop** on the same card. One identity is active at a
|
||||||
|
time, and both work equally well in every co-op room.
|
||||||
|
|
||||||
|
**Members-only, readable history.** The space is invite-only and unencrypted
|
||||||
|
so every member can read the full history (including messages from before
|
||||||
|
they joined). Invites are restricted to admins — so while any member is
|
||||||
|
welcome to bring a collaborator or guest, an admin sends the invite rather
|
||||||
|
than members handing the space to outsiders directly.
|
||||||
|
|
||||||
|
- **#general** is the main room — say hello, ask questions, start
|
||||||
|
conversations. Beyond it, the space holds a handful of topical rooms
|
||||||
|
(infrastructure, design, an automated co-op updates feed) — explore the
|
||||||
|
space and join whatever is interesting to you. New rooms appear there as
|
||||||
|
the co-op grows.
|
||||||
|
|
||||||
|
- The updates feed is posted by the co-op's automation account. See
|
||||||
|
[Working with the agent](working-with-the-agent.md) for the agents behind
|
||||||
|
it, what they do, and how members interact with them.
|
||||||
|
|
||||||
- See the [Charter](charter.md) for how governance is structured.
|
- See the [Charter](charter.md) for how governance is structured.
|
||||||
- See [Open questions](open-questions.md) for the decisions currently on the table.
|
- See [Open questions](open-questions.md) for the decisions currently on the table.
|
||||||
|
|
||||||
During the pilot phase, Nathan Schneider serves as General Manager and holds
|
|
||||||
final discretion — but members deliberate now, and full member governance is the
|
|
||||||
goal as the co-op grows.
|
|
||||||
|
|
||||||
### Help build it
|
### Help build it
|
||||||
|
|
||||||
@@ -71,8 +172,9 @@ and the "Contributing" section for how to propose changes through
|
|||||||
|
|
||||||
## Your allowance and credits
|
## Your allowance and credits
|
||||||
|
|
||||||
Membership includes **$15 of usage credit each month**, shared between chat and
|
Membership includes **$15 of usage credit each month** (same for every member,
|
||||||
API. It's a starting value, not a hard rule:
|
regardless of what you pay), shared between chat and API. It's a starting
|
||||||
|
value, not a hard rule:
|
||||||
|
|
||||||
- The allowance resets each month on your personal billing date (the day your
|
- The allowance resets each month on your personal billing date (the day your
|
||||||
membership payment recurs) — not on a fixed calendar day.
|
membership payment recurs) — not on a fixed calendar day.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Guest access
|
||||||
|
|
||||||
|
Any member can **give a friend guest access to the co-op**: **$2 of AI usage
|
||||||
|
for 60 days**, spent in the chat and the API. It doesn't come out of your own
|
||||||
|
balance. You can send it once every 30 days, and an address can receive it
|
||||||
|
only once.
|
||||||
|
|
||||||
|
## How to send guest access
|
||||||
|
|
||||||
|
On your [dashboard](https://dashboard.inference.coop), open the **Guests**
|
||||||
|
card, enter your friend's email address (name optional), and click
|
||||||
|
**Send invite**. There's no approval step: the account is provisioned right
|
||||||
|
away, and your friend gets an **invitation email** from the co-op, naming
|
||||||
|
you, with a link to set up their account. Setting it up takes about a minute.
|
||||||
|
|
||||||
|
New guests show as **Invited — awaiting setup** until they accept it. The
|
||||||
|
card also lists the guests you've sent.
|
||||||
|
|
||||||
|
## What your guest gets
|
||||||
|
|
||||||
|
- **$2 of credit** for the chat and the API — a hard cap: usage stops when
|
||||||
|
it's spent.
|
||||||
|
- **Their own dashboard** at
|
||||||
|
[dashboard.inference.coop](https://dashboard.inference.coop) showing what's
|
||||||
|
left.
|
||||||
|
- **The weekly newsletter** and these public docs.
|
||||||
|
|
||||||
|
Guest access is not membership — no Matrix, governance forum, or vote.
|
||||||
|
|
||||||
|
## When it ends
|
||||||
|
|
||||||
|
When **the $2 is spent or the 60 days are up**, whichever comes first, your
|
||||||
|
friend gets an email saying so. Access stops; **the account and its data are
|
||||||
|
kept** — and a guest can end it early with the **Cancel guest access** link
|
||||||
|
on their dashboard, returning any unspent credit to the co-op.
|
||||||
|
|
||||||
|
## Becoming a member
|
||||||
|
|
||||||
|
Contribute on
|
||||||
|
[Open Collective](https://opencollective.com/inference-cooperative/contribute)
|
||||||
|
and the account converts automatically — same login, same dashboard. Any
|
||||||
|
unspent guest credit carries over. See
|
||||||
|
[Become a member](getting-started.md#1-become-a-member) for how membership
|
||||||
|
works.
|
||||||
+141
-147
@@ -5,109 +5,153 @@ and contributors who want to work on it.
|
|||||||
|
|
||||||
## The big picture
|
## The big picture
|
||||||
|
|
||||||
Members reach the co-op through two front doors: the **chat** (Open WebUI) and
|
Members reach the co-op through two front doors: the **chat** (LibreChat) and
|
||||||
the **API** (the LiteLLM gateway). Both route through a single gateway to cloud
|
the **API** (the LiteLLM gateway). Both route through a single gateway to
|
||||||
LLM providers running inside **TEE-protected enclaves** (Tinfoil), so inference
|
multiple LLM providers, so members can choose what they value for each task:
|
||||||
is private end-to-end. A thin layer of custom middleware — the member portal —
|
architecturally private inference in **TEE-protected enclaves** (Tinfoil), or
|
||||||
ties Open Collective membership to Cloudron accounts and LiteLLM keys.
|
100% renewable energy (GreenPT, Regolo). A thin layer of custom middleware — the
|
||||||
|
member portal — ties Open Collective membership to Cloudron accounts and LiteLLM
|
||||||
|
keys.
|
||||||
|
|
||||||
```
|
```
|
||||||
Members ──> Open WebUI (chat) ─┐
|
┌─> Tinfoil (TEE) 🔒 private
|
||||||
├──> LiteLLM gateway ──> Tinfoil (TEE) ──> models
|
Members ──> LibreChat (chat) ──┐ │
|
||||||
Members ──> your own tools ────┘
|
├──> LiteLLM ──┼─> GreenPT (renewable) 🌱 green
|
||||||
(API)
|
Members ──> your own tools ────┘ gateway │
|
||||||
|
(API) (keys, └─> Regolo (renewable) 🌱 green
|
||||||
|
metering,
|
||||||
|
routing)
|
||||||
```
|
```
|
||||||
|
|
||||||
The whole stack runs on [Cloudron](https://cloudron.io), which handles hosting,
|
The whole stack runs on [Cloudron](https://cloudron.io), which handles hosting,
|
||||||
single sign-on, and per-app isolation.
|
single sign-on, and per-app isolation.
|
||||||
|
|
||||||
|
## Where it runs
|
||||||
|
|
||||||
|
The co-op's member-facing services run on a server the co-op owns and pays for
|
||||||
|
directly, at **Infomaniak** — a Swiss sovereign cloud provider whose data centres
|
||||||
|
are in Geneva, run on renewable energy, and are certified to ISO 14001, ISO
|
||||||
|
50001 and ISO 27001.
|
||||||
|
|
||||||
|
The co-op's **automation runs on a separate server**, also hosted on Infomaniak
|
||||||
|
and deliberately kept apart from the member-facing applications. The steward bot
|
||||||
|
— the automation behind the digests and the newsletter drafts — and the co-op's
|
||||||
|
other agents live there. Keeping them on their own host means an agent problem
|
||||||
|
stays an agent problem: it cannot reach the chat, the gateway or member data.
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
|
||||||
|
Cloudron writes **encrypted** backups to Infomaniak's object storage in
|
||||||
|
Switzerland every night and keeps the last **seven days**, so a failed upgrade
|
||||||
|
or a lost server can be restored. The automation host backs up its own data to
|
||||||
|
the same object storage.
|
||||||
|
|
||||||
|
### Domain and DNS
|
||||||
|
|
||||||
|
The co-op's domain is **inference.coop** — a `.coop` domain. The co-op registers
|
||||||
|
and manages its domains with **domains.coop**, the domain provider run by the
|
||||||
|
cooperative movement, and holds them in the co-op's own name rather than in any
|
||||||
|
individual's. DNS itself is deliberately simple, and the co-op's own nameservers
|
||||||
|
answer it.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
| Component | Purpose | URL | Code |
|
| Component | Purpose | URL |
|
||||||
|-----------|---------|-----|------|
|
|-----------|---------|-----|
|
||||||
| **Open WebUI** | Member-facing chat (web search, file uploads) | chat.inference.coop | — |
|
| **LibreChat** | Member-facing chat (models, web search, image generation, voice) | chat.inference.coop |
|
||||||
| **LiteLLM** | AI gateway — keys, metering, model routing | gateway.inference.coop | [code/litellm](https://git.inference.coop/code/litellm) |
|
| **Open WebUI** | Legacy chat instance — read-only reference (old history and file attachments) | chat-legacy.inference.coop |
|
||||||
| **Member Portal** | Membership middleware — onboarding, key provisioning, Loomio sync | portal.inference.coop | [code/member-portal](https://git.inference.coop/code/member-portal) |
|
| **LiteLLM** | AI gateway — keys, metering and model routing, for chat and API | gateway.inference.coop |
|
||||||
| **Member Dashboard** | Member-facing usage view + API key management | dashboard.inference.coop | [code/member-dashboard](https://git.inference.coop/code/member-dashboard) |
|
| **Member portal** | Membership middleware — onboarding, key provisioning, Loomio sync | portal.inference.coop |
|
||||||
| **Admin Panel** | Admin-only member overview, spend, balance management | panel.inference.coop | [code/admin-panel](https://git.inference.coop/code/admin-panel) |
|
| **Member dashboard** | Your usage view and API keys | dashboard.inference.coop |
|
||||||
| **SearXNG** | Self-hosted web search (feeds the chat's search) | search.inference.coop | — |
|
| **Loomio** | Member governance | forum.inference.coop |
|
||||||
| **Loomio** | Member governance | forum.inference.coop | — |
|
| **Matrix (Synapse)** | Member chat server, federated, SSO login via Cloudron | matrix.inference.coop |
|
||||||
| **Gitea** | Git hosting (code + docs) | git.inference.coop | — |
|
| **Gitea** | Git hosting for the co-op's code and this documentation | git.inference.coop |
|
||||||
| **Surfer** | Static hosting (landing page, thanks page) | inference.coop | [co-op/website](https://git.inference.coop/co-op/website) |
|
| **Listmonk** | Member newsletter (synced from membership) | newsletter.inference.coop |
|
||||||
| **Listmonk** | Member newsletter (auto-synced from membership) | newsletter.inference.coop | — |
|
| **SnappyMail** | Webmail | email.inference.coop |
|
||||||
| **Vaultwarden** | Password manager (admin credentials) | vault.inference.coop | — |
|
|
||||||
| **SnappyMail** | Webmail | mail.inference.coop | — |
|
The co-op's software behind all of these is open source — see [The
|
||||||
|
code](#the-code) below for where each piece lives.
|
||||||
|
|
||||||
Open Collective (billing and fiscal sponsorship) is the one external service in
|
Open Collective (billing and fiscal sponsorship) is the one external service in
|
||||||
the flow — see [opencollective.com/inference-cooperative](https://opencollective.com/inference-cooperative).
|
the flow — see
|
||||||
|
[opencollective.com/inference-cooperative](https://opencollective.com/inference-cooperative).
|
||||||
|
|
||||||
## How membership is wired
|
## Mail
|
||||||
|
|
||||||
Membership flows through the member portal, which is the single source of truth
|
The co-op runs its own `@inference.coop` mail. **Incoming** mail arrives directly
|
||||||
for "who is a member":
|
on the co-op's server, and webmail and mail apps reach it over the usual
|
||||||
|
encrypted ports (IMAP and SMTP over TLS). **Outgoing** mail is sent through
|
||||||
|
**Scaleway**, an EU-based email service, authenticated for our domain.
|
||||||
|
|
||||||
|
You can reach your mailbox through webmail at email.inference.coop, or add it to
|
||||||
|
a standard mail app on your phone or computer. If mail to your `@inference.coop`
|
||||||
|
address stops arriving, that is a fault on our side to fix.
|
||||||
|
|
||||||
|
## Matrix
|
||||||
|
|
||||||
|
Members get a federated Matrix account on the co-op's self-hosted Synapse
|
||||||
|
(matrix.inference.coop, user IDs `@username:inference.coop`), signing in with the
|
||||||
|
same Cloudron SSO as the chat. The co-op invites every active member to the
|
||||||
|
private **Inference Cooperative** space (with a **General** room) automatically,
|
||||||
|
so new members land there as they join. Federation is on, so you can also join
|
||||||
|
from any homeserver you already use.
|
||||||
|
|
||||||
|
### Accounts you may meet
|
||||||
|
|
||||||
|
A few accounts in the space are automation rather than people:
|
||||||
|
|
||||||
|
- **`@inferencebot` — the steward.** It posts the digests, newsletter notes and
|
||||||
|
reminders. It holds no more power than posting needs.
|
||||||
|
- **`@inference-admin`** — the administrator of record for the co-op's rooms
|
||||||
|
(#general, #community, #design, #skillshare, #updates and #infrastructure) and
|
||||||
|
their membership. It **never speaks in a member room** — a message from it is
|
||||||
|
a mistake, not a feature.
|
||||||
|
|
||||||
|
The house rule: **power belongs to `@inference-admin`, voice belongs to
|
||||||
|
`@inferencebot`.** Please don't create an account whose name differs from an
|
||||||
|
existing one by a character or two — spotting lookalikes is exactly what this
|
||||||
|
convention is for.
|
||||||
|
|
||||||
|
## Membership
|
||||||
|
|
||||||
|
Membership runs through the member portal, the single source of truth for who is
|
||||||
|
a member:
|
||||||
|
|
||||||
1. A member contributes on Open Collective.
|
1. A member contributes on Open Collective.
|
||||||
2. Open Collective fires a webhook to the portal, which provisions a Cloudron
|
2. Open Collective notifies the portal, which provisions a Cloudron account, a
|
||||||
account, a LiteLLM key, and a welcome email.
|
LiteLLM key, and a welcome email.
|
||||||
3. A nightly sweep reconciles against the live Open Collective list, so a missed
|
3. A nightly reconciliation against the live Open Collective list means a missed
|
||||||
webhook self-heals within 24 hours.
|
notification self-heals within a day.
|
||||||
|
|
||||||
The portal database is authoritative for member emails and balances; LiteLLM's
|
You may see **"Inference Co-op Bot"** listed as an admin of the collective on
|
||||||
team budget is the source of truth for a member's allowance, shared between chat
|
Open Collective. It is infrastructure, not a person or a member: a dedicated
|
||||||
and API.
|
account the portal uses, **read-only**, to check the member roster and tiers. It
|
||||||
|
cannot move money, issue refunds or change the collective — payments always come
|
||||||
### The bot account on Open Collective
|
from a human's own contribution, and automation only reads the result. Its code
|
||||||
|
is open source in
|
||||||
You may notice **"Inference Co-op Bot"** listed as an admin of the collective on
|
[code/member-portal](https://git.inference.coop/code/member-portal), and a
|
||||||
Open Collective. This is infrastructure, not a member or a person:
|
collective admin can revoke its access at any time.
|
||||||
|
|
||||||
- **What it is:** a dedicated Open Collective account owned by the co-op. It
|
|
||||||
holds a **Personal Token** that the member portal uses to read membership
|
|
||||||
data — the roster, tiers (needed to route credit-pack purchases correctly),
|
|
||||||
and contributor emails (including guests, which only admins can see).
|
|
||||||
- **What it can do:** read member and tier data via Open Collective's API.
|
|
||||||
It cannot move money, issue refunds, or change the collective — Open
|
|
||||||
Collective restricts those to dashboard actions by human admins. Its API
|
|
||||||
access was verified read-mostly during the October 2026 security review.
|
|
||||||
- **Why it has admin:** reading guest contributor emails requires admin on
|
|
||||||
Open Collective; without it, membership reconciliation and credit-pack
|
|
||||||
routing can't function.
|
|
||||||
- **Who supervises it:** all of its code is open source in
|
|
||||||
[code/member-portal](https://git.inference.coop/code/member-portal) — every
|
|
||||||
API call it makes is documented there. Nathan Schneider (collective admin)
|
|
||||||
oversees its use and can revoke or rotate its token at any time from the
|
|
||||||
Open Collective dashboard.
|
|
||||||
|
|
||||||
Nothing on Open Collective is charged, transferred, or refunded by automation:
|
|
||||||
payments always come from a human's own contribution action, and any automated
|
|
||||||
system only *reads* the resulting records.
|
|
||||||
|
|
||||||
## The code
|
## The code
|
||||||
|
|
||||||
The cooperative's software is open source, in the [`code`](https://git.inference.coop/code)
|
The co-op's software is open source, in the [`code`](https://git.inference.coop/code)
|
||||||
organization on Gitea:
|
and [`co-op`](https://git.inference.coop/co-op) organizations on Gitea:
|
||||||
|
|
||||||
- [**code/member-portal**](https://git.inference.coop/code/member-portal) — membership middleware (Python).
|
- [**code/member-portal**](https://git.inference.coop/code/member-portal) — membership middleware (Python).
|
||||||
- [**code/member-dashboard**](https://git.inference.coop/code/member-dashboard) — member-facing usage + API keys (Python).
|
- [**code/member-dashboard**](https://git.inference.coop/code/member-dashboard) — member dashboard: usage and API keys.
|
||||||
- [**code/admin-panel**](https://git.inference.coop/code/admin-panel) — admin overview (Python).
|
|
||||||
- [**code/litellm**](https://git.inference.coop/code/litellm) — the gateway packaged as a Cloudron app.
|
- [**code/litellm**](https://git.inference.coop/code/litellm) — the gateway packaged as a Cloudron app.
|
||||||
|
|
||||||
Plus the `co-op` organization:
|
|
||||||
|
|
||||||
- [**co-op/website**](https://git.inference.coop/co-op/website) — landing page and static site.
|
- [**co-op/website**](https://git.inference.coop/co-op/website) — landing page and static site.
|
||||||
- [**co-op/docs**](https://git.inference.coop/co-op/docs) — this documentation.
|
- [**co-op/docs**](https://git.inference.coop/co-op/docs) — this documentation.
|
||||||
- [**co-op/design-assets**](https://git.inference.coop/co-op/design-assets) — logos, fonts, brand materials.
|
|
||||||
|
|
||||||
## Contributing (the Git is yours, too)
|
The chat, portal and other apps live in `code/` and `co-op/` too; everything is
|
||||||
|
browsable at [git.inference.coop](https://git.inference.coop). Everything the co-op runs
|
||||||
Everything the co-op runs is open source and lives at
|
is open source, so as a member you can browse all of
|
||||||
[git.inference.coop](https://git.inference.coop) — the website, this
|
it and propose changes.
|
||||||
documentation, the member portal, the dashboard, the admin panel, and the
|
|
||||||
gateway. As a member, you can browse all of it and propose changes.
|
|
||||||
|
|
||||||
**Who can sign in.** Git access is tied to your membership: you log into the Git
|
**Who can sign in.** Git access is tied to your membership: you log into the Git
|
||||||
with the same single sign-on you use for the chat. Members are in automatically
|
with the same single sign-on you use for the chat. Members are in automatically
|
||||||
(through the `members` group); when a membership lapses, Git access lapses with it.
|
(through the `members` group); when a membership lapses, Git access lapses with
|
||||||
|
it.
|
||||||
|
|
||||||
**How to contribute.** Anyone can read the repos without an account. To make a
|
**How to contribute.** Anyone can read the repos without an account. To make a
|
||||||
change, fork the repo, edit in your fork, and open a **pull request**:
|
change, fork the repo, edit in your fork, and open a **pull request**:
|
||||||
@@ -118,89 +162,39 @@ change, fork the repo, edit in your fork, and open a **pull request**:
|
|||||||
4. Open a **pull request** back to the original repo, describing your change.
|
4. Open a **pull request** back to the original repo, describing your change.
|
||||||
|
|
||||||
**All changes are reviewed.** Nothing merges directly into `main` — every change
|
**All changes are reviewed.** Nothing merges directly into `main` — every change
|
||||||
comes in as a pull request that an admin reviews and approves first. You don't
|
comes in as a pull request an admin reviews and approves first. You don't need
|
||||||
need to be able to create repos or organizations to contribute; you just fork,
|
to be able to create repos or organizations to contribute; you just fork, edit
|
||||||
edit, and ask for review.
|
and ask for review.
|
||||||
|
|
||||||
Questions about the Git itself? Write to info@inference.coop.
|
|
||||||
|
|
||||||
## Models and providers
|
## Models and providers
|
||||||
|
|
||||||
The co-op serves models from multiple providers (Tinfoil for privacy, GreenPT
|
The co-op serves models from multiple providers — Tinfoil for architectural
|
||||||
for renewable energy, PublicAI coming soon). See the [model list](models.md) for
|
privacy, GreenPT and Regolo for renewable energy. See the [model list](models.md),
|
||||||
the current catalog — that's the single source of truth for which models are
|
the single source of truth for which models are available and how they're named.
|
||||||
available and how they're named.
|
|
||||||
|
|
||||||
One infra detail worth knowing: a local Tinfoil proxy sidecar handles the
|
|
||||||
enclave encryption on our side of the gateway — only Tinfoil traffic goes
|
|
||||||
through it; the other providers are called directly.
|
|
||||||
|
|
||||||
## Security posture (plain language)
|
## Security posture (plain language)
|
||||||
|
|
||||||
- **Token-gated control plane.** The portal is publicly reachable, but every
|
- **Member isolation.** Each API key is scoped to its owner's team and budget; a
|
||||||
sensitive surface requires a secret token (admin, broker, webhook). All fail
|
member can only ever act on their own keys.
|
||||||
closed — unauthenticated requests get `401`.
|
- **Separated secrets.** Chat, portal, admin and model-layer secrets are
|
||||||
- **Separated secrets.** Chat, broker, admin, and model-layer secrets are
|
|
||||||
independent and independently rotatable, so a leak has bounded scope.
|
independent and independently rotatable, so a leak has bounded scope.
|
||||||
- **Member isolation.** Each API key is scoped to its owner's LiteLLM team and
|
- **No secrets in version control.** Credentials live in the hosting platform's
|
||||||
budget; a member can only ever act on their own keys.
|
environment, never in the repos.
|
||||||
- **Rate limiting** on control-plane endpoints; the inference path is capped by
|
|
||||||
each member's budget instead.
|
|
||||||
- **No secrets in version control.** Credentials live in Cloudron env vars, never
|
|
||||||
in the repos.
|
|
||||||
- **Accepted trade-offs** (documented, not hidden): member keys are stored in
|
- **Accepted trade-offs** (documented, not hidden): member keys are stored in
|
||||||
plaintext in the portal database (functionally necessary), and the gateway
|
the portal database so the co-op can serve them, and the API gateway must stay
|
||||||
must stay publicly reachable for API access (its admin UI and docs are
|
publicly reachable. Its admin surfaces are disabled; only the public API is
|
||||||
disabled; only the `/v1/*` API is exposed).
|
exposed.
|
||||||
|
|
||||||
## For operators
|
## For operators
|
||||||
|
|
||||||
Maintaining the co-op's infrastructure — rotating Cloudron tokens, editing the
|
Maintaining the co-op's infrastructure — token rotation, app configuration and
|
||||||
portal's environment, and troubleshooting the provisioning pipeline — is
|
troubleshooting — is documented in the git-only
|
||||||
documented in the [operator runbook](operator-token-runbook.md).
|
[`co-op/ops-docs`](https://git.inference.coop/co-op/ops-docs) repository, which is
|
||||||
|
deliberately not published on this site. It is the home for the operator runbook,
|
||||||
## Gateway version and security update policy
|
the maintenance notes, incident write-ups, and the record of past server
|
||||||
|
migrations. Anyone may read it; operators are the audience.
|
||||||
The LiteLLM gateway is pinned to an **exact upstream tag** in
|
|
||||||
[`code/litellm`](https://git.inference.coop/code/litellm) (currently
|
|
||||||
`v1.84.0`). Two reasons:
|
|
||||||
|
|
||||||
1. **CVE fix line.** A member security review (Oct 2026) found the prior pin
|
|
||||||
(`main-v1.74.0-stable`) sat below the fix line for two published
|
|
||||||
vulnerabilities: CVE-2026-35029 (authorization bypass on
|
|
||||||
`/config/update`, fixed in 1.83.0, with documented in-the-wild probing)
|
|
||||||
and CVE-2026-59822 (MCP session auth bypass, fixed in 1.84.0, listed in
|
|
||||||
the CISA Known Exploited Vulnerabilities catalog). `v1.84.0` is at or
|
|
||||||
above both fix lines.
|
|
||||||
2. **Floating-tag drift.** `main-v1.74.0-stable` is a *floating* tag — the
|
|
||||||
registry silently moved it to a newer build. A rebuild intended as a
|
|
||||||
no-op therefore pulled different bits and triggered a full schema
|
|
||||||
migration, which is how the 2026-10-02 outage happened (see below).
|
|
||||||
|
|
||||||
Rule: **never pin a floating tag** (`main-*` / `latest`); always the exact
|
|
||||||
version tag. When upgrading, expect the first boot after the upgrade to take
|
|
||||||
10–15 minutes (prisma migration with internal retries) — plan the restart
|
|
||||||
window accordingly.
|
|
||||||
|
|
||||||
CORS: the gateway answers browser preflights only for
|
|
||||||
`https://chat.inference.coop` (via LiteLLM's native
|
|
||||||
`LITELLM_CORS_ORIGINS`). The old `LITELLM_CORS_ALLOWED_ORIGINS` name
|
|
||||||
(our v1.74.0-era sed-patch variable) is dead — v1.84.0 reads the native
|
|
||||||
name only.
|
|
||||||
|
|
||||||
## Incident log
|
## Incident log
|
||||||
|
|
||||||
**2026-10-02 — gateway outage after security-upgrade rebuild.** A Cloudron
|
Write-ups for outages live in the operator docs, in
|
||||||
update triggered by the CVE report rebuilt the app image; the floating
|
[`incidents/`](https://git.inference.coop/co-op/ops-docs/src/branch/main/incidents).
|
||||||
`main-v1.74.0-stable` tag had moved to newer bits (v1.84.0-era), so the
|
|
||||||
rebuild changed the running version and kicked off a full prisma DB
|
|
||||||
migration that takes 10–15 minutes per boot with internal timeouts/retries.
|
|
||||||
The gateway was down ~50 minutes (04:50–05:34 UTC); no data loss. Repairs:
|
|
||||||
version pinned to the exact tag in the repo (PR
|
|
||||||
[code/litellm#1](https://git.inference.coop/code/litellm/pulls/1) — pending
|
|
||||||
merge), `LITELLM_CORS_ORIGINS` set as a Cloudron env var (the upgrade had
|
|
||||||
silently reverted CORS to `*`), packaging synced to mirror the live
|
|
||||||
deployment. Also during the incident: the domain's authoritative
|
|
||||||
nameservers (yoursrs.com) were intermittently unreachable, briefly failing
|
|
||||||
resolution of some hostnames (git, portal) — transient registrar-side
|
|
||||||
issue, self-recovered, no records were wrong.
|
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
# MkDocs configuration for the member-readable documentation site.
|
||||||
|
#
|
||||||
|
# The documentation source stays exactly where it is — Markdown files at the
|
||||||
|
# repository root. This is a *publishing* layer over that source, not an edit
|
||||||
|
# of it. docs_dir is the repo root, so no document has to move for the
|
||||||
|
# generator to find it.
|
||||||
|
#
|
||||||
|
# Build: see publishing/build.sh
|
||||||
|
# Deploy: see publishing/deploy.sh
|
||||||
|
#
|
||||||
|
# Constraints baked in here:
|
||||||
|
# * theme.font: false -> Material must NOT pull Google Fonts.
|
||||||
|
# * fonts are self-hosted (publishing/fonts/) and OFL-licensed (Figtree).
|
||||||
|
# * no CDN / third-party host appears anywhere in the output.
|
||||||
|
|
||||||
|
site_name: Inference Cooperative docs
|
||||||
|
site_description: >-
|
||||||
|
Member-readable documentation for the Inference Cooperative: how to join and
|
||||||
|
use the service, the model catalogue, API access, governance and policies.
|
||||||
|
site_url: https://docs.inference.coop/
|
||||||
|
# MkDocs requires the docs directory to be a *child* of the config's directory
|
||||||
|
# and refuses to read the repository root directly. So the build stages the
|
||||||
|
# Markdown into .site-src/ (a generated, gitignored mirror of the documents)
|
||||||
|
# and renders that. No document is moved or rewritten in the repository — the
|
||||||
|
# staging copy exists only during the build.
|
||||||
|
docs_dir: .site-src
|
||||||
|
# Build output.
|
||||||
|
site_dir: .site-build
|
||||||
|
use_directory_urls: true
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
# Theme overrides. Used only to add a "documentation source" link to the
|
||||||
|
# footer on *every* page (overrides/partials/copyright.html) without putting
|
||||||
|
# it in the Markdown sources. No styling is changed here.
|
||||||
|
custom_dir: overrides
|
||||||
|
language: en
|
||||||
|
# Disable the theme's built-in Google Fonts loader. Without this, Material
|
||||||
|
# requests Roboto from fonts.googleapis.com / fonts.gstatic.com.
|
||||||
|
font: false
|
||||||
|
logo: publishing/logo.svg
|
||||||
|
favicon: publishing/logo.svg
|
||||||
|
palette:
|
||||||
|
# Light is listed first, so a fresh visitor gets the bright theme.
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
primary: custom
|
||||||
|
accent: custom
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-night
|
||||||
|
name: Switch to dark mode
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: custom
|
||||||
|
accent: custom
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-sunny
|
||||||
|
name: Switch to light mode
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.top
|
||||||
|
- navigation.indexes
|
||||||
|
- search.suggest
|
||||||
|
- search.highlight
|
||||||
|
- content.code.copy
|
||||||
|
- toc.integrate
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: README.md
|
||||||
|
- Getting started: getting-started.md
|
||||||
|
- Guest access: guest-access.md
|
||||||
|
- Models: models.md
|
||||||
|
- API access: api-access.md
|
||||||
|
- Energy & carbon: energy.md
|
||||||
|
- Infrastructure: infrastructure.md
|
||||||
|
- Background: background.md
|
||||||
|
- Working with the agent: working-with-the-agent.md
|
||||||
|
- Governance:
|
||||||
|
- Charter: charter.md
|
||||||
|
- Code of conduct: code-of-conduct.md
|
||||||
|
- Open questions: open-questions.md
|
||||||
|
- Terms of Service: terms-of-service.md
|
||||||
|
- Privacy Policy: privacy-policy.md
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
# Built-in, offline search — the index is a JSON file in the build output.
|
||||||
|
- search
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
# Rewrites directory-style links (e.g. [incidents/](incidents/)) to the
|
||||||
|
# landing pages the build generates for them. Publishing-layer logic only.
|
||||||
|
- publishing/hooks.py
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
- tables
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
|
||||||
|
extra_css:
|
||||||
|
# Co-op brand: palette from the website + self-hosted Figtree (SIL OFL 1.1).
|
||||||
|
- publishing/brand.css
|
||||||
|
- publishing/typography.css
|
||||||
|
|
||||||
|
# Keep build/tooling files out of the rendered site.
|
||||||
|
exclude_docs: |
|
||||||
|
.git/
|
||||||
|
*.sh
|
||||||
|
overrides/
|
||||||
|
publishing/README.md
|
||||||
|
publishing/requirements.txt
|
||||||
|
|
||||||
|
# Links to git.inference.coop (e.g. code-of-conduct.md in charter.md) are
|
||||||
|
# intentional absolute URLs; do not warn about them.
|
||||||
|
validation:
|
||||||
|
links:
|
||||||
|
absolute_links: ignore
|
||||||
|
unrecognized_links: ignore
|
||||||
@@ -16,20 +16,22 @@ provider today is best on both.
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **Tinfoil** | *private* | **Strongest — architectural** | Not a green claim | Runs in hardware enclaves (TEEs) with end-to-end encryption (EHBP), verifiable via remote attestation. Neither we nor Tinfoil can read your prompts or responses at inference time. Energy mix is not disclosed. |
|
| **Tinfoil** | *private* | **Strongest — architectural** | Not a green claim | Runs in hardware enclaves (TEEs) with end-to-end encryption (EHBP), verifiable via remote attestation. Neither we nor Tinfoil can read your prompts or responses at inference time. Energy mix is not disclosed. |
|
||||||
| **GreenPT** | *green* | **Policy-based** | **Strongest — 100% renewable** | Standard (non-enclave) hosting in the EU; privacy rests on GreenPT's commitments and EU data rules, not on cryptography. Compute runs on 100% renewable energy. |
|
| **GreenPT** | *green* | **Policy-based** | **Strongest — 100% renewable** | Standard (non-enclave) hosting in the EU; privacy rests on GreenPT's commitments and EU data rules, not on cryptography. Compute runs on 100% renewable energy. |
|
||||||
| **PublicAI** | *public* | Policy-based | Undisclosed | A gateway for **publicly developed, sovereign models** — open-weight models built by national/public programs rather than private labs. Privacy is policy-based (no enclave); energy mix is not disclosed. |
|
| **Regolo** | *green* | Policy-based — **zero data retention** | **100% renewable** | EU-hosted (Italy, Seeweb) with a stated zero-data-retention policy: prompts are not stored or reused. Compute runs on 100% renewable energy. |
|
||||||
|
|
||||||
**Reading the gradations:**
|
**Reading the gradations:**
|
||||||
|
|
||||||
- **Privacy:** *architectural* (Tinfoil) beats *policy-based* (GreenPT, PublicAI). An
|
- **Privacy:** *architectural* (Tinfoil) beats *policy-based* (GreenPT,
|
||||||
enclave protects you even if the provider is compromised; a policy protects
|
Regolo). An enclave protects you even if the provider is compromised; a
|
||||||
you only as far as the provider keeps its word. Both are better than a
|
policy protects you only as far as the provider keeps its word. Both are
|
||||||
provider with neither.
|
better than a provider with neither — and Regolo adds a written
|
||||||
- **Green energy:** *100% renewable* (GreenPT) beats *undisclosed* (Tinfoil,
|
zero-data-retention commitment on top of its policy.
|
||||||
PublicAI). GreenPT reports its energy mix; the others do not, so we can't
|
How the co-op measures its own energy use and carbon emissions is on the
|
||||||
claim greenness for them.
|
[energy and carbon page](energy.md) — including what those figures cover and what
|
||||||
- **Sovereignty:** *publicly developed* (PublicAI) is its own value — the models
|
they miss.
|
||||||
come from public/national AI programs, built as public goods rather than
|
|
||||||
proprietary products. It's a third axis, independent of privacy and energy.
|
- **Green energy:** *100% renewable* (GreenPT, Regolo) beats *undisclosed*
|
||||||
|
(Tinfoil). GreenPT and Regolo both run on 100% renewable energy; Tinfoil's
|
||||||
|
mix is not disclosed.
|
||||||
|
|
||||||
Trade-offs are real: the most private option (Tinfoil) isn't the greenest, and
|
Trade-offs are real: the most private option (Tinfoil) isn't the greenest, and
|
||||||
the greenest option (GreenPT) isn't the most private. The point of the co-op is
|
the greenest option (GreenPT) isn't the most private. The point of the co-op is
|
||||||
@@ -37,51 +39,75 @@ that **you choose** which axis matters more for a given task.
|
|||||||
|
|
||||||
**How to pick, quick guide:**
|
**How to pick, quick guide:**
|
||||||
|
|
||||||
- **Everyday chat and drafting** → a fast model: `tinfoil/glm-5-3-flash`,
|
- **Default** → `greenpt/deepseek-v4.1-flash`: DeepSeek's efficient multimodal
|
||||||
`greenpt/glm-5.3-flash`, or `greenpt/green-l`. These handle the bulk of what
|
reasoning model on renewable energy, and the model the chat starts on. For the
|
||||||
people use chat for, at a fraction of flagship cost.
|
TEE-protected (private) version of the same model, use `tinfoil/deepseek-v4-1-flash`.
|
||||||
- **Hard reasoning, coding, agentic work** → a flagship: `tinfoil/glm-5-3` or
|
- **Everyday chat and drafting** → a fast model: `greenpt/glm-5.3-flash`.
|
||||||
`greenpt/glm-5.3` (same underlying model, different provider trade-off),
|
These handle the bulk of what people use chat for, at a fraction of flagship
|
||||||
or `tinfoil/deepseek-v4-1-flash` for long documents and tool-driven tasks.
|
cost.
|
||||||
- **Images in, text out (vision)** → `tinfoil/glm-5-3-flash`,
|
- **Hard reasoning, coding, agentic work** → a flagship: `greenpt/glm-5.3`
|
||||||
`tinfoil/deepseek-v4-1-flash`, or the Apertus models — see the ✅ in the
|
(renewable) or `tinfoil/glm-5.3` (TEE-protected) — the GLM flagship on the
|
||||||
table below.
|
provider of your choice — or `tinfoil/deepseek-v4-1-flash` for long documents
|
||||||
|
and tool-driven tasks.
|
||||||
|
- **Images in, text out (vision)** → `tinfoil/deepseek-v4-1-flash` or the
|
||||||
|
Apertus models — see the ✅ in the table below.
|
||||||
- **Cheap and cheerful** → `tinfoil/gpt-oss-120b` or `greenpt/glm-5.3-flash`.
|
- **Cheap and cheerful** → `tinfoil/gpt-oss-120b` or `greenpt/glm-5.3-flash`.
|
||||||
- **Model origins matter to you** → the Apertus models (publicly developed;
|
- **Model origins matter to you** → the Apertus models (publicly developed;
|
||||||
see below).
|
see below).
|
||||||
|
- **Semantic search / RAG (API)** → an embedding model: `greenpt/qwen3-embedding-8b`
|
||||||
|
or `greenpt/green-embeddings` (renewable energy), or `tinfoil/nomic-embed-text`
|
||||||
|
(TEE-protected — the model the co-op's own docs search runs on). API-only; see
|
||||||
|
[Embedding models](#embedding-models) below.
|
||||||
|
|
||||||
## Current models
|
## Current models
|
||||||
|
|
||||||
Pricing is per 1 million tokens, input / output (what the co-op pays the
|
Pricing is per 1 million tokens, input / output / **cached input** (what the
|
||||||
provider — your own cost is your monthly allowance, not per-token billing).
|
co-op pays the provider — your own cost is your monthly allowance, not
|
||||||
**Vision** means the model accepts images as input (screenshots, photos,
|
per-token billing). The cached-input rate applies when a model reuses a
|
||||||
documents to reason about) — all models output text. None of our current
|
shared prompt prefix (multi-turn conversations, agent loops); most providers
|
||||||
models generate images.
|
charge a steep discount for it. Models with `—` don't offer a distinct
|
||||||
|
cache-read price. **Vision** means the model accepts images as input
|
||||||
|
(screenshots, photos, documents to reason about). **Image generation** is
|
||||||
|
available via the chat's image feature (the 🎨 button) using
|
||||||
|
`regolo/qwen-image`.
|
||||||
|
|
||||||
| Model | Provider | Privacy | Green | Vision | Context | Pricing (in / out) | Notes |
|
The emoji in the model name marks its **value tier** (the same icons used in the chat picker):
|
||||||
|
|
||||||
|
- 🔒 **Private** — Tinfoil: architecturally private (TEE enclaves)
|
||||||
|
- 🌱 **Green** — GreenPT and Regolo: renewable-powered EU hosts (Regolo adds
|
||||||
|
a zero-data-retention commitment). The 🌱 emoji marks renewable-powered
|
||||||
|
providers, not a specific company.
|
||||||
|
|
||||||
|
| Model | Provider | Privacy | Green | Vision | Context | Pricing (in / out / cached) | Notes |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
| `tinfoil/glm-5-3-flash` | Tinfoil | architectural | undisclosed | ✅ | 1M | $0.40 / $1.25 | Fast multimodal MoE; great everyday default with image input. |
|
| `🌱 greenpt/deepseek-v4.1-flash` | GreenPT | policy | ✅ 100% renewable | ✅ | 1M | $0.22 / $1.10 / $0.011 | **Default.** DeepSeek's multimodal reasoning model, renewable energy; TEE twin below. Base V4.1 Flash release — not GreenPT's 17 Oct 2026 "1017" snapshot (as of 2026-11-09). |
|
||||||
| `tinfoil/glm-5-3` | Tinfoil | architectural | undisclosed | — | 1M | $1.80 / $5.75 | GLM flagship; strongest agentic model, private tier. |
|
| `🔒 tinfoil/deepseek-v4-1-flash` | Tinfoil | architectural | undisclosed | ✅ | 1M | $0.65 / $1.45 / $0.13 | TEE-protected version of the default; image input, tool calling, long docs/agents. |
|
||||||
| `tinfoil/deepseek-v4-1-flash` | Tinfoil | architectural | undisclosed | ✅ | 1M | $0.65 / $1.45 | Efficient MoE, image input, tool calling; great for long docs and agents. |
|
| `🔒 tinfoil/glm-5.3` | Tinfoil | architectural | undisclosed | — | 1M | $1.80 / $5.75 / $0.45 | GLM flagship, TEE-protected; strongest private-tier model for agentic coding and long-horizon tasks. |
|
||||||
| `tinfoil/gpt-oss-120b` | Tinfoil | architectural | undisclosed | — | 131K | $0.15 / $0.60 | Lightweight open-weight reasoning; cheapest private tier. |
|
| `🔒 tinfoil/gpt-oss-120b` | Tinfoil | architectural | undisclosed | — | 131K | $0.15 / $0.60 / — | Lightweight open-weight reasoning; cheapest private tier. |
|
||||||
| `tinfoil/kimi-k3` | Tinfoil | architectural | undisclosed | ✅ (image + video) | 262K | $4.00 / $20.00 | Moonshot's flagship multimodal MoE; premium tier. |
|
| `🌱 greenpt/glm-5.3-flash` | GreenPT | policy | ✅ 100% renewable | — | 1M | $0.11 / $0.44 / $0.022 | Fast GLM model, renewable energy. |
|
||||||
| `greenpt/green-r` | GreenPT | policy | 100% renewable | — | 128K | $0.35 / $0.95 | Reasoning-focused, renewable energy. |
|
| `🌱 greenpt/glm-5.3` | GreenPT | policy | ✅ 100% renewable | — | 1M | $1.10 / $4.40 / $0.275 | GLM flagship — strongest agentic model in the green tier. |
|
||||||
| `greenpt/green-l` | GreenPT | policy | 100% renewable | — | 128K | $0.25 / $0.80 | Lightweight, renewable energy. |
|
| `🌱 regolo/apertus-70b` | Regolo | policy + ZDR | ✅ 100% renewable | ✅ | 262K | $1.16 / $3.94 / — | Fully open sovereign model (Apache-2.0), large/capable, image understanding. |
|
||||||
| `greenpt/glm-5.3-flash` | GreenPT | policy | 100% renewable | — | 1M | $0.11 / $0.44 | Fast GLM model, renewable energy. |
|
| `🌱 regolo/qwen3.5-122b` | Regolo | policy + ZDR | ✅ 100% renewable | ✅ | 262K | $0.70 / $3.94 / — | Large vision+reasoning MoE (Qwen 3.5, 122B-A10B); 262K context. |
|
||||||
| `greenpt/glm-5.3` | GreenPT | policy | 100% renewable | — | 1M | $1.10 / $4.40 | GLM flagship — strongest agentic model in the green tier. |
|
|
||||||
| `publicai/apertus-v1.5-8b` | PublicAI | policy | undisclosed | ✅ | 262K | $0.10 / $0.20 | Fully open sovereign model, small/fast, image understanding. |
|
|
||||||
| `publicai/apertus-v1.5-70b` | PublicAI | policy | undisclosed | ✅ | 262K | $0.82 / $2.92 | Fully open sovereign model, large/capable, image understanding. |
|
|
||||||
|
|
||||||
Vision is verified working through the co-op gateway (base64 image input via
|
Vision is verified working through the co-op gateway (base64 image input via
|
||||||
`/v1/chat/completions`, standard OpenAI format). In the chat UI, just attach
|
`/v1/chat/completions`, standard OpenAI format). In the chat UI, just attach
|
||||||
an image to your message while using a ✅ model.
|
an image to your message while using a ✅ model.
|
||||||
|
|
||||||
**Same model, two providers:** `glm-5-3` (Tinfoil) and `glm-5.3` (GreenPT)
|
Context figures are measured the same way, through the gateway. Where a
|
||||||
are the same Z.ai model offered by two providers — Tinfoil's is
|
provider's public catalog advertises a smaller limit than the model actually
|
||||||
architecturally private at $1.80/$5.75; GreenPT's is 100% renewable at
|
accepts, the measured figure is the one shown here.
|
||||||
$1.10/$4.40. That price/privacy trade-off in a single comparison is the
|
|
||||||
co-op's whole idea in miniature: you pick which axis matters for the task
|
**Reading the Privacy column:** *architectural* means the protection is built
|
||||||
at hand.
|
into the infrastructure (TEE enclaves) — even the provider can't read your
|
||||||
|
prompts. *Policy* means the provider commits, by contract and policy, not to
|
||||||
|
store or reuse your data. *ZDR* (zero data retention) is a stronger policy:
|
||||||
|
the provider keeps no copy of prompts or outputs at all after the response is
|
||||||
|
delivered.
|
||||||
|
|
||||||
|
**Same model, two providers:** `glm-5.3` (Z.ai's GLM flagship) is offered both
|
||||||
|
on GreenPT (renewable, $1.10/$4.40) and on Tinfoil (TEE-protected, $1.80/$5.75)
|
||||||
|
— pick the axis that matters to you. The fast `glm-5.3-flash` variant is
|
||||||
|
GreenPT-only (Tinfoil deprecated their flash variant in October 2026).
|
||||||
|
|
||||||
### Audio models
|
### Audio models
|
||||||
|
|
||||||
@@ -92,13 +118,27 @@ model picker to avoid confusion.
|
|||||||
|
|
||||||
| Model | Provider | Use | Pricing |
|
| Model | Provider | Use | Pricing |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `tinfoil/whisper-large-v3-turbo` | Tinfoil | Speech-to-text | $0.05 / 1M input tokens |
|
| `🔒 tinfoil/whisper-large-v3-turbo` | Tinfoil | Speech-to-text | $0.01 per request (flat; zero per-token) |
|
||||||
| `tinfoil/voxtral-tts` | Tinfoil | Text-to-speech | listed $0 (verify on invoice) |
|
| `🔒 tinfoil/voxtral-small-24b` | Tinfoil | Speech-to-text (token-priced) | $0.20 / 1M input + $0.60 / 1M output tokens |
|
||||||
|
| `🔒 tinfoil/voxtral-tts` | Tinfoil | Text-to-speech | $0.002 per request (zero per-token) |
|
||||||
|
|
||||||
Both run inside Tinfoil's enclaves, so voice data gets the same architectural
|
All three run inside Tinfoil's enclaves, so voice data gets the same
|
||||||
privacy as the private chat models. TTS voices available: `neutral_female`,
|
architectural privacy as the private chat models. TTS voices available:
|
||||||
`neutral_male`, `casual_female/male`, `cheerful_female`, plus French, German,
|
`neutral_female`, `neutral_male`, `casual_female/male`, `cheerful_female`,
|
||||||
Spanish, Italian, Portuguese, Dutch, and Hindi variants.
|
plus French, German, Spanish, Italian, Portuguese, Dutch, and Hindi variants.
|
||||||
|
|
||||||
|
Tinfoil bills these at cost, but its audio API returns **no token usage** to
|
||||||
|
the gateway — so dashboard rows for these models read `$0.01 · 0 tokens`: the
|
||||||
|
charge is real, the token columns stay empty.
|
||||||
|
|
||||||
|
`whisper-large-v3-turbo` and `voxtral-tts` charge **flat per request**.
|
||||||
|
`voxtral-small-24b` is token-priced and the better pick for small requests:
|
||||||
|
sent through the chat API with `input_audio` it meters exactly per token, and
|
||||||
|
on `/v1/audio/transcriptions` (the route dictation uses) the gateway bills
|
||||||
|
Tinfoil's measured chunk-equivalent — their audio encoder tokenizes in ~30 s
|
||||||
|
chunks, so a short request costs ≈**$0.0001**, about 1/125th of the whisper
|
||||||
|
floor. For audio longer than ~30 s, prefer the chat route, which meters
|
||||||
|
exactly.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Speech-to-text
|
# Speech-to-text
|
||||||
@@ -107,6 +147,15 @@ curl https://gateway.inference.coop/v1/audio/transcriptions \
|
|||||||
-F file=@your-audio.mp3 \
|
-F file=@your-audio.mp3 \
|
||||||
-F model=tinfoil/whisper-large-v3-turbo
|
-F model=tinfoil/whisper-large-v3-turbo
|
||||||
|
|
||||||
|
# Speech-to-text (token-priced model, chat route — meters exactly per token)
|
||||||
|
curl https://gateway.inference.coop/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer ***" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model": "tinfoil/voxtral-small-24b", "messages": [{"role": "user", "content": [
|
||||||
|
{"type": "text", "text": "Transcribe this audio."},
|
||||||
|
{"type": "input_audio", "input_audio": {"data": "<base64 audio>", "format": "wav"}}
|
||||||
|
]}]}'
|
||||||
|
|
||||||
# Text-to-speech (returns WAV audio)
|
# Text-to-speech (returns WAV audio)
|
||||||
curl https://gateway.inference.coop/v1/audio/speech \
|
curl https://gateway.inference.coop/v1/audio/speech \
|
||||||
-H "Authorization: Bearer ***" \
|
-H "Authorization: Bearer ***" \
|
||||||
@@ -118,12 +167,61 @@ curl https://gateway.inference.coop/v1/audio/speech \
|
|||||||
curl https://gateway.inference.coop/v1/chat/completions \
|
curl https://gateway.inference.coop/v1/chat/completions \
|
||||||
-H "Authorization: Bearer ***" \
|
-H "Authorization: Bearer ***" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"model": "tinfoil/glm-5-3-flash", "messages": [{"role": "user", "content": [
|
-d '{"model": "tinfoil/deepseek-v4-1-flash", "messages": [{"role": "user", "content": [
|
||||||
{"type": "text", "text": "What is in this image?"},
|
{"type": "text", "text": "What is in this image?"},
|
||||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
|
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
|
||||||
]}]}'
|
]}]}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Embedding models
|
||||||
|
|
||||||
|
Embeddings turn text into a numeric vector — the input to semantic search,
|
||||||
|
clustering and RAG. They're called on `/v1/embeddings` with your co-op API key
|
||||||
|
and are **not chat models**: they never appear in the chat picker, the API is
|
||||||
|
the interface (the same shape as the audio models above). You choose between
|
||||||
|
them the same way as for chat: a TEE-protected option (Tinfoil) or
|
||||||
|
renewable-energy options (GreenPT).
|
||||||
|
|
||||||
|
| Model | Provider | Privacy | Green | Dimensions | Max input | Pricing |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `🌱 greenpt/green-embeddings` | GreenPT | policy | ✅ 100% renewable | 2560 (measured) | 32K tokens (per GreenPT's catalog) | $0.20 / 1M input tokens (no output charge) |
|
||||||
|
| `🌱 greenpt/qwen3-embedding-8b` | GreenPT | policy | ✅ 100% renewable | 4096 (measured) | 32K tokens (per GreenPT's catalog) | $0.25 / 1M input tokens (no output charge) |
|
||||||
|
| `🔒 tinfoil/nomic-embed-text` | Tinfoil | **architectural (TEE)** | — (mix not disclosed) | 768 (measured) | 8K tokens (per Tinfoil's catalog) | $0.016 / 1M input tokens (no output charge) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://gateway.inference.coop/v1/embeddings \
|
||||||
|
-H "Authorization: Bearer ***" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model": "tinfoil/nomic-embed-text", "input": "the text to embed"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The response is standard OpenAI shape: `data[0].embedding` holds the vector,
|
||||||
|
and `usage.prompt_tokens` is what meters against your allowance at the rate
|
||||||
|
above. Embeddings bill input tokens only — there is no output charge.
|
||||||
|
|
||||||
|
Vectors from different models are not interchangeable — pick one per search
|
||||||
|
index and stay with it (re-index if you switch). All three are callable with a
|
||||||
|
member key; for reference, the co-op's own documentation search runs on
|
||||||
|
`tinfoil/nomic-embed-text` inside the TEE.
|
||||||
|
|
||||||
|
### Image generation
|
||||||
|
|
||||||
|
`🌱 regolo/qwen-image` (Regolo, EU-hosted, zero data retention) generates
|
||||||
|
images from a text prompt. It runs the December 2025 "2512" checkpoint of
|
||||||
|
Qwen-Image — not the newer Qwen Image 2.1 (September 2026), which Regolo does
|
||||||
|
not list (as of 2026-11-09). In the chat, press the **Integrations** button
|
||||||
|
below the message box, choose **Image**, and describe what you want — the
|
||||||
|
generated image appears in the conversation. Via the API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://gateway.inference.coop/v1/images/generations \
|
||||||
|
-H "Authorization: Bearer ***" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model": "regolo/qwen-image", "prompt": "a watercolor painting of a mountain lake", "n": 1, "size": "512x512"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The response returns base64-encoded JPEG(s). Billed per pixel generated.
|
||||||
|
|
||||||
### About the Apertus models
|
### About the Apertus models
|
||||||
|
|
||||||
Apertus is the **Swiss AI Initiative's** open foundation model, built by a
|
Apertus is the **Swiss AI Initiative's** open foundation model, built by a
|
||||||
@@ -147,6 +245,9 @@ window, improved tool use, and instruction-following. It's the *publicly
|
|||||||
developed* counterpart to the private (Tinfoil) and renewable (GreenPT)
|
developed* counterpart to the private (Tinfoil) and renewable (GreenPT)
|
||||||
offerings — sovereignty as a third axis of member choice.
|
offerings — sovereignty as a third axis of member choice.
|
||||||
|
|
||||||
|
For more information on open-source AI and the Apertus models, see the
|
||||||
|
[European Open Source AI Index](https://osai-index.eu/database/).
|
||||||
|
|
||||||
*Source: [apertus-ai.org](https://www.apertus-ai.org/) and the
|
*Source: [apertus-ai.org](https://www.apertus-ai.org/) and the
|
||||||
[Swiss AI Hugging Face](https://huggingface.co/swiss-ai) model cards.*
|
[Swiss AI Hugging Face](https://huggingface.co/swiss-ai) model cards.*
|
||||||
|
|
||||||
@@ -156,6 +257,42 @@ numbers. Pricing here mirrors the `model_info` in the LiteLLM config
|
|||||||
(`code/litellm/config.yaml`), which is the authoritative source — the two are
|
(`code/litellm/config.yaml`), which is the authoritative source — the two are
|
||||||
kept in sync.
|
kept in sync.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
Most models here generate text. **Metis**, from GreenPT, makes *decisions*
|
||||||
|
instead: you give it a `state` (the situation, in plain text) plus up to 32
|
||||||
|
typed questions, and it returns **probabilities, choices, and scores** — never
|
||||||
|
prose. Use it when you want a calibrated judgement rather than a written
|
||||||
|
answer: "should we…?", "which option…?", "how confident is that?".
|
||||||
|
|
||||||
|
It's on the gateway at `/v1/systemone`, called with your normal co-op API key
|
||||||
|
(the same key you use for chat):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://gateway.inference.coop/v1/systemone \
|
||||||
|
-H "Authorization: Bearer ***" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "metis",
|
||||||
|
"state": "Your situation, described in plain text.",
|
||||||
|
"questions": {
|
||||||
|
"go": {"type": "noul", "instructions": "Should we proceed?",
|
||||||
|
"criteria": {"true": "proceed", "false": "do not proceed"}},
|
||||||
|
"which": {"type": "choice", "instructions": "Which factor matters most?",
|
||||||
|
"criteria": {"cost": "price", "speed": "latency"}},
|
||||||
|
"confidence": {"type": "score", "instructions": "How confident are you?",
|
||||||
|
"criteria": ["very low", "low", "medium", "high", "very high"]}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Metis is hosted by **GreenPT** — the same policy-based privacy, EU hosting, and
|
||||||
|
100% renewable energy as the 🌱 chat models. **Decisions count toward your
|
||||||
|
monthly allowance**, like every other call: each request is metered at
|
||||||
|
**$0.12 per 1M input tokens**, computed from the exact token count GreenPT
|
||||||
|
reports back, and appears in your usage record alongside your chat calls.
|
||||||
|
(Metis generates no prose, so there is no output charge.)
|
||||||
|
|
||||||
## Limitations on privacy
|
## Limitations on privacy
|
||||||
|
|
||||||
Only **Tinfoil** offers architectural privacy. The other providers are chosen
|
Only **Tinfoil** offers architectural privacy. The other providers are chosen
|
||||||
@@ -163,5 +300,8 @@ for their value (renewable energy, public models) but do not run in enclaves —
|
|||||||
prompts and responses pass through them in the ordinary way. Your **chat
|
prompts and responses pass through them in the ordinary way. Your **chat
|
||||||
history** is also stored on our server so you can revisit it, and that stored
|
history** is also stored on our server so you can revisit it, and that stored
|
||||||
history is not encrypted in a way that prevents us from technically reading it —
|
history is not encrypted in a way that prevents us from technically reading it —
|
||||||
we commit not to. The full distinction — what's architecturally private versus
|
we commit not to. Requests made through the **API** with your own key are a
|
||||||
what's a policy commitment — is in the [Privacy Policy](privacy-policy.md).
|
different matter: those are **not stored on our side at all**, beyond the usage
|
||||||
|
record needed for metering ([API access](api-access.md)). The full distinction —
|
||||||
|
what's architecturally private versus what's a policy commitment — is in the
|
||||||
|
[Privacy Policy](privacy-policy.md).
|
||||||
+2
-28
@@ -1,20 +1,11 @@
|
|||||||
# Open Questions for Member Discussion
|
# Open Questions for Member Discussion
|
||||||
|
|
||||||
A living list of decisions the cooperative needs to make. Each question is framed for discussion on [Loomio](https://forum.inference.coop). As questions are resolved, they move to a "Decisions" log (to be created) rather than being deleted.
|
A living list of some of the decisions the cooperative needs to make.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Privacy & Infrastructure
|
## Privacy & Infrastructure
|
||||||
|
|
||||||
### Search: self-managed or Tinfoil's encrypted search?
|
|
||||||
|
|
||||||
We currently run our own [SearXNG](https://searxng.github.io/searxng/) instance (`search.inference.coop`) to power web search in the chat. Tinfoil also offers a `websearch` model — a private, encrypted search that runs inside their enclave.
|
|
||||||
|
|
||||||
- **Self-managed (SearXNG):** full control, no external dependency, but search queries are visible to our own infrastructure (and to the search engines SearXNG queries).
|
|
||||||
- **Tinfoil's encrypted search:** queries are encrypted end-to-end, but it's a third-party dependency and may cost per-query.
|
|
||||||
|
|
||||||
**Question:** Do we prioritize self-hosting (control, no external deps) or encrypted search (stronger privacy, but a dependency on Tinfoil)?
|
|
||||||
|
|
||||||
### Green energy vs. privacy — when we can't have both?
|
### Green energy vs. privacy — when we can't have both?
|
||||||
|
|
||||||
Our inference runs through Tinfoil (TEE-protected, verifiable privacy), but Tinfoil's energy mix is not disclosed. Self-hosting on renewable-powered hardware (e.g. Hetzner hydropower, Scaleway wind+hydro) would be greener but would lose the TEE privacy guarantee at our current scale.
|
Our inference runs through Tinfoil (TEE-protected, verifiable privacy), but Tinfoil's energy mix is not disclosed. Self-hosting on renewable-powered hardware (e.g. Hetzner hydropower, Scaleway wind+hydro) would be greener but would lose the TEE privacy guarantee at our current scale.
|
||||||
@@ -24,12 +15,6 @@ Our inference runs through Tinfoil (TEE-protected, verifiable privacy), but Tinf
|
|||||||
|
|
||||||
**Question:** When the two conflict, which value wins? Is there a threshold (e.g. member count, cost) at which we'd switch?
|
**Question:** When the two conflict, which value wins? Is there a threshold (e.g. member count, cost) at which we'd switch?
|
||||||
|
|
||||||
### Should we adopt Tinfoil's other models (embeddings, private search, audio)?
|
|
||||||
|
|
||||||
Tinfoil's catalog includes `nomic-embed-text` (embeddings), `websearch` (private search), and audio models (Whisper, TTS). We currently use Open WebUI's bundled local embedding model for file uploads.
|
|
||||||
|
|
||||||
**Question:** Do we standardize on Tinfoil for all model needs (consistency, privacy), or keep the local embedding model (no per-token cost, no dependency)?
|
|
||||||
|
|
||||||
### Which models should we offer?
|
### Which models should we offer?
|
||||||
|
|
||||||
We currently expose three chat models: DeepSeek V4 Flash (default), GPT-OSS 120B, and GLM-5.3 Flash. Tinfoil's full catalog also includes GLM-5.3 (full), Kimi K3, Llama 3.3 70B, Gemma 4 31B, and others.
|
We currently expose three chat models: DeepSeek V4 Flash (default), GPT-OSS 120B, and GLM-5.3 Flash. Tinfoil's full catalog also includes GLM-5.3 (full), Kimi K3, Llama 3.3 70B, Gemma 4 31B, and others.
|
||||||
@@ -48,7 +33,7 @@ Currently chat history is stored on co-op servers. We could potentially switch t
|
|||||||
|
|
||||||
### How should pricing account for unequal usage?
|
### How should pricing account for unequal usage?
|
||||||
|
|
||||||
Members currently pay a flat $15/month for a $15/month credit budget. But usage is unequal — some members use far more tokens than others.
|
Members currently pay dues on a sliding scale (from $5/month) with a flat $15/month credit budget. But usage is unequal — some members use far more tokens than others.
|
||||||
|
|
||||||
- **Flat (current):** simple, predictable, but heavy users subsidize light users (or vice versa).
|
- **Flat (current):** simple, predictable, but heavy users subsidize light users (or vice versa).
|
||||||
- **Usage-based:** pay for what you use, but unpredictable and may discourage experimentation.
|
- **Usage-based:** pay for what you use, but unpredictable and may discourage experimentation.
|
||||||
@@ -57,22 +42,11 @@ Members currently pay a flat $15/month for a $15/month credit budget. But usage
|
|||||||
|
|
||||||
**Question:** What pricing model best balances fairness, simplicity, and the cooperative's solidarity values? How do we handle a member who consistently exceeds their budget?
|
**Question:** What pricing model best balances fairness, simplicity, and the cooperative's solidarity values? How do we handle a member who consistently exceeds their budget?
|
||||||
|
|
||||||
### What happens when a member's budget runs out mid-month?
|
|
||||||
|
|
||||||
Members have a **flexible balance**, not a hardcoded $15/month. The balance starts at $15 but can be topped up by payments (via Open Collective or another system) through the portal's `/admin/set-balance` endpoint, which pushes the new cap to the member's LiteLLM team budget (chat + API draw from the same balance). LiteLLM enforces the cap — requests stop when it's exhausted.
|
|
||||||
|
|
||||||
**Question:** Should members be able to top up self-serve? Should there be a grace period? Should the co-op pool surplus to cover light users? (The *mechanism* is built — these are now governance decisions, not technical blockers.)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Governance & Membership
|
## Governance & Membership
|
||||||
|
|
||||||
### What does "member-governed" mean in practice before 50 members?
|
|
||||||
|
|
||||||
The charter says the General Manager (Nathan) has full governance until 50 members, then transitions to member governance. But members can already deliberate on Loomio.
|
|
||||||
|
|
||||||
**Question:** Which decisions should be opened to member input *now* (even if non-binding), versus reserved for the General Manager during the pilot?
|
|
||||||
|
|
||||||
### How do we handle institutional members?
|
### How do we handle institutional members?
|
||||||
|
|
||||||
AI Potluck's "government-as-contributor" framing suggests a path for institutions (universities, municipalities) to contribute compute or data without owning the whole.
|
AI Potluck's "government-as-contributor" framing suggests a path for institutions (universities, municipalities) to contribute compute or data without owning the whole.
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
# Operator runbook — Cloudron tokens
|
|
||||||
|
|
||||||
How the co-op's Cloudron credentials are organized, and how to rotate them
|
|
||||||
without breaking the stack.
|
|
||||||
|
|
||||||
## The two-token split
|
|
||||||
|
|
||||||
There are **two separate** Cloudron API tokens, deliberately decoupled so
|
|
||||||
rotating one doesn't break the other:
|
|
||||||
|
|
||||||
| Token | Purpose | Where it lives |
|
|
||||||
|-------|---------|----------------|
|
|
||||||
| **Agent token** | Inferencebot's own Cloudron access | The agent's private `.env` (gitignored) |
|
|
||||||
| **Portal token** | The member portal's Cloudron access (create users, set groups, invite links) | Cloudron's env store, under the portal app's `CLOUDRON_TOKEN` |
|
|
||||||
|
|
||||||
The portal needs only a handful of Cloudron capabilities (user/group management,
|
|
||||||
invite links). It is the most exposed app (public webhook + broker endpoints),
|
|
||||||
so it must **not** hold the agent's token — a portal compromise shouldn't reach
|
|
||||||
the agent's wider Cloudron access.
|
|
||||||
|
|
||||||
## Rotating a token
|
|
||||||
|
|
||||||
### Portal token
|
|
||||||
|
|
||||||
1. In `my.inference.coop` → **Settings → API Tokens**, create a **"Read and Write"** token.
|
|
||||||
2. Set it as the portal's `CLOUDRON_TOKEN` env var (custom apps have no env-var
|
|
||||||
UI — use the Cloudron CLI or the API; see the next section).
|
|
||||||
3. Verify: the portal can reach `GET /api/v1/users` with the new token (HTTP 200).
|
|
||||||
|
|
||||||
### Agent token
|
|
||||||
|
|
||||||
1. Create a fresh token in `Settings → API Tokens`.
|
|
||||||
2. Put it in the agent's `.env` as `CLOUDRON_TOKEN`.
|
|
||||||
3. Verify against `/api/v1/cloudron/status`.
|
|
||||||
|
|
||||||
**The trap this split exists to prevent:** the portal keeps its *own copy* of a
|
|
||||||
Cloudron token. If the agent's token is rotated without also updating the portal
|
|
||||||
copy, the portal's Cloudron calls start returning `401` — which breaks
|
|
||||||
provisioning (new members can't be created) and the member dashboard (its
|
|
||||||
usage/keys data is resolved through the portal's Cloudron calls).
|
|
||||||
|
|
||||||
## Custom apps have no env-var UI
|
|
||||||
|
|
||||||
The member portal is a *custom* app (packaged by us), not an App Store app.
|
|
||||||
Custom apps do **not** expose environment variables in the Cloudron dashboard.
|
|
||||||
Env vars for custom apps are set via:
|
|
||||||
|
|
||||||
- **Cloudron CLI** (on your workstation, *not* the server): `cloudron env set --app portal.inference.coop CLOUDRON_TOKEN=...`
|
|
||||||
- **Cloudron API**: `POST /apps/:appId/configure/env`
|
|
||||||
|
|
||||||
## `configure/env` is a full replace
|
|
||||||
|
|
||||||
`POST /apps/:appId/configure/env` with body `{"env": {KEY: value, ...}}`
|
|
||||||
**replaces the entire environment** — it does not merge. Sending a partial map
|
|
||||||
(e.g. a single test variable) wipes every other variable.
|
|
||||||
|
|
||||||
- To change **one** variable, read the current env first (`GET /api/v1/apps/:id`
|
|
||||||
→ `.env`), merge in memory, and POST the full map back.
|
|
||||||
- Never probe the endpoint with a throwaway body against a live app.
|
|
||||||
- If the env is wiped, it is recoverable from a Cloudron app backup:
|
|
||||||
`GET /api/v1/apps/:appId/backups` → newest entry → `GET .../backups/:backupId/download`
|
|
||||||
(a `.tar.gz` containing `config.json`, whose `.env` field holds the original map).
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
- Cloudron CLI: `npm install -g cloudron` (install on your workstation, not the server), then `cloudron login my.inference.coop`.
|
|
||||||
- Cloudron API docs: `docs.cloudron.io/api/`.
|
|
||||||
|
|
||||||
## Open Collective: the bot token needs the `email` scope (Sept 2026)
|
|
||||||
|
|
||||||
The portal identifies members by email, read via the bot's OC personal token
|
|
||||||
(`OC_PERSONAL_TOKEN`) as `collective.members.nodes[].account.email` with the
|
|
||||||
`... on Individual { email }` inline fragment.
|
|
||||||
|
|
||||||
**The `email` scope is load-bearing.** The bot's personal token must be minted
|
|
||||||
with the `email` scope. Without it, the GraphQL `email` field returns `null` for
|
|
||||||
*every* member — including the bot's own `me { email }` — even though:
|
|
||||||
|
|
||||||
- the bot still authenticates and reports `isAdmin: true`, and
|
|
||||||
- the OC **admin CSV export still shows emails** (it's a separate, non-GraphQL
|
|
||||||
path; its `contributorEmail` column is not a GraphQL field).
|
|
||||||
|
|
||||||
**Symptoms when the scope is missing:** `fetch_members()` filters out empty
|
|
||||||
emails and returns `[]`, so the webhook can't provision anyone by email and the
|
|
||||||
nightly sweep goes into its empty-list fail-safe (skips, doesn't deactivate).
|
|
||||||
A new member's webhook fires cleanly but no "Sent email to …" log line follows,
|
|
||||||
and no Cloudron user is created. (This is easy to misread as a "guest with no
|
|
||||||
email" problem — it is not; verify the scope first.)
|
|
||||||
|
|
||||||
**Diagnosis:** run `me { email }` with the bot token. `null` here while
|
|
||||||
`isAdmin: true` → the token is missing the `email` scope (or the scope was
|
|
||||||
revoked). Fix: re-add the `email` scope to the token on Open Collective
|
|
||||||
(Settings → Developers → Personal Tokens). No token rotation, no re-granting of
|
|
||||||
admin — just the scope.
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="md-copyright">
|
||||||
|
{% if config.copyright %}
|
||||||
|
<div class="md-copyright__highlight">
|
||||||
|
{{ config.copyright }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="md-copyright__highlight">
|
||||||
|
Co-op homepage:
|
||||||
|
<a href="https://inference.coop/" rel="noopener">inference.coop</a>
|
||||||
|
</div>
|
||||||
|
<div class="md-copyright__highlight">
|
||||||
|
Documentation source:
|
||||||
|
<a href="https://git.inference.coop/co-op/docs" target="_blank" rel="noopener">co-op/docs on Gitea</a>
|
||||||
|
</div>
|
||||||
|
{% if not config.extra.generator == false %}
|
||||||
|
Made with
|
||||||
|
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||||
|
Material for MkDocs
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
+51
-8
@@ -1,12 +1,12 @@
|
|||||||
# Privacy Policy
|
# Privacy Policy
|
||||||
|
|
||||||
**Last updated:** September 10, 2026
|
**Last updated:** October 28, 2026
|
||||||
|
|
||||||
## In plain language
|
## In plain language
|
||||||
|
|
||||||
- We collect only what we need: your email, name, and basic usage data.
|
- We collect only what we need: your email, name, and basic usage data.
|
||||||
- **Your AI conversations are encrypted** — even we can't read them at the inference step.
|
- We offer a **changing variety of models with different privacy and value affordances** — some run in encrypted enclaves we cannot read, others differ. **You are responsible for assessing each model's properties and picking the one that fits your needs.**
|
||||||
- We **do** store your chat history on our server (so you can revisit it). We promise not to read it, but this is a promise, not a technical guarantee.
|
- We **do** store your chat history on our server (so you can revisit it). We promise not to read it, but this is a promise, not a technical guarantee. **Temporary chats** are the exception: nothing is stored at all, and you can make them your default — see §4 if you'd like sensitive conversations to leave nothing behind.
|
||||||
- We never train on your data, and we never sell it.
|
- We never train on your data, and we never sell it.
|
||||||
- You can export or delete your data at any time.
|
- You can export or delete your data at any time.
|
||||||
|
|
||||||
@@ -23,19 +23,60 @@ The Inference Cooperative is a member-governed project. We are **not** an ad-sup
|
|||||||
- **Account data:** your email and name (from Open Collective).
|
- **Account data:** your email and name (from Open Collective).
|
||||||
- **Membership data:** your contribution status and role.
|
- **Membership data:** your contribution status and role.
|
||||||
- **Usage data:** token counts, model used, and timestamps (for metering your monthly budget).
|
- **Usage data:** token counts, model used, and timestamps (for metering your monthly budget).
|
||||||
- **Content:** your prompts and the AI's responses (stored in your chat history).
|
- **Content:** your prompts and the AI's responses. Whether we keep them depends on how you use the service: in the **chat interface** they are saved as your chat history, while through the **API** they are not stored at all. See §4.
|
||||||
- **Billing data:** handled by Open Collective — we do not see full payment details.
|
- **Billing data:** handled by Open Collective — we do not see full payment details.
|
||||||
|
|
||||||
## 3. What we *can't* see (architectural privacy)
|
## 3. Privacy depends on the model you choose
|
||||||
|
|
||||||
Inference runs inside **encrypted enclaves** (TEEs). Your model inputs and outputs are encrypted end-to-end, so **even our own infrastructure cannot read them at the inference step.** This is verifiable through remote attestation — it's a property of the system, not a promise.
|
We serve a **changing catalog of models from multiple providers, and they do not all offer the same privacy**. Models differ in their affordances — some run inference inside **encrypted enclaves** (TEEs) where inputs and outputs are not readable in plaintext at the inference step; others are selected for renewable energy, openness, or cost rather than architectural privacy. The set of available models, and their properties, will change as members decide.
|
||||||
|
|
||||||
|
**You are responsible for assessing each model's privacy and value properties and choosing the one that fits your needs.** As a rule of thumb:
|
||||||
|
|
||||||
|
- Some models run inference in encrypted enclaves, verifiable through remote attestation — a property of the system, not a promise.
|
||||||
|
- Other models are processed in the clear under their provider's own terms and data-processing agreement.
|
||||||
|
|
||||||
|
We never read or sell your data regardless of model — but the *architectural* guarantee applies only to models that offer it. Check the current model catalog and each provider's terms before relying on a particular model for sensitive material.
|
||||||
|
|
||||||
## 4. What we *can* see (an honest caveat)
|
## 4. What we *can* see (an honest caveat)
|
||||||
|
|
||||||
Your **chat history is stored on our server** so you can revisit past conversations. Unlike the inference step, stored chat history is **not** encrypted in a way that prevents us from technically reading it.
|
Your **chat history is stored on our server** so you can revisit past conversations. Stored chat history is **not** encrypted in a way that prevents us from technically reading it, regardless of which model generated it.
|
||||||
|
|
||||||
We commit not to read it. But this is a **policy commitment**, not an architectural guarantee. We're transparent about this distinction because it matters.
|
We commit not to read it. But this is a **policy commitment**, not an architectural guarantee. We're transparent about this distinction because it matters.
|
||||||
|
|
||||||
|
Note the boundary precisely: enclave models protect your conversation **while it is being processed** — the model provider and we cannot read the exchange at inference time. Stored history is a separate leg: once a conversation is saved, it sits on our server in a form we can technically read. So for sensitive material, the strongest combination available today is a **Temporary Chat with an enclave model** — protected in transit and at inference, and never stored afterwards.
|
||||||
|
|
||||||
|
### If you use the API, nothing you send is stored
|
||||||
|
|
||||||
|
This is the part that surprises people, so it is worth stating plainly. If you call
|
||||||
|
the co-op's API with your own key and your own client, there is no conversation on
|
||||||
|
our side to save. Our gateway records **that** a request happened and what it cost:
|
||||||
|
the model, the token counts, the cost, and which key made the request. It does not
|
||||||
|
record **what you asked or what came back**.
|
||||||
|
|
||||||
|
We measured this rather than asserting it. A request with a distinctive marker was
|
||||||
|
sent through the gateway and then searched for: it appears nowhere in the gateway's
|
||||||
|
logs, the gateway's configuration enables no content logging, and the request records
|
||||||
|
themselves carry usage fields only. So the API path keeps no text.
|
||||||
|
|
||||||
|
Two boundaries on that statement, because it would be easy to read it as more than
|
||||||
|
it is:
|
||||||
|
|
||||||
|
- **We can still tell that a given key made a request**, and therefore which member
|
||||||
|
it belongs to. That link is how metering works — it is not something we hide.
|
||||||
|
- **"Not stored by us" is not the same as "not stored."** The provider's handling is
|
||||||
|
a separate question from ours, and it depends on the model you choose — see §3. Not
|
||||||
|
being stored on our side says nothing about the far end of the request.
|
||||||
|
|
||||||
|
### Temporary chats (recommended for anything sensitive)
|
||||||
|
|
||||||
|
The chat has a **Temporary Chat** mode designed exactly for this:
|
||||||
|
|
||||||
|
- Click the **Temporary Chat** toggle in the chat header, and the conversation is **never saved** — it doesn't appear in your history, it isn't written to our database, and it disappears when you leave the page.
|
||||||
|
- To make this the default for every new conversation, open **Settings → Conversations** and enable **Default Temporary Chat**. The preference is stored in your browser, so it's yours to change at any time.
|
||||||
|
- The trade-off is that there's nothing to revisit: no history, no search, no sync across devices. Use it whenever that trade is right for you — you can mix saved and temporary conversations freely.
|
||||||
|
|
||||||
|
Temporary chats change what we store, not who processes your request: the model you choose still receives your messages to generate a reply, under the properties described in §3.
|
||||||
|
|
||||||
### Operational logs
|
### Operational logs
|
||||||
|
|
||||||
Our server software keeps standard operational logs (dates, error messages, and — for account management like provisioning and welcome emails — the email addresses involved). These logs exist to run and debug the service, are kept briefly, and are not used to profile members.
|
Our server software keeps standard operational logs (dates, error messages, and — for account management like provisioning and welcome emails — the email addresses involved). These logs exist to run and debug the service, are kept briefly, and are not used to profile members.
|
||||||
@@ -52,7 +93,8 @@ Web search runs through our self-hosted search instance. Search queries are forw
|
|||||||
|
|
||||||
## 7. Third parties
|
## 7. Third parties
|
||||||
|
|
||||||
- **Tinfoil** — provides encrypted inference (cannot read your content).
|
The models we serve are provided by third parties that change over time, each with its own terms and privacy posture. We never sell your data, but when you use a model, that model's provider processes your inputs under its own terms and data-processing agreement — some in encrypted enclaves, some in the clear. Review each provider's terms before relying on it for sensitive material. Our current providers are listed in the model catalog.
|
||||||
|
|
||||||
- **Open Collective** — handles billing and membership (their own privacy policy applies).
|
- **Open Collective** — handles billing and membership (their own privacy policy applies).
|
||||||
- **Cloudron** — our self-hosted platform; no third-party cloud provider.
|
- **Cloudron** — our self-hosted platform; no third-party cloud provider.
|
||||||
- **Search engines** — receive your search queries (see §5).
|
- **Search engines** — receive your search queries (see §5).
|
||||||
@@ -60,6 +102,7 @@ Web search runs through our self-hosted search instance. Search queries are forw
|
|||||||
## 8. Retention and deletion
|
## 8. Retention and deletion
|
||||||
|
|
||||||
- Chat history is retained until you delete it.
|
- Chat history is retained until you delete it.
|
||||||
|
- **Temporary chats are never retained** — they are not written to our database at all (see §4).
|
||||||
- On leaving, your account is deactivated (data preserved for reactivation).
|
- On leaving, your account is deactivated (data preserved for reactivation).
|
||||||
- You may request full deletion at any time: info@inference.coop.
|
- You may request full deletion at any time: info@inference.coop.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Publishing the documentation site
|
||||||
|
|
||||||
|
The Markdown in this repository is the single source of truth. This directory
|
||||||
|
is the publishing layer that turns it into a member-readable static site and
|
||||||
|
puts it on the co-op's docs Surfer app at https://docs.inference.coop/.
|
||||||
|
Documents are never edited here — a change
|
||||||
|
to a document is a normal commit or pull request against the Markdown at the
|
||||||
|
repository root.
|
||||||
|
|
||||||
|
## What is built
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `../mkdocs.yml` | Site configuration (MkDocs + Material for MkDocs) |
|
||||||
|
| `brand.css` | Co-op palette + self-hosted Figtree webfont (SIL OFL 1.1) |
|
||||||
|
| `fonts/` | Figtree woff2 files and the OFL licence text |
|
||||||
|
| `logo.svg` | Co-op logo (shared with the website repo) |
|
||||||
|
| `build.sh` | Stage the Markdown into `.site-src/`, render to `.site-build/` |
|
||||||
|
| `hooks.py` | MkDocs hooks: directory-link rewriting + content-hashed stylesheet URLs |
|
||||||
|
| `deploy.sh` | Push the build into the Surfer app container |
|
||||||
|
| `poll-watcher.sh` | Polls Gitea main every 60s; deploys on change (agent host) |
|
||||||
|
| `docs-deploy-poll.service` | systemd unit for the poller |
|
||||||
|
| `webhook-receiver.py` | Gitea webhook → pull → build → deploy (agent host) |
|
||||||
|
| `webhook-receiver.service` | systemd unit for the receiver |
|
||||||
|
| `requirements.txt` | Pinned Python build toolchain |
|
||||||
|
|
||||||
|
Nothing external is loaded by the site: no CDN, no third-party font service.
|
||||||
|
Fonts are served from this repository's own output.
|
||||||
|
|
||||||
|
### Cache-proof stylesheets
|
||||||
|
|
||||||
|
`brand.css` and `typography.css` are listed under `extra_css`, so at a fixed URL
|
||||||
|
they would be vulnerable to a stale browser cache after a deploy — a warm cache
|
||||||
|
keeps serving the old file because the URL never changes. `hooks.py`'s
|
||||||
|
`on_post_build` fixes this at build time: it copies each stylesheet to
|
||||||
|
`<name>.<sha256-8>.css` in the output and rewrites the `<link>` in every built
|
||||||
|
page to the hashed name. A change to a stylesheet's content therefore always
|
||||||
|
produces a new URL. The authored sources keep their names and locations; the
|
||||||
|
hashed copies exist only in the build output. The hashed copy sits in the same
|
||||||
|
directory as the original, so the `@font-face` `url("fonts/…")` references keep
|
||||||
|
resolving. The deploy is unchanged.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
publishing/build.sh # creates .venv-docs on first run
|
||||||
|
```
|
||||||
|
|
||||||
|
Output lands in `.site-build/` (gitignored). `mkdocs build --strict` is what
|
||||||
|
CI-style runs should use, so broken links fail the build.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CLOUDRON_TOKEN=... publishing/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes the build into the **docs Surfer app** (Cloudron app `docs.inference.coop`,
|
||||||
|
`io.cloudron.surfer`) at:
|
||||||
|
|
||||||
|
- `/app/data/public/` — served at **https://docs.inference.coop/**
|
||||||
|
|
||||||
|
That is now the one canonical location. The docs used to be a pair of
|
||||||
|
directories inside the *marketing* Surfer app (`inference.coop`) as well, at
|
||||||
|
`/app/data/public-docs/` and `/app/data/public/docs/`; both were deleted when
|
||||||
|
this app took over, so `https://inference.coop/docs/` no longer serves the docs
|
||||||
|
(nothing on the marketing site linked to it).
|
||||||
|
|
||||||
|
Override the destination with `SURFER_APP` (the app's Cloudron location) and
|
||||||
|
`DOCS_DEST` (path inside the container) if you ever need to.
|
||||||
|
|
||||||
|
## Automatic rebuilds (push to main)
|
||||||
|
|
||||||
|
Two mechanisms keep the site in step with `main`; both run on the agent host
|
||||||
|
and deploy into the docs Cloudron app above.
|
||||||
|
|
||||||
|
**Polling (running now).** `docs-deploy-poll.service` polls Gitea every 60
|
||||||
|
seconds and deploys whenever `main` moves. A push appears on the site within
|
||||||
|
about a minute, with no human action. The Gitea webhook alone was not enough
|
||||||
|
because the agent host's cloud firewall only admits inbound SSH — a webhook
|
||||||
|
POST is silently dropped — while outbound access to Gitea works.
|
||||||
|
|
||||||
|
The service builds from a **dedicated clean clone** at
|
||||||
|
`/home/ubuntu/.docs-publisher`, not from anyone's working copy, so uncommitted
|
||||||
|
edits in a working clone can never block a deploy. Edit, commit, push — that
|
||||||
|
is the whole deployment story.
|
||||||
|
|
||||||
|
**Webhook (wired, waiting on one port).** A Gitea webhook is registered on
|
||||||
|
`co-op/docs` (push events, branch `main`) pointing at the receiver below. The
|
||||||
|
moment the Infomaniak panel allows inbound TCP to the agent host — port 8787
|
||||||
|
from the Cloudron box 179.237.86.116 is enough — start the receiver and pushes
|
||||||
|
deploy instantly instead of within a minute:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo systemctl enable --now docs-deploy-webhook
|
||||||
|
tail -f /home/ubuntu/docs-deploy.log # what both mechanisms are doing
|
||||||
|
```
|
||||||
|
|
||||||
|
The receiver authenticates webhooks with an HMAC-SHA256 shared secret
|
||||||
|
(`DEPLOY_WEBHOOK_SECRET` in the agent host's env file) and only accepts pushes
|
||||||
|
to `main` of `co-op/docs` from the Cloudron box. Woodpecker CI (Cloudron
|
||||||
|
catalog) could do the same job inside Cloudron, at the cost of running another
|
||||||
|
app; the agent-host mechanisms were chosen because they add no new Cloudron app.
|
||||||
|
|
||||||
|
Each deploy writes `meta.json` into the site with the commit it was built
|
||||||
|
from, so a deploy can be verified from the served site itself:
|
||||||
|
https://docs.inference.coop/meta.json
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
/* Inference Cooperative brand layer for the documentation site.
|
||||||
|
*
|
||||||
|
* Design-system source of truth: the co-op's chat/dashboard/portal/panel
|
||||||
|
* palette, carried in two places and kept identical there —
|
||||||
|
* repos/librechat-custom/assets/custom.css (html.light / html.dark RGB triples)
|
||||||
|
* repos/member-dashboard/app/main.py (the dashboard's :root palette)
|
||||||
|
* This file reuses those exact values so the documentation reads as the same
|
||||||
|
* organisation in both schemes, not a default Material theme.
|
||||||
|
*
|
||||||
|
* Standing constraint: everything here is local. No third-party font service,
|
||||||
|
* no CDN, no other external host.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* -- the co-op's light palette (dashboard/portal/panel design system) -- */
|
||||||
|
--cream: #faf8f5; /* page background */
|
||||||
|
--white: #ffffff; /* cards */
|
||||||
|
--ink: #2d3327; /* primary text (warm, not near-black) */
|
||||||
|
--muted: #6b7a62; /* secondary text */
|
||||||
|
--light-muted: #94a08c; /* tertiary text */
|
||||||
|
--green-deep: #5b8c5a;
|
||||||
|
--green-mid: #6ba86b;
|
||||||
|
--green-light: #7ab87a;
|
||||||
|
--green-link: #4e7a4d; /* dashboard link green — 4.7:1 on cream */
|
||||||
|
--green-ink: #2f5c30;
|
||||||
|
--border: #e4e0d8; /* warm neutral border (design-system value) */
|
||||||
|
--wash: #eaf3ea; /* the dashboard's green wash, used sparingly */
|
||||||
|
--chip: #f4f2ec; /* warm chip / code background */
|
||||||
|
--hover: #f0ede6; /* warm hover */
|
||||||
|
--bar-track: #eef0ea; /* neutral track for meters */
|
||||||
|
--earth-red: #a04b3a;
|
||||||
|
--earth-red-wash: #f3e6e1;
|
||||||
|
--bronze: #a97a4a;
|
||||||
|
--bronze-wash: #f1e8da;
|
||||||
|
|
||||||
|
/* -- the co-op's dark palette: the deep-pine family from the chat overlay -- */
|
||||||
|
--pine: #22271f; /* 34 39 31 — page background */
|
||||||
|
--pine-2: #2a3027; /* 42 48 39 — raised surface (footer, cards) */
|
||||||
|
--pine-3: #343b30; /* 52 59 48 — chips / code background */
|
||||||
|
--pine-hover: #2d3327; /* --ink reused as the hover band */
|
||||||
|
--dark-border: #4a5240; /* 74 82 64 — warm dark border */
|
||||||
|
--dark-text: #e8f5e8; /* 232 245 232 — light warm text */
|
||||||
|
--dark-muted: #94a08c; /* 148 160 140 — muted text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Material for MkDocs: map the palette onto the theme ---------- */
|
||||||
|
|
||||||
|
/* Light scheme: the header is a pale green WASH with dark text, not a saturated green
|
||||||
|
* bar. The colour is the homepage's "open source" pill (#e8f5e8). The deep greens stay
|
||||||
|
* for ACCENTS -- links, active navigation, rules -- rather than as large background fills.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root,
|
||||||
|
[data-md-color-scheme="default"] {
|
||||||
|
--md-default-bg-color: var(--cream);
|
||||||
|
--md-default-fg-color: var(--ink);
|
||||||
|
--md-default-fg-color--light: var(--muted);
|
||||||
|
--md-default-fg-color--lighter: var(--light-muted);
|
||||||
|
--md-default-fg-color--lightest: var(--border);
|
||||||
|
|
||||||
|
--md-primary-fg-color: #e8f5e8;
|
||||||
|
--md-primary-fg-color--light: #eef4ea;
|
||||||
|
--md-primary-fg-color--dark: var(--green-deep);
|
||||||
|
--md-primary-bg-color: var(--ink);
|
||||||
|
--md-primary-bg-color--light: rgba(250, 248, 245, 0.7);
|
||||||
|
|
||||||
|
--md-accent-fg-color: var(--earth-red);
|
||||||
|
--md-typeset-a-color: var(--green-link);
|
||||||
|
--md-typeset-color: var(--ink);
|
||||||
|
--md-typeset-table-color: var(--border);
|
||||||
|
|
||||||
|
--md-code-bg-color: var(--chip);
|
||||||
|
--md-code-fg-color: var(--ink);
|
||||||
|
|
||||||
|
--md-footer-bg-color: var(--wash);
|
||||||
|
--md-footer-fg-color: var(--ink);
|
||||||
|
--md-footer-fg-color--light: var(--muted);
|
||||||
|
|
||||||
|
--md-admonition-bg-color: var(--wash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark scheme: the co-op's own deep pine, not Material's cold blue-grey slate.
|
||||||
|
* The old page background measured #1e2129 (a blue-grey); this scheme is the
|
||||||
|
* chat overlay's dark palette (--surface-primary #22271f and its family), so a
|
||||||
|
* dark screen reads as the co-op's green identity, not stock Material. */
|
||||||
|
[data-md-color-scheme="slate"] {
|
||||||
|
--md-default-bg-color: var(--pine);
|
||||||
|
--md-default-fg-color: var(--dark-text);
|
||||||
|
--md-default-fg-color--light: var(--dark-muted);
|
||||||
|
--md-default-fg-color--lighter: #6f7a68;
|
||||||
|
--md-default-fg-color--lightest: rgba(148, 160, 140, 0.12);
|
||||||
|
|
||||||
|
/* Header bar now matches the chat overlay's dark menu: #171717 with white
|
||||||
|
* text (operator request 2026-11-09) — neutral dark, not green. */
|
||||||
|
--md-primary-fg-color: #171717;
|
||||||
|
--md-primary-fg-color--light: #171717;
|
||||||
|
--md-primary-fg-color--dark: #171717;
|
||||||
|
--md-primary-bg-color: #ffffff;
|
||||||
|
--md-primary-bg-color--light: rgba(255, 255, 255, 0.7);
|
||||||
|
|
||||||
|
--md-accent-fg-color: var(--green-light);
|
||||||
|
--md-typeset-a-color: var(--green-light);
|
||||||
|
--md-typeset-color: var(--dark-text);
|
||||||
|
--md-typeset-table-color: var(--dark-border);
|
||||||
|
|
||||||
|
--md-code-bg-color: var(--pine-3);
|
||||||
|
--md-code-fg-color: var(--dark-text);
|
||||||
|
|
||||||
|
--md-footer-bg-color: var(--pine-2);
|
||||||
|
--md-footer-fg-color: var(--dark-text);
|
||||||
|
--md-footer-fg-color--light: var(--dark-muted);
|
||||||
|
|
||||||
|
--md-admonition-bg-color: var(--pine-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Typography: Figtree, self-hosted (SIL OFL 1.1) ---------- */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Figtree";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("fonts/figtree-400.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Figtree";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("fonts/figtree-500.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Figtree";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("fonts/figtree-600.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Figtree";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("fonts/figtree-700.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--md-text-font-family: "Figtree", system-ui, -apple-system, "Segoe UI",
|
||||||
|
Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--md-code-font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono",
|
||||||
|
"Segoe UI Mono", Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Friendly details ---------- */
|
||||||
|
|
||||||
|
/* Links read like the website's: brand green, semibold, underline on hover. */
|
||||||
|
.md-typeset a {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.md-typeset a:hover,
|
||||||
|
.md-typeset a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounder cards and tables; a warm neutral border, never a near-black one. */
|
||||||
|
.md-typeset .grid.cards > ul > li,
|
||||||
|
.md-typeset .grid > .card,
|
||||||
|
.md-typeset table:not([class]) {
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
}
|
||||||
|
.md-typeset table:not([class]) {
|
||||||
|
border: 1px solid var(--md-typeset-table-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header: co-op green with the cream wordmark. */
|
||||||
|
.md-header {
|
||||||
|
box-shadow: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="slate"] .md-header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
.md-header .md-header__title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The menu-bar (tab) strip. In light it is the same pale green WASH as the
|
||||||
|
* header — #e8f5e8, the homepage's "open source" pill — not a saturated fill:
|
||||||
|
* the deep greens are accents (links, active navigation, rules), and a large
|
||||||
|
* --green-deep bar clashed with the rest of the light palette. A warm hairline
|
||||||
|
* separates the wash from the cream page below it. Dark is unchanged. */
|
||||||
|
.md-tabs {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
/* In dark, the tab row drops to the sheet's dark warm-green (dark-border
|
||||||
|
* #4a5240): quieter than green-mid, white labels still clear 7:1. */
|
||||||
|
[data-md-color-scheme="slate"] .md-tabs {
|
||||||
|
background-color: var(--dark-border);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
/* Search input: the white field is a light-mode choice only. In dark mode the
|
||||||
|
* search surfaces use the pine family — input on the chips tone, light text. */
|
||||||
|
html:not([data-md-color-scheme="slate"]) .md-search__input {
|
||||||
|
background-color: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="slate"] .md-search__form,
|
||||||
|
[data-md-color-scheme="slate"] .md-search__input {
|
||||||
|
background-color: var(--pine-3);
|
||||||
|
color: var(--dark-text);
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="slate"] .md-search__form .md-icon,
|
||||||
|
[data-md-color-scheme="slate"] .md-search__input::placeholder {
|
||||||
|
color: var(--dark-muted);
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="slate"] .md-search__scrollwrap,
|
||||||
|
[data-md-color-scheme="slate"] .md-search-result__article {
|
||||||
|
background-color: var(--pine-2);
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="slate"] .md-search-result__meta {
|
||||||
|
color: var(--dark-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admonitions get the co-op's washes instead of stock greys. */
|
||||||
|
.md-typeset .admonition,
|
||||||
|
.md-typeset details {
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
}
|
||||||
|
.md-typeset .admonition.tip,
|
||||||
|
.md-typeset .admonition.note {
|
||||||
|
border-color: var(--green-mid);
|
||||||
|
}
|
||||||
|
.md-typeset .admonition.warning {
|
||||||
|
border-color: var(--earth-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Typography: make Figtree actually apply ----------
|
||||||
|
* Material renders body and headings with var(--md-text-font-family) and defines that
|
||||||
|
* variable on <body>, so a :root definition loses to it by proximity in the DOM -- the
|
||||||
|
* same way the colours lost to the palette. Declare the font on the elements themselves
|
||||||
|
* as well. Sizing lives in publishing/typography.css, which loads after this file.
|
||||||
|
*/
|
||||||
|
body,
|
||||||
|
.md-typeset,
|
||||||
|
.md-nav,
|
||||||
|
.md-header,
|
||||||
|
.md-tabs {
|
||||||
|
font-family: "Figtree", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
||||||
|
Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset h1,
|
||||||
|
.md-typeset h2,
|
||||||
|
.md-typeset h3,
|
||||||
|
.md-typeset h4,
|
||||||
|
.md-typeset h5,
|
||||||
|
.md-typeset h6,
|
||||||
|
.md-header__title {
|
||||||
|
font-family: "Figtree", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
||||||
|
Arial, sans-serif;
|
||||||
|
}
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the member-readable documentation site from this repository.
|
||||||
|
#
|
||||||
|
# The repository is the single source of truth. This script:
|
||||||
|
# 1. stages a mirror of the documents into .site-src/ (generated, gitignored),
|
||||||
|
# because MkDocs cannot read the repository root directly;
|
||||||
|
# 2. renders .site-src/ to .site-build/ with MkDocs + Material.
|
||||||
|
#
|
||||||
|
# No document is moved, renamed, or rewritten — the staging copy is a build
|
||||||
|
# artifact and disappears with `--clean`.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# publishing/build.sh # create/use .venv-docs and build
|
||||||
|
# publishing/build.sh /path/to/venv # use an existing venv
|
||||||
|
#
|
||||||
|
# In CI (Woodpecker), install the toolchain and run MkDocs directly:
|
||||||
|
# pip install -r publishing/requirements.txt && mkdocs build --strict
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
VENV="${1:-$REPO_DIR/.venv-docs}"
|
||||||
|
SITE_SRC="$REPO_DIR/.site-src"
|
||||||
|
|
||||||
|
# 1. Stage the documents (Markdown at the root, maintenance/, incidents/) plus
|
||||||
|
# the publishing assets (fonts, CSS, icon). Exclude tooling and build output.
|
||||||
|
rm -rf "$SITE_SRC"
|
||||||
|
mkdir -p "$SITE_SRC"
|
||||||
|
tar cf - \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='.site-src' \
|
||||||
|
--exclude='.site-build' \
|
||||||
|
--exclude='.venv-docs' \
|
||||||
|
--exclude='*.sh' \
|
||||||
|
--exclude='*.py' \
|
||||||
|
--exclude='publishing/webhook-receiver.py' \
|
||||||
|
--exclude='publishing/webhook-receiver.service' \
|
||||||
|
--exclude='mkdocs.yml' \
|
||||||
|
--exclude='.gitignore' \
|
||||||
|
--exclude='requirements.txt' \
|
||||||
|
. | tar xf - -C "$SITE_SRC"
|
||||||
|
|
||||||
|
# A file that must sit at the site root (crawler rules), not under publishing/.
|
||||||
|
cp publishing/robots.txt "$SITE_SRC/robots.txt"
|
||||||
|
|
||||||
|
# The documents sometimes link to a directory (`incidents/`) the way Gitea
|
||||||
|
# renders it. A static site has no directory listing, so generate a small
|
||||||
|
# index page for any documented subdirectory that lacks one. This is a build
|
||||||
|
# artifact — the documents themselves are never touched.
|
||||||
|
(cd "$SITE_SRC" && find . -mindepth 1 -type d ! -path './publishing*' | while read -r dir; do
|
||||||
|
if compgen -G "$dir"/*.md >/dev/null && [ ! -f "$dir/index.md" ]; then
|
||||||
|
dir_name="$(basename "$dir")"
|
||||||
|
title="$(echo "$dir_name" | sed 's/-/ /g' | sed 's/^./\u&/')"
|
||||||
|
{
|
||||||
|
echo "# $title"
|
||||||
|
echo
|
||||||
|
echo "Landing page for this directory, generated by the publishing layer — the"
|
||||||
|
echo "documents themselves live untouched in the repository."
|
||||||
|
echo
|
||||||
|
for f in "$dir"/*.md; do
|
||||||
|
[ "$(basename "$f")" = "index.md" ] && continue
|
||||||
|
heading="$(grep -m1 '^# ' "$f" | sed 's/^# //' || true)"
|
||||||
|
echo "- [${heading:-$(basename "$f" .md)}]($(basename "$f"))"
|
||||||
|
done
|
||||||
|
} > "$dir/index.md"
|
||||||
|
fi
|
||||||
|
done)
|
||||||
|
|
||||||
|
# 2. Build.
|
||||||
|
if [ ! -x "$VENV/bin/mkdocs" ]; then
|
||||||
|
echo "Creating build venv at $VENV"
|
||||||
|
python3 -m venv "$VENV"
|
||||||
|
"$VENV/bin/pip" install --quiet --upgrade pip
|
||||||
|
"$VENV/bin/pip" install --quiet -r "$REPO_DIR/publishing/requirements.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building site -> $REPO_DIR/.site-build"
|
||||||
|
"$VENV/bin/mkdocs" build --clean
|
||||||
|
|
||||||
|
echo "Done: $REPO_DIR/.site-build"
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Publish the built documentation site to the docs Surfer app.
|
||||||
|
#
|
||||||
|
# The docs have their OWN Cloudron app now:
|
||||||
|
#
|
||||||
|
# Cloudron app: docs.inference.coop (io.cloudron.surfer, id 85b7ce06-e2af-49a4-bd4f-a9c2e692e3ab)
|
||||||
|
# served at: https://docs.inference.coop/
|
||||||
|
# on disk: /app/data/public (Surfer's "Default" site)
|
||||||
|
#
|
||||||
|
# They used to be a directory inside the marketing Surfer app
|
||||||
|
# (inference.coop, id 55f5f904-...), reachable at /app/data/public-docs and
|
||||||
|
# /app/data/public/docs. Both of those copies were deleted once this app was
|
||||||
|
# serving, so there is exactly one canonical location and nothing to drift.
|
||||||
|
#
|
||||||
|
# Mechanism (evidence: Cloudron app "Surfer" io.cloudron.surfer 7.4.1, and
|
||||||
|
# repos/website/deploy.sh, which deploys the marketing site the same way):
|
||||||
|
# pipe a tar of the build output into the container via `cloudron exec`, which
|
||||||
|
# needs only the Cloudron API token — no WebDAV token, SFTP credentials or SSH.
|
||||||
|
# The tar is spooled to a file inside the container first; piping it straight
|
||||||
|
# into `tar xf -` twice would leave the second extraction with an empty stdin.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# CLOUDRON_TOKEN=... publishing/deploy.sh
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# CLOUDRON_TOKEN (required) Cloudron API token
|
||||||
|
# CLOUDRON_SERVER default my.inference.coop
|
||||||
|
# SURFER_APP default docs.inference.coop (the docs app's location)
|
||||||
|
# DOCS_DEST default /app/data/public (Surfer's Default site root)
|
||||||
|
# SITE_DIR default <repo>/.site-build
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SITE_DIR="${SITE_DIR:-$REPO_DIR/.site-build}"
|
||||||
|
SERVER="${CLOUDRON_SERVER:-my.inference.coop}"
|
||||||
|
APP="${SURFER_APP:-docs.inference.coop}"
|
||||||
|
DEST="${DOCS_DEST:-/app/data/public}"
|
||||||
|
|
||||||
|
CLOUDRON_BIN="${CLOUDRON_BIN:-$(command -v cloudron || true)}"
|
||||||
|
if [ -z "$CLOUDRON_BIN" ] && [ -x "$HOME/.local/bin/cloudron" ]; then
|
||||||
|
CLOUDRON_BIN="$HOME/.local/bin/cloudron"
|
||||||
|
fi
|
||||||
|
: "${CLOUDRON_BIN:?cloudron CLI not found (set CLOUDRON_BIN or add it to PATH)}"
|
||||||
|
|
||||||
|
# The cloudron CLI is a node script (`#!/usr/bin/env node`); under systemd the
|
||||||
|
# PATH may not contain the host's node, so look in the usual places.
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
for dir in "$HOME"/.hermes/tools/node*/bin /usr/local/bin; do
|
||||||
|
if [ -x "$dir/node" ]; then export PATH="$dir:$PATH"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${CLOUDRON_TOKEN:?CLOUDRON_TOKEN is required (the Cloudron API token)}"
|
||||||
|
|
||||||
|
if [ ! -f "$SITE_DIR/index.html" ]; then
|
||||||
|
echo "No build found at $SITE_DIR — run publishing/build.sh first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build metadata: written into the output so a deploy can be verified from the
|
||||||
|
# served site itself (which push produced it, and when).
|
||||||
|
COMMIT="$(git -C "$REPO_DIR" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||||
|
COMMIT_SHORT="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
cat > "$SITE_DIR/meta.json" <<EOF
|
||||||
|
{
|
||||||
|
"commit": "$COMMIT",
|
||||||
|
"commit_short": "$COMMIT_SHORT",
|
||||||
|
"built_at": "$BUILT_AT",
|
||||||
|
"generator": "mkdocs-material (publishing/build.sh)",
|
||||||
|
"source": "https://git.inference.coop/co-op/docs"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Deploying $SITE_DIR (commit $COMMIT_SHORT) -> $SERVER:$APP"
|
||||||
|
echo " $DEST (docs.inference.coop)"
|
||||||
|
|
||||||
|
tar cf - -C "$SITE_DIR" . | \
|
||||||
|
"$CLOUDRON_BIN" --server "$SERVER" --token "$CLOUDRON_TOKEN" exec --app "$APP" -- \
|
||||||
|
sh -c "set -e
|
||||||
|
cat > /tmp/docs-site.tar
|
||||||
|
rm -rf '$DEST' && mkdir -p '$DEST' && tar xf /tmp/docs-site.tar -C '$DEST'
|
||||||
|
rm -f /tmp/docs-site.tar"
|
||||||
|
|
||||||
|
echo "Done:"
|
||||||
|
echo " https://docs.inference.coop/"
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# systemd units for the docs deploy automation (agent host).
|
||||||
|
#
|
||||||
|
# Two mechanisms, one job — keep https://docs.inference.coop/ in step with
|
||||||
|
# co-op/docs main:
|
||||||
|
#
|
||||||
|
# docs-deploy-poll.service ALWAYS ON. Polls Gitea every 60s; deploys on
|
||||||
|
# any change to main. Works with the agent
|
||||||
|
# host's current firewall (outbound only).
|
||||||
|
#
|
||||||
|
# docs-deploy-webhook.service INSTALLED, INACTIVE UNTIL THE PORT IS OPEN.
|
||||||
|
# Instant push -> deploy once the Infomaniak
|
||||||
|
# panel allows inbound TCP to the agent host
|
||||||
|
# (the Gitea webhook is already registered).
|
||||||
|
#
|
||||||
|
# Install both:
|
||||||
|
# sudo cp publishing/poll-watcher.sh publishing/webhook-receiver.py /home/ubuntu/repos/docs/publishing/
|
||||||
|
# sudo cp publishing/docs-deploy-poll.service publishing/webhook-receiver.service /etc/systemd/system/
|
||||||
|
# sudo systemctl daemon-reload
|
||||||
|
# sudo systemctl enable --now docs-deploy-poll
|
||||||
|
# sudo systemctl enable --now docs-deploy-webhook # when the port is open
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Inference docs: poll Gitea main and deploy to Surfer
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/home/ubuntu/.docs-publisher
|
||||||
|
Environment=DOCS_REPO_DIR=/home/ubuntu/.docs-publisher
|
||||||
|
ExecStart=/bin/bash /home/ubuntu/.docs-publisher/publishing/poll-watcher.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,144 @@
|
|||||||
|
"""MkDocs hooks for the publishing layer.
|
||||||
|
|
||||||
|
Two jobs, both publishing-layer only — neither edits the documents or the
|
||||||
|
authored stylesheets in the repository.
|
||||||
|
|
||||||
|
1. Directory-style links. The documents sometimes link to a directory the way
|
||||||
|
Gitea renders it, e.g. infrastructure.md contains [`incidents/`](incidents/).
|
||||||
|
On a static site with directory URLs, that href resolves relative to the
|
||||||
|
*page* (giving /infrastructure/incidents/ and a 404). This hook rewrites such
|
||||||
|
links to the generated landing page for that directory (build.sh creates
|
||||||
|
<dir>/index.md for documented directories that lack one).
|
||||||
|
|
||||||
|
2. Content-hashed stylesheet URLs. The site's own stylesheets (brand.css,
|
||||||
|
typography.css) are listed in mkdocs.yml under extra_css, so every page
|
||||||
|
links them at a FIXED URL (publishing/brand.css). A fixed URL means a browser
|
||||||
|
with a warm cache keeps serving the old stylesheet after a deploy — the URL
|
||||||
|
never changes even when the content does. Material's own assets
|
||||||
|
(assets/stylesheets/main.<hash>.min.css) are content-hashed and immune; ours
|
||||||
|
were not. This hook gives ours the same property: at build time it copies
|
||||||
|
each to <name>.<sha256-8>.css in the output and rewrites the links in the
|
||||||
|
built HTML, so any change to a stylesheet's content necessarily yields a new
|
||||||
|
URL. The authored sources keep their names and locations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from mkdocs.utils import get_relative_url
|
||||||
|
|
||||||
|
_HREF = re.compile(r'(href=)("([^"]+)/")')
|
||||||
|
|
||||||
|
|
||||||
|
def on_page_content(html, page, config, files):
|
||||||
|
index_urls = {}
|
||||||
|
for f in files:
|
||||||
|
src = f.src_path
|
||||||
|
if src.endswith("/index.md"):
|
||||||
|
index_urls[posixpath.dirname(src)] = f.url
|
||||||
|
|
||||||
|
page_dir = posixpath.dirname(page.file.src_path)
|
||||||
|
|
||||||
|
def fix(match):
|
||||||
|
href = match.group(3)
|
||||||
|
if href.startswith(("http://", "https://", "/", "#")):
|
||||||
|
return match.group(0)
|
||||||
|
target = posixpath.normpath(posixpath.join(page_dir, href))
|
||||||
|
if target in (".", "..") or target not in index_urls:
|
||||||
|
return match.group(0)
|
||||||
|
return f'{match.group(1)}"{get_relative_url(index_urls[target], page.file.url)}"'
|
||||||
|
|
||||||
|
return _HREF.sub(fix, html)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Content-hashed stylesheet URLs ----------------------------------------
|
||||||
|
|
||||||
|
# Material's own asset names use 8 hex characters (main.57a0d79d.min.css); match
|
||||||
|
# that so the site's naming is uniform and the hash is short but collision-safe
|
||||||
|
# for a handful of stylesheets.
|
||||||
|
_HASH_LEN = 8
|
||||||
|
# A hashed sibling of brand.css is brand.<8 hex>.css.
|
||||||
|
_HASHED_SIBLING = re.compile(r"^(?P<stem>.+)\.(?P<hash>[0-9a-f]{%d})(?P<ext>\.[^.]+)$" % _HASH_LEN)
|
||||||
|
|
||||||
|
|
||||||
|
def _hashed_links(config, files=None):
|
||||||
|
"""Build each extra_css file to a content-hashed name in the site output.
|
||||||
|
|
||||||
|
Returns {original-relative-path: hashed-relative-path}. The hashed copy
|
||||||
|
lands in the *same directory* as the original, so relative references
|
||||||
|
inside the CSS (e.g. brand.css's @font-face url("fonts/figtree-400.woff2"))
|
||||||
|
keep resolving. Stale hashed siblings from an earlier build are removed, so
|
||||||
|
the output does not accumulate files across builds.
|
||||||
|
"""
|
||||||
|
site_dir = config["site_dir"]
|
||||||
|
replacements = {}
|
||||||
|
|
||||||
|
for css in config.get("extra_css") or []:
|
||||||
|
rel = css.lstrip("/").replace("\\", "/")
|
||||||
|
original = os.path.join(site_dir, *rel.split("/"))
|
||||||
|
if not os.path.isfile(original):
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(original, "rb") as fh:
|
||||||
|
digest = hashlib.sha256(fh.read()).hexdigest()[:_HASH_LEN]
|
||||||
|
|
||||||
|
stem, ext = posixpath.splitext(rel)
|
||||||
|
hashed_rel = f"{stem}.{digest}{ext}"
|
||||||
|
hashed = os.path.join(site_dir, *hashed_rel.split("/"))
|
||||||
|
|
||||||
|
# Copy to the hashed name (a no-op if it is already correct).
|
||||||
|
if os.path.abspath(hashed) != os.path.abspath(original):
|
||||||
|
shutil.copyfile(original, hashed)
|
||||||
|
|
||||||
|
# Drop stale hashed siblings of the original (same stem/ext, any hash).
|
||||||
|
directory = os.path.dirname(original)
|
||||||
|
base_stem, base_ext = posixpath.splitext(os.path.basename(rel))
|
||||||
|
keep = os.path.basename(hashed)
|
||||||
|
for name in os.listdir(directory):
|
||||||
|
m = _HASHED_SIBLING.match(name)
|
||||||
|
if not m or name == keep:
|
||||||
|
continue
|
||||||
|
if m.group("stem") == base_stem and m.group("ext") == base_ext:
|
||||||
|
os.remove(os.path.join(directory, name))
|
||||||
|
|
||||||
|
replacements[rel] = hashed_rel
|
||||||
|
|
||||||
|
return replacements
|
||||||
|
|
||||||
|
|
||||||
|
def _rewrite_stylesheet_links(site_dir, replacements):
|
||||||
|
"""Point every <link href="…brand.css"> at the hashed file, in place.
|
||||||
|
|
||||||
|
The href is relative to each page (root pages use "publishing/brand.css",
|
||||||
|
nested pages "../publishing/brand.css"), so match any prefix up to the
|
||||||
|
known relative path and swap only the path tail, preserving the page's
|
||||||
|
own relative prefix.
|
||||||
|
"""
|
||||||
|
patterns = [
|
||||||
|
(re.compile(r'(href="[^"]*)' + re.escape(orig) + r'(")'), hashed)
|
||||||
|
for orig, hashed in replacements.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
for root, _dirs, filenames in os.walk(site_dir):
|
||||||
|
for name in filenames:
|
||||||
|
if not name.endswith(".html"):
|
||||||
|
continue
|
||||||
|
path = os.path.join(root, name)
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
html = fh.read()
|
||||||
|
new = html
|
||||||
|
for pattern, hashed in patterns:
|
||||||
|
new = pattern.sub(lambda m, h=hashed: m.group(1) + h + m.group(2), new)
|
||||||
|
if new != html:
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(new)
|
||||||
|
|
||||||
|
|
||||||
|
def on_post_build(config):
|
||||||
|
replacements = _hashed_links(config)
|
||||||
|
if replacements:
|
||||||
|
_rewrite_stylesheet_links(config["site_dir"], replacements)
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130 130">
|
||||||
|
<rect width="130" height="130" rx="24" fill="#faf8f5"/>
|
||||||
|
<circle cx="65" cy="14" r="5" fill="#5b8c5a"/>
|
||||||
|
<circle cx="58" cy="30" r="6" fill="#7ab87a"/>
|
||||||
|
<circle cx="74" cy="30" r="4" fill="#6ba86b"/>
|
||||||
|
<circle cx="51.5" cy="46" r="6.5" fill="#5b8c5a"/>
|
||||||
|
<circle cx="65" cy="46" r="5" fill="#7ab87a"/>
|
||||||
|
<circle cx="81" cy="46" r="4" fill="#6ba86b"/>
|
||||||
|
<circle cx="44" cy="62" r="7" fill="#6ba86b"/>
|
||||||
|
<circle cx="61" cy="62" r="5.5" fill="#5b8c5a"/>
|
||||||
|
<circle cx="76" cy="62" r="4.5" fill="#7ab87a"/>
|
||||||
|
<circle cx="89.5" cy="62" r="3.5" fill="#6ba86b"/>
|
||||||
|
<circle cx="36.5" cy="78" r="7.5" fill="#5b8c5a"/>
|
||||||
|
<circle cx="54" cy="78" r="6" fill="#7ab87a"/>
|
||||||
|
<circle cx="70" cy="78" r="5" fill="#6ba86b"/>
|
||||||
|
<circle cx="84" cy="78" r="4" fill="#5b8c5a"/>
|
||||||
|
<circle cx="97.5" cy="78" r="3.5" fill="#7ab87a"/>
|
||||||
|
<circle cx="44" cy="94" r="6.5" fill="#6ba86b"/>
|
||||||
|
<circle cx="61" cy="94" r="5.5" fill="#5b8c5a"/>
|
||||||
|
<circle cx="76" cy="94" r="4.5" fill="#7ab87a"/>
|
||||||
|
<circle cx="89.5" cy="94" r="3.5" fill="#6ba86b"/>
|
||||||
|
<circle cx="51.5" cy="110" r="5.5" fill="#5b8c5a"/>
|
||||||
|
<circle cx="65" cy="110" r="4.5" fill="#7ab87a"/>
|
||||||
|
<circle cx="81" cy="110" r="3.5" fill="#6ba86b"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
Executable
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Poll Gitea for new commits on main and deploy the docs site when it moves.
|
||||||
|
#
|
||||||
|
# Why polling: the agent host's cloud firewall only admits inbound SSH, so a
|
||||||
|
# Gitea webhook cannot reach the receiver (publishing/webhook-receiver.py) —
|
||||||
|
# every port is silently dropped. Outbound to git.inference.coop works, so the
|
||||||
|
# agent host polls instead: a push to main is picked up within ~a minute and
|
||||||
|
# deployed with no human action. When the operator opens a TCP port to the
|
||||||
|
# agent host in the Infomaniak panel, the webhook takes over (its unit is
|
||||||
|
# already installed); until then this watcher is the automation.
|
||||||
|
#
|
||||||
|
# Runs on the agent host as a systemd service (docs-deploy-poll.service).
|
||||||
|
# Deploy steps are serialised with the same lock the webhook receiver uses, so
|
||||||
|
# the two mechanisms can never deploy concurrently.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="${DOCS_REPO_DIR:-/home/ubuntu/repos/docs}"
|
||||||
|
ENV_FILE="${DOCS_ENV_FILE:-/home/ubuntu/.hermes/.env}"
|
||||||
|
LOG_FILE="${DOCS_DEPLOY_LOG:-/home/ubuntu/docs-deploy.log}"
|
||||||
|
LOCK_FILE="${DOCS_DEPLOY_LOCK:-/home/ubuntu/.docs-deploy.lock}"
|
||||||
|
STATE_FILE="${DOCS_DEPLOY_STATE:-/home/ubuntu/.docs-deploy.state}"
|
||||||
|
INTERVAL="${DOCS_POLL_INTERVAL:-60}"
|
||||||
|
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [poll] $*" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
|
||||||
|
# The Cloudron token travels by environment only; it is read from the env file
|
||||||
|
# and never echoed.
|
||||||
|
CLOUDRON_TOKEN="$(sed -n 's/^CLOUDRON_TOKEN=//p' "$ENV_FILE" | head -1 | tr -d '"' | tr -d "'")"
|
||||||
|
export CLOUDRON_TOKEN
|
||||||
|
if [ -z "$CLOUDRON_TOKEN" ]; then
|
||||||
|
log "FATAL: CLOUDRON_TOKEN not found in $ENV_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "watcher started: polling $REPO_DIR every ${INTERVAL}s"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ! git fetch --quiet origin main 2>>"$LOG_FILE"; then
|
||||||
|
log "git fetch failed; will retry"
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE="$(git rev-parse origin/main 2>/dev/null || echo none)"
|
||||||
|
# Compare against the commit last DEPLOYED (not the clone's HEAD): a push
|
||||||
|
# made from this very clone would otherwise look like "no change".
|
||||||
|
LAST_DEPLOYED="$(cat "$STATE_FILE" 2>/dev/null || echo none)"
|
||||||
|
|
||||||
|
if [ "$LAST_DEPLOYED" != "$REMOTE" ]; then
|
||||||
|
log "main moved: ${LAST_DEPLOYED:0:12} -> ${REMOTE:0:12}; deploying"
|
||||||
|
# flock -n shares the receiver's lock: one deploy at a time, ever.
|
||||||
|
if flock -n "$LOCK_FILE" bash -c '
|
||||||
|
set -e
|
||||||
|
cd "$1"
|
||||||
|
git pull --ff-only origin main
|
||||||
|
bash publishing/build.sh
|
||||||
|
bash publishing/deploy.sh
|
||||||
|
' _ "$REPO_DIR" >>"$LOG_FILE" 2>&1; then
|
||||||
|
printf '%s\n' "$REMOTE" > "$STATE_FILE"
|
||||||
|
log "deploy finished for ${REMOTE:0:12}"
|
||||||
|
else
|
||||||
|
log "deploy FAILED for ${REMOTE:0:12} (see log above)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Pinned build toolchain for the documentation site.
|
||||||
|
# Reproducible: `pip install -r publishing/requirements.txt && mkdocs build`
|
||||||
|
mkdocs-material==9.8.0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Documentation site crawler rules
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/* Typography for the co-op documentation site.
|
||||||
|
*
|
||||||
|
* Loaded after publishing/brand.css on purpose: the font has to be declared on the
|
||||||
|
* elements themselves. Material renders body and headings with
|
||||||
|
* var(--md-text-font-family), which it defines on <body>, so a :root-level value
|
||||||
|
* loses to it by DOM proximity no matter how the cascade is ordered.
|
||||||
|
*
|
||||||
|
* Figtree is self-hosted -- see the @font-face rules in brand.css. No CDN, no
|
||||||
|
* third-party font service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body,
|
||||||
|
.md-typeset,
|
||||||
|
.md-nav,
|
||||||
|
.md-header,
|
||||||
|
.md-tabs,
|
||||||
|
.md-footer {
|
||||||
|
font-family: "Figtree", system-ui, -apple-system, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset h1,
|
||||||
|
.md-typeset h2,
|
||||||
|
.md-typeset h3,
|
||||||
|
.md-typeset h4,
|
||||||
|
.md-typeset h5,
|
||||||
|
.md-typeset h6,
|
||||||
|
.md-header__title,
|
||||||
|
.md-nav__title {
|
||||||
|
font-family: "Figtree", system-ui, -apple-system, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset h1,
|
||||||
|
.md-typeset h2,
|
||||||
|
.md-typeset h3,
|
||||||
|
.md-typeset h4,
|
||||||
|
.md-header__title {
|
||||||
|
/* Never break a word mid-way; wrap at the space, and let the browser balance the
|
||||||
|
two lines rather than leaving one word on its own. */
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: normal;
|
||||||
|
hyphens: none;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fluid scale. Fixed rem sizes do not shrink on a phone -- Material keeps a 20px root
|
||||||
|
* even at 360px wide -- so a 2.6rem h1 stays 52px and "Cooperative" (297px at that
|
||||||
|
* size) runs out of room on anything narrower than ~370px and breaks mid-word.
|
||||||
|
* These track the viewport instead and land at the same desktop size.
|
||||||
|
*/
|
||||||
|
.md-typeset h1 { font-size: clamp(1.6rem, 1.5rem + 3.2vw, 2.6rem); font-weight: 700; letter-spacing: -0.02em; }
|
||||||
|
.md-typeset h2 { font-size: clamp(1.3rem, 1.1rem + 1.6vw, 1.7rem); font-weight: 600; }
|
||||||
|
.md-typeset h3 { font-size: clamp(1.1rem, 1rem + 0.7vw, 1.25rem); font-weight: 600; }
|
||||||
|
.md-header__title { font-size: clamp(0.85rem, 0.7rem + 0.8vw, 0.95rem); font-weight: 600; }
|
||||||
|
|
||||||
|
/* Long prose stays readable; measured in characters rather than pixels. */
|
||||||
|
.md-typeset { line-height: 1.65; }
|
||||||
|
|
||||||
|
/* Colour is scheme-specific, so it stays out of the rules above.
|
||||||
|
* Light: the co-op's warm ink (#2d3327), never a neutral near-black.
|
||||||
|
* Dark: the chat overlay's light warm text (#e8f5e8) on the deep-pine page. */
|
||||||
|
[data-md-color-scheme="default"] .md-typeset h1,
|
||||||
|
[data-md-color-scheme="default"] .md-typeset h2,
|
||||||
|
[data-md-color-scheme="default"] .md-typeset h3,
|
||||||
|
[data-md-color-scheme="default"] .md-typeset h4 {
|
||||||
|
color: #2d3327;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset h1,
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset h2,
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset h3,
|
||||||
|
[data-md-color-scheme="slate"] .md-typeset h4 {
|
||||||
|
color: #e8f5e8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gitea webhook receiver that rebuilds and republishes the docs site.
|
||||||
|
|
||||||
|
Flow: a push to main in co-op/docs fires a Gitea webhook -> this receiver
|
||||||
|
verifies it -> pulls the latest main -> runs publishing/build.sh ->
|
||||||
|
publishing/deploy.sh (which writes the build into the Surfer app).
|
||||||
|
|
||||||
|
Runs on the agent host (no new Cloudron app). Standard library only.
|
||||||
|
|
||||||
|
Security model:
|
||||||
|
* Only POSTs from the Cloudron box (git.inference.coop's host) are accepted.
|
||||||
|
* The payload must carry a valid HMAC-SHA256 signature (X-Gitea-Signature,
|
||||||
|
shared secret DEPLOY_WEBHOOK_SECRET). The secret itself is never sent.
|
||||||
|
* Only pushes to refs/heads/main of co-op/docs trigger a deploy.
|
||||||
|
* The receiver binds 0.0.0.0; an iptables rule installed by the systemd unit
|
||||||
|
narrows the port to the Cloudron box as well (defence in depth).
|
||||||
|
|
||||||
|
Secrets are read from /home/ubuntu/.hermes/.env and are never logged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
REPO_DIR = os.environ.get("DOCS_REPO_DIR", "/home/ubuntu/repos/docs")
|
||||||
|
ENV_FILE = os.environ.get("DOCS_ENV_FILE", "/home/ubuntu/.hermes/.env")
|
||||||
|
LOG_FILE = os.environ.get("DOCS_DEPLOY_LOG", "/home/ubuntu/docs-deploy.log")
|
||||||
|
LOCK_FILE = os.environ.get("DOCS_DEPLOY_LOCK", "/home/ubuntu/.docs-deploy.lock")
|
||||||
|
PORT = int(os.environ.get("DOCS_WEBHOOK_PORT", "8787"))
|
||||||
|
EXPECT_REPO = "co-op/docs"
|
||||||
|
EXPECT_BRANCH = "refs/heads/main"
|
||||||
|
ALLOWED_SOURCES = [
|
||||||
|
ipaddress.ip_network(n)
|
||||||
|
for n in os.environ.get(
|
||||||
|
"DOCS_WEBHOOK_SOURCES",
|
||||||
|
# The Cloudron box (hosts git.inference.coop) plus loopback for local tests.
|
||||||
|
"179.237.86.116/32,2001:1600:18:206::89/128,127.0.0.1/32,::1/128",
|
||||||
|
).split(",")
|
||||||
|
if n.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
log = logging.getLogger("docs-deploy")
|
||||||
|
deploy_lock_file = open(LOCK_FILE, "a")
|
||||||
|
|
||||||
|
|
||||||
|
def load_env_file(path):
|
||||||
|
"""Return KEY=VALUE pairs from the agent host's env file (values stay local)."""
|
||||||
|
values = {}
|
||||||
|
try:
|
||||||
|
with open(path) as handle:
|
||||||
|
for line in handle:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
values[key.strip()] = value.strip().strip('"').strip("'")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
_FILE_ENV = load_env_file(ENV_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def secret(name):
|
||||||
|
return os.environ.get(name) or _FILE_ENV.get(name) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def source_allowed(address):
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(address.split("%")[0])
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return any(ip in net for net in ALLOWED_SOURCES)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, timeout, cwd=None):
|
||||||
|
log.info("exec: %s", " ".join(cmd))
|
||||||
|
# Hand the build/deploy scripts the credentials they need (from the env
|
||||||
|
# file, if not already set). Values are passed by environment only and are
|
||||||
|
# never logged.
|
||||||
|
env = dict(os.environ)
|
||||||
|
for key in ("CLOUDRON_TOKEN", "CLOUDRON_SERVER", "SURFER_APP", "GIT_ASKPASS"):
|
||||||
|
if not env.get(key) and secret(key):
|
||||||
|
env[key] = secret(key)
|
||||||
|
env.setdefault("GIT_ASKPASS", "/home/ubuntu/.hermes/scripts/git_askpass.sh")
|
||||||
|
env.setdefault("PATH", os.environ.get("PATH", ""))
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, cwd=cwd or REPO_DIR, capture_output=True, text=True, timeout=timeout,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
if result.stdout.strip():
|
||||||
|
log.info("stdout: %s", result.stdout.strip()[-2000:])
|
||||||
|
if result.returncode != 0 and result.stderr.strip():
|
||||||
|
log.error("stderr: %s", result.stderr.strip()[-2000:])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def deploy(commit_hint=""):
|
||||||
|
"""Pull main, rebuild, republish. Returns (ok, log_tail)."""
|
||||||
|
try:
|
||||||
|
fcntl.flock(deploy_lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except BlockingIOError:
|
||||||
|
return False, "another deploy is already running; skipped"
|
||||||
|
|
||||||
|
tail = []
|
||||||
|
try:
|
||||||
|
steps = [
|
||||||
|
(["git", "pull", "--ff-only", "origin", "main"], 120),
|
||||||
|
(["bash", "publishing/build.sh"], 900),
|
||||||
|
(["bash", "publishing/deploy.sh"], 600),
|
||||||
|
]
|
||||||
|
for cmd, timeout in steps:
|
||||||
|
result = run(cmd, timeout)
|
||||||
|
out = (result.stdout or "") + (result.stderr or "")
|
||||||
|
tail.append(f"$ {' '.join(cmd)} -> exit {result.returncode}")
|
||||||
|
tail.extend(out.strip().splitlines()[-12:])
|
||||||
|
if result.returncode != 0:
|
||||||
|
log.error("step failed: %s", " ".join(cmd))
|
||||||
|
return False, "\n".join(tail)
|
||||||
|
log.info("deploy complete%s", f" ({commit_hint})" if commit_hint else "")
|
||||||
|
return True, "\n".join(tail)
|
||||||
|
except Exception as exc: # noqa: BLE001 - report, never crash the receiver
|
||||||
|
log.exception("deploy failed")
|
||||||
|
tail.append(f"error: {exc}")
|
||||||
|
return False, "\n".join(tail)
|
||||||
|
finally:
|
||||||
|
fcntl.flock(deploy_lock_file, fcntl.LOCK_UN)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
server_version = "docs-deploy/1.0"
|
||||||
|
|
||||||
|
def log_message(self, format, *args): # noqa: A002 - stdlib signature
|
||||||
|
log.info("%s - %s", self.client_address[0], format % args)
|
||||||
|
|
||||||
|
def _json(self, code, payload):
|
||||||
|
body = json.dumps(payload).encode()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == "/healthz":
|
||||||
|
return self._json(200, {"status": "ok"})
|
||||||
|
return self._json(404, {"error": "not found"})
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
peer = self.client_address[0]
|
||||||
|
if not source_allowed(peer):
|
||||||
|
log.warning("rejected: source %s not allowed", peer)
|
||||||
|
return self._json(403, {"error": "forbidden"})
|
||||||
|
|
||||||
|
if self.headers.get("X-Gitea-Event") != "push":
|
||||||
|
return self._json(400, {"error": "not a push event"})
|
||||||
|
|
||||||
|
length = int(self.headers.get("Content-Length") or 0)
|
||||||
|
if length <= 0 or length > 1_000_000:
|
||||||
|
return self._json(400, {"error": "bad payload size"})
|
||||||
|
body = self.rfile.read(length)
|
||||||
|
|
||||||
|
signature = self.headers.get("X-Gitea-Signature") or ""
|
||||||
|
expected = hmac.new(
|
||||||
|
secret("DEPLOY_WEBHOOK_SECRET").encode(), body, hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
if not signature or not hmac.compare_digest(signature, expected):
|
||||||
|
log.warning("rejected: bad signature from %s", peer)
|
||||||
|
return self._json(403, {"error": "invalid signature"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return self._json(400, {"error": "invalid json"})
|
||||||
|
|
||||||
|
repo = (payload.get("repository") or {}).get("full_name", "")
|
||||||
|
ref = payload.get("ref", "")
|
||||||
|
commit = (payload.get("after") or "")[:12]
|
||||||
|
pusher = ((payload.get("pusher") or {}).get("login")) or "unknown"
|
||||||
|
if repo != EXPECT_REPO or ref != EXPECT_BRANCH:
|
||||||
|
log.info("ignored: push to %s %s (not %s %s)", repo, ref, EXPECT_REPO, EXPECT_BRANCH)
|
||||||
|
return self._json(200, {"status": "ignored", "reason": "not main on co-op/docs"})
|
||||||
|
|
||||||
|
log.info("push received: %s %s commit=%s pusher=%s", repo, ref, commit, pusher)
|
||||||
|
threading.Thread(target=self._run_deploy, args=(commit,), daemon=True).start()
|
||||||
|
return self._json(202, {"status": "accepted", "commit": commit})
|
||||||
|
|
||||||
|
def _run_deploy(self, commit):
|
||||||
|
ok, tail = deploy(commit)
|
||||||
|
log.info("deploy result for %s: %s", commit, "ok" if ok else "FAILED")
|
||||||
|
log.info("tail:\n%s", tail[-1500:])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)s %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not secret("DEPLOY_WEBHOOK_SECRET"):
|
||||||
|
log.error("DEPLOY_WEBHOOK_SECRET is not set; refusing to start")
|
||||||
|
sys.exit(1)
|
||||||
|
if not secret("CLOUDRON_TOKEN"):
|
||||||
|
log.warning("CLOUDRON_TOKEN not set; deploys will fail")
|
||||||
|
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
||||||
|
log.info("listening on 0.0.0.0:%d, repo %s", PORT, REPO_DIR)
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# systemd unit for the docs deploy webhook receiver (agent host).
|
||||||
|
#
|
||||||
|
# Install:
|
||||||
|
# sudo cp publishing/webhook-receiver.service /etc/systemd/system/
|
||||||
|
# sudo systemctl daemon-reload && sudo systemctl enable --now docs-deploy-webhook
|
||||||
|
#
|
||||||
|
# The ExecStartPre lines narrow the webhook port to the Cloudron box (which
|
||||||
|
# hosts git.inference.coop); the receiver additionally checks the source address
|
||||||
|
# and the payload signature, so a scanner hitting the port gets nothing.
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Inference docs: Gitea webhook -> build -> Surfer deploy
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/home/ubuntu/.docs-publisher
|
||||||
|
Environment=DOCS_REPO_DIR=/home/ubuntu/.docs-publisher
|
||||||
|
ExecStartPre=+/bin/sh -c '/usr/sbin/iptables -C INPUT -i lo -p tcp --dport 8787 -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I INPUT 1 -i lo -p tcp --dport 8787 -j ACCEPT'
|
||||||
|
ExecStartPre=+/bin/sh -c '/usr/sbin/iptables -C INPUT -p tcp --dport 8787 -s 179.237.86.116 -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I INPUT 2 -p tcp --dport 8787 -s 179.237.86.116 -j ACCEPT'
|
||||||
|
ExecStartPre=+/bin/sh -c '/usr/sbin/iptables -C INPUT -p tcp --dport 8787 ! -s 179.237.86.116 -j DROP 2>/dev/null || /usr/sbin/iptables -I INPUT 3 -p tcp --dport 8787 ! -s 179.237.86.116 -j DROP'
|
||||||
|
ExecStartPre=+/bin/sh -c '/usr/sbin/ip6tables -C INPUT -p tcp --dport 8787 -j DROP 2>/dev/null || /usr/sbin/ip6tables -I INPUT -p tcp --dport 8787 -j DROP'
|
||||||
|
ExecStart=/home/ubuntu/.docs-publisher/.venv-docs/bin/python /home/ubuntu/.docs-publisher/publishing/webhook-receiver.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+28
-18
@@ -1,15 +1,15 @@
|
|||||||
# Terms of Service
|
# Terms of Service
|
||||||
|
|
||||||
**Last updated:** September 10, 2026
|
**Last updated:** October 6, 2026
|
||||||
|
|
||||||
## In plain language
|
## In plain language
|
||||||
|
|
||||||
- You are a **member**, not a customer. You help govern this project.
|
- You are a **member**, not a customer. You help govern this project.
|
||||||
- We provide **private AI chat** through open models, protected by encryption.
|
- We provide AI chat and API access through open models, with **a variety of privacy and value options** (some models run in encrypted enclaves; others differ — see §3).
|
||||||
- Your prompts and the AI's outputs are yours to keep.
|
- Your prompts and the AI's outputs are yours to keep.
|
||||||
- We don't train on your data, and we don't sell it.
|
- We don't train on your data, and we don't sell it.
|
||||||
- AI can be wrong. Use your judgment.
|
- AI can be wrong. Use your judgment.
|
||||||
- If you misuse the service, we may remove you (per our Charter).
|
- If you misuse the service, we may remove you (per our Charter) — and you are responsible for what you do with it (§4).
|
||||||
- Our liability is limited to what you've paid in dues.
|
- Our liability is limited to what you've paid in dues.
|
||||||
|
|
||||||
The full terms are below. If anything is unclear, write to info@inference.coop.
|
The full terms are below. If anything is unclear, write to info@inference.coop.
|
||||||
@@ -18,13 +18,14 @@ The full terms are below. If anything is unclear, write to info@inference.coop.
|
|||||||
|
|
||||||
## 1. Who we are
|
## 1. Who we are
|
||||||
|
|
||||||
The **Inference Cooperative** ("we", "us") is a member-governed project providing private AI inference. We are fiscally sponsored by **Metagov**, a nonprofit that provides our financial and legal services.
|
The **Inference Cooperative** ("we", "us") is a member-governed project providing AI inference. We are fiscally sponsored by **Metagov**, a nonprofit that provides our financial and legal structure.
|
||||||
|
|
||||||
When you join, you become a **member** — someone with a voice in how the project is run — not a customer of a vendor.
|
When you join, you become a **member** — someone with a voice in how the project is run — not a customer of a vendor.
|
||||||
|
|
||||||
## 2. Membership
|
## 2. Membership
|
||||||
|
|
||||||
- Membership is through **Open Collective**, where you pay dues on a sliding scale.
|
- Membership is through **Open Collective**, where you pay dues on a sliding
|
||||||
|
scale (currently $5/month minimum).
|
||||||
- You must be **18 or older** to join.
|
- You must be **18 or older** to join.
|
||||||
- Membership is governed by our [Charter](charter.md), including our Code of Conduct.
|
- Membership is governed by our [Charter](charter.md), including our Code of Conduct.
|
||||||
- We may suspend or remove membership for Code of Conduct violations or non-payment, as described in the Charter.
|
- We may suspend or remove membership for Code of Conduct violations or non-payment, as described in the Charter.
|
||||||
@@ -33,25 +34,34 @@ When you join, you become a **member** — someone with a voice in how the proje
|
|||||||
|
|
||||||
We provide:
|
We provide:
|
||||||
|
|
||||||
- **Private AI chat** through open-source models, served through encrypted (TEE-protected) infrastructure.
|
- **AI chat and a developer API** through a changing catalog of open models from multiple providers. You choose per task not just *which model*, but *which affordances it offers* — models differ in privacy architecture, energy source, openness, capability, and cost. Some run inference inside encrypted enclaves; others do not. Some run on renewable energy; others do not. The catalog changes as members decide, and the specific models and providers available will evolve.
|
||||||
- **Web search** and **file uploads** within the chat interface.
|
- **Web search** and **file uploads** within the chat interface.
|
||||||
- **Governance** — a seat in decisions about models, pricing, and direction.
|
- **Governance** — a seat in decisions about models, pricing, and direction.
|
||||||
|
|
||||||
|
Models are **not interchangeable in their affordances**. Each model has its own privacy, sustainability, and openness properties, and the set of available models changes over time. You are **responsible for assessing** which model fits your needs for a given task — in particular its privacy properties — before relying on it. See our [Privacy Policy](privacy-policy.md) for what this means in practice.
|
||||||
|
|
||||||
The service is provided **"as is"** and may change as the project evolves. We do not guarantee uptime or specific performance at this stage.
|
The service is provided **"as is"** and may change as the project evolves. We do not guarantee uptime or specific performance at this stage.
|
||||||
|
|
||||||
## 4. Acceptable use
|
## 4. Acceptable use
|
||||||
|
|
||||||
You agree not to:
|
You agree not to use the service to:
|
||||||
|
|
||||||
- Use the service for anything illegal.
|
- Violate any law or regulation.
|
||||||
- Harass, abuse, or harm others.
|
- Generate, distribute, or facilitate **illegal, harmful, abusive, or discriminatory content** — including content that sexualizes minors, defrauds, incites violence, or harasses others.
|
||||||
- Attempt to circumvent usage limits or the privacy architecture.
|
- Create **deepfakes, impersonations, or deceptive media** of real people without their consent.
|
||||||
- Share your account credentials or resell access.
|
- Process **regulated data** — including protected health information (e.g. HIPAA), payment-card data, or other categories that require a separate legal agreement — unless we have agreed to that in writing.
|
||||||
- Use the service to generate content that violates our [Code of Conduct](charter.md).
|
- Submit **special-category personal data** under GDPR Article 9 (health, ethnicity, political views, religion, biometrics, etc.) without a written agreement.
|
||||||
|
- Use the service in or for the benefit of **embargoed or sanctioned jurisdictions or persons**, or otherwise in violation of **export-control or sanctions law**.
|
||||||
|
- Use AI outputs as the **sole basis for consequential decisions** — legal, medical, financial, employment, housing, or safety-critical — without appropriate human review and professional oversight.
|
||||||
|
- Use the service for **high-risk or fail-safe** applications (e.g. critical infrastructure, life-support, weapons systems).
|
||||||
|
- Attempt to circumvent usage limits, the privacy architecture, or rate limits.
|
||||||
|
- Share your account credentials, or resell or redistribute access to the service.
|
||||||
|
|
||||||
|
You are **responsible for what you submit and what you do with AI outputs**, and you must have the rights needed to submit your content.
|
||||||
|
|
||||||
## 5. Usage limits
|
## 5. Usage limits
|
||||||
|
|
||||||
- Membership includes a **monthly credit budget** (currently $15 of usage).
|
- Membership includes a **monthly credit budget** (currently $15 of usage, the same for every member regardless of what you pay).
|
||||||
- Members may also purchase **credit packs**: non-expiring credits drawn only
|
- Members may also purchase **credit packs**: non-expiring credits drawn only
|
||||||
after the monthly budget is exhausted. Credits do not expire and carry over
|
after the monthly budget is exhausted. Credits do not expire and carry over
|
||||||
from month to month.
|
from month to month.
|
||||||
@@ -67,7 +77,7 @@ You agree not to:
|
|||||||
|
|
||||||
## 7. Privacy
|
## 7. Privacy
|
||||||
|
|
||||||
Your privacy is a core value, not an afterthought. Inference runs in encrypted enclaves so that even our own infrastructure cannot read your model inputs and outputs at the inference step. Details are in the [Privacy Policy](privacy-policy.md).
|
Privacy is a core value, but it is **not uniform across the service** — and the set of available models changes over time. Models differ in their privacy affordances: some run inference in encrypted enclaves so inputs and outputs are not readable in plaintext at the inference step, while others are selected for renewable energy or openness rather than architectural privacy. **You are responsible for assessing each model's privacy properties and choosing one that matches your needs.** See the [Privacy Policy](privacy-policy.md) for the current picture.
|
||||||
|
|
||||||
## 8. Termination
|
## 8. Termination
|
||||||
|
|
||||||
@@ -77,14 +87,14 @@ Your privacy is a core value, not an afterthought. Inference runs in encrypted e
|
|||||||
|
|
||||||
## 9. Governance
|
## 9. Governance
|
||||||
|
|
||||||
- Decisions are made through **Loomio** and governed by the [Charter](charter.md).
|
The Co-op's governance proceeds according to the content of the current [Charter](charter.md).
|
||||||
- During the pilot phase, the General Manager holds final discretion; this transitions to full member governance as the project grows.
|
|
||||||
|
|
||||||
## 10. Disclaimers and liability
|
## 10. Disclaimers, liability, and indemnity
|
||||||
|
|
||||||
- **AI outputs may be inaccurate or inappropriate.** We provide no warranty of correctness. Use your judgment.
|
- **AI outputs may be inaccurate, biased, or inappropriate.** We provide no warranty of correctness. Use your judgment, and do not rely on outputs as a sole source of truth or as a substitute for professional advice.
|
||||||
- To the maximum extent permitted by law, our liability is limited to the amount of dues you have paid.
|
- To the maximum extent permitted by law, our liability is limited to the amount of dues you have paid.
|
||||||
- Members are not personally liable for the project's obligations.
|
- Members are not personally liable for the project's obligations.
|
||||||
|
- **You agree to indemnify and hold harmless** the Inference Cooperative, Metagov, and their officers, directors, employees, and agents from any claims, losses, liabilities, damages, costs, and expenses (including reasonable legal fees) arising from **your use of the service, your content, your violation of law or third-party rights, or your violation of these Terms**. In plain terms: if what you do with the service causes a legal problem, it is your responsibility, not the co-op's or Metagov's.
|
||||||
|
|
||||||
## 11. Changes to these terms
|
## 11. Changes to these terms
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Working with the agent
|
||||||
|
|
||||||
|
The co-op runs a small number of agents to help steward its infrastructure,
|
||||||
|
each isolated from the others: its own OS user, its own installation, and its
|
||||||
|
own gateway key with a model allowlist and a budget enforced at the gateway.
|
||||||
|
The first, **inferencebot**, stewards the co-op's services. The second,
|
||||||
|
**infrastructure-bot**, lives in #infrastructure and answers only when
|
||||||
|
mentioned. They run on co-op infrastructure under co-op identities, use the
|
||||||
|
co-op's own gateway for inference, and their skills, scripts and configuration
|
||||||
|
are mirrored to a Gitea repository nightly. These norms apply to all of them.
|
||||||
|
This page is for members who read their output or give them instructions.
|
||||||
|
|
||||||
|
## Two handles that look alike
|
||||||
|
|
||||||
|
- `@inference-bot:inference.coop` (with a hyphen) is the co-op's automation
|
||||||
|
account. It administers rooms, sends invitations and posts the updates feed.
|
||||||
|
It does not take instructions.
|
||||||
|
- `@inferencebot:inference.coop` (no hyphen) is the steward agent that
|
||||||
|
admitted members can instruct.
|
||||||
|
|
||||||
|
## Where they speak and listen
|
||||||
|
|
||||||
|
- **#inferencebot-updates** is read-only: daily digests, model changes,
|
||||||
|
upgrades, maintenance, outages. Posts are anonymized.
|
||||||
|
- **Instructing an agent** is limited to the members on that agent's
|
||||||
|
allowlist of Matrix IDs, kept in the agent's own configuration, and only in
|
||||||
|
the rooms where it is configured to answer; most require a mention. The
|
||||||
|
General Manager decides who is on the list, and the list is published here
|
||||||
|
once it is settled.
|
||||||
|
|
||||||
|
## What to ask for
|
||||||
|
|
||||||
|
- Operational work that a runbook already describes, or that ends in a PR, an
|
||||||
|
issue or a post members can see.
|
||||||
|
- Research and summaries that would otherwise land in an issue.
|
||||||
|
- Status: what is running, what failed, what changed.
|
||||||
|
|
||||||
|
## What not to ask in a shared room
|
||||||
|
|
||||||
|
- Anything involving member-identifying data. That work happens in a session a
|
||||||
|
named steward opened, and the result is reported without the data.
|
||||||
|
- Anything that moves money or changes who is a member. Those go through the
|
||||||
|
portal and a human.
|
||||||
|
- Credentials, in either direction.
|
||||||
|
|
||||||
|
## What they do without asking
|
||||||
|
|
||||||
|
Read, monitor, draft, open issues and PRs, post updates, run the scheduled jobs
|
||||||
|
listed in [Infrastructure](infrastructure.md), and deploy bug and security
|
||||||
|
fixes immediately, reporting each one in #inferencebot-updates.
|
||||||
|
|
||||||
|
## What always needs a human
|
||||||
|
|
||||||
|
- Member-visible, destructive or risky changes to the gateway, the portal or
|
||||||
|
the chat: confirmed before they deploy.
|
||||||
|
- Changing the model catalog, prices or budgets.
|
||||||
|
- Minting or rotating credentials.
|
||||||
|
- Anything a runbook marks "confirm".
|
||||||
|
|
||||||
|
This matches the standing policy the agents operate under today. A stricter
|
||||||
|
norm is a policy decision for the [forum](https://forum.inference.coop).
|
||||||
|
|
||||||
|
## Attribution and the off switch
|
||||||
|
|
||||||
|
Every instruction an agent acts on is attributable to the member who gave it,
|
||||||
|
in the room where it was given. If an agent misbehaves, the operator, or anyone
|
||||||
|
holding gateway-admin and Matrix account access, can stop it: revoke its
|
||||||
|
gateway key, then its Matrix access token. Stewards generally do not hold that
|
||||||
|
access today, so say what you saw in #general and the operator acts. Stopping
|
||||||
|
is not restarting: the backup and the git mirror restore state, not uptime,
|
||||||
|
and someone has to start the agent again. The steps, and the steps for
|
||||||
|
rotating everything an agent holds, are in the agent's runbook; the credential
|
||||||
|
inventory in the [operator token
|
||||||
|
runbook](https://git.inference.coop/co-op/ops-docs/src/branch/main/operator-token-runbook.md)
|
||||||
|
says what that is.
|
||||||
|
|
||||||
|
Changes to this page are policy: propose them in the
|
||||||
|
[forum](https://forum.inference.coop).
|
||||||
Reference in new issue
Block a user