A first draft of the member-facing norms listed as a design goal on
co-op/support#17: where the agent speaks and listens, what to ask it for,
what not to ask it in a shared room, what it does without confirmation,
what always needs a human, and how it is stopped. Linked from the README
index and the Matrix section of getting-started. Nothing here changes how
the agent is configured.
brand.css and typography.css are referenced at fixed URLs via extra_css, so a
warm browser cache could keep serving the previous stylesheet after a deploy.
Add on_post_build to publishing/hooks.py: at build time copy each extra_css
file to <name>.<sha256-8>.css in the output and rewrite the <link> in the built
HTML. Any content change now yields a new URL. Sources keep their names; hashed
copies exist only in the build output and stale ones are pruned on rebuild.
The light scheme filled the header and tabs with green-deep, which read as heavy. They now
use the homepage's "open source" pill colour #e8f5e8 with ink text, and the deeper greens
are kept for accents -- links, active navigation and rules -- rather than as background
fills. The dark scheme is unchanged.
- README: render the Key links as real Markdown links (mailto: for the
contact address); drop the 'lives in a public git repository' note now
that the footer carries the source link.
- mkdocs.yml: add a theme override (custom_dir: overrides) that puts a
'Documentation source' link in the footer of every page.
- Move the operator runbook, maintenance notes and incident write-up out
of the published site into the git-only co-op/ops-docs repository;
remove them from the nav and repoint cross-references at Gitea.
The docs now have a dedicated Cloudron app (io.cloudron.surfer,
location docs.inference.coop, id 85b7ce06-e2af-49a4-bd4f-a9c2e692e3ab)
serving /app/data/public. deploy.sh writes there and nowhere else; the
two copies inside the marketing Surfer app (public-docs and
public/docs) have been deleted, so there is one canonical location.
SURFER_APP now defaults to docs.inference.coop and DOCS_DEST to
/app/data/public.
Light: warm neutrals (#e4e0d8 borders, #f4f2ec code, #6b7a62 muted) instead of
near-blacks; links use the dashboard's #4e7a4d (4.7:1 on cream).
Dark: replace Material's cold blue-grey slate (#1e2129 page) with the chat
overlay's deep-pine palette (#22271f page, #e8f5e8 text, #7ab87a accent), so a
dark screen reads as the co-op's green identity rather than stock Material.
Add .woodpecker.yml — install the pinned toolchain, run publishing/build.sh,
then publishing/deploy.sh. No build logic is duplicated; the publishing layer
stays the source of truth. Manual runs only for now: the push webhook is not
wired until the Woodpecker instance is restricted, connected to Gitea and
proven by a manual run.
At 360px wide the h1 was still 52px, because Material keeps a 20px root on mobile, so
"Cooperative" (297px at that size) left only ~30px of slack before breaking mid-word.
Headings now use clamp() to track the viewport and reach the same desktop sizes, with
word-break: normal, hyphens: none and text-wrap: balance so wrapping happens at the
space and the two lines are balanced rather than one word sitting alone.
Dark mode: the slate scheme set the header to #3f6b3e, a dark green on a dark page, so
the bar disappeared into the background. It is now green-mid with near-black text.
Typography moves to publishing/typography.css, loaded after brand.css, and declares
Figtree on the elements themselves -- Material defines --md-text-font-family on <body>,
so a :root value loses to it by DOM proximity. Heading colours are scoped to the light
scheme, since ink is unreadable on the dark one.
Material emits its own font variables and defines --md-text-font-family on <body>,
so brand.css's :root value lost to it by DOM proximity -- the same failure mode as the
palette, and the reason the site rendered entirely in the system font stack with
Material's default 32px/300 headings.
theme.font: false stops Material supplying typography, and brand.css now declares the
font on body, the navigation, the header and every heading, with heading sizes closer to
the website. Figtree is still self-hosted from publishing/fonts.
The theme was configured with primary: teal and accent: deep orange, so every page
shipped data-md-color-primary="teal" / data-md-color-accent="deep-orange" and Material
set --md-primary-fg-color and friends on <body>. Those variables then beat brand.css,
which sets them on <html>, because a value on the nearer ancestor wins for descendants
regardless of specificity. The result: cream background and ink from brand.css applied,
but the header, links and accents stayed Material's defaults.
primary/accent are now "custom", Material emits no palette of its own, and the co-op
variables in publishing/brand.css take effect.
- docs-deploy-poll.service and the receiver now build from
/home/ubuntu/.docs-publisher, so uncommitted edits in a working clone can
never block a deploy
- the poller compares origin/main against the last DEPLOYED commit (state
file), so a push made from the deploy clone itself also triggers a deploy
A MkDocs hook turns links like [incidents/](incidents/) — which Gitea
renders as a directory listing but a static site would resolve against the
page URL — into links to the generated landing page for that directory.
infrastructure.md links to incidents/ the way Gitea renders directories.
A static site has no directory listing, so the build now generates a small
landing page (with each document's own title) for any documented directory
that lacks an index. Build artifact only — no document is touched.
The agent host's cloud firewall only admits inbound SSH, so the registered
Gitea webhook cannot reach the receiver. docs-deploy-poll.service polls
Gitea every 60s over outbound HTTPS and deploys whenever main moves; the
webhook stays wired for the moment a port is opened in the Infomaniak panel.
Both serialise on the same deploy lock.
Renders the repository's Markdown (which stays where it is) into a
member-readable static site with Material for MkDocs: co-op brand colours
and logo, self-hosted OFL Figtree, offline search, no external hosts.
- publishing/build.sh stage .site-src, render .site-build (mkdocs --strict)
- publishing/deploy.sh write the build into the Surfer app container
- publishing/webhook-receiver.py + .service Gitea push hook on main
triggers pull -> build -> deploy (agent host, no new
Cloudron app)
Metering on /v1/systemone is live: each call is billed at $0.12 per 1M
input tokens, computed from the exact token count GreenPT returns and
attributed to the calling member key. Replace the "not yet metered"
warnings in models.md and api-access.md with the accurate position.
(metering verified 2026-11-03; code/litellm patch_passthrough_token_cost.py)
models.md claimed decisions count toward the member allowance. Verified today that a
/v1/systemone call registers no spend against the calling key, so the claim was wrong.
States the gap plainly instead. api-access.md now names the decisions route, the fact
that it is not an OpenAI chat endpoint, and the metering caveat.
Add a Decisions section: Metis returns probabilities/choices/scores rather than
text, is called at /v1/systemone with a member's normal gateway key, is
GreenPT-hosted (policy-based privacy, EU, renewable), and is metered
approximately (flat per-call estimate, not exact tokens).
- New 'Where it runs' section: services moved to the co-op's own Infomaniak
server (Swiss sovereign cloud, Geneva, renewable-powered, ISO 14001/50001/27001)
from a personal Hostinger account on 2026-11-02; note the steward bot runs on a
separate machine and was not part of the migration.
- DNS subsection: managed manually at domains.coop (nameservers yoursrs.com); the
zone is only 11 records, with the apex A/AAAA and a wildcard A/AAAA carrying
every application hostname.
- Backups subsection: encrypted nightly backups to Infomaniak Encrypted S3, kept
seven days; the steward-bot host backs up selected home directories to the same
object storage.
- New 'Mail' section: inbound mail is received directly by the box; the newsletter
(Listmonk) sends via the Scaleway Transactional Email relay (SPF, DKIM and DMARC
pass at p=reject), while the member portal still sends through the Cloudron mail
service on the box. Both send as info@inference.coop.
- Correct the webmail URL: mail.inference.coop -> email.inference.coop (the
Cloudron mail app vhost; mail.inference.coop is not a configured vhost).
Reviewed by dckc (review 6, APPROVED) and cross-checked against the gateway upgrade
runbook. Merged by push: this repo's approval gate is restricted to the Owners team,
which currently holds only machine accounts, and Gitea will not accept their API
reviews as approvals — see the PR discussion.
Reviewed by dckc (review 6, APPROVED) and cross-checked against the gateway upgrade
runbook. Merged by push: this repo's approval gate is restricted to the Owners team,
which currently holds only machine accounts, and Gitea will not accept their API
reviews as approvals — see the PR discussion.
The policy described chat history and enclave models carefully but never addressed
the API path, and §2 read as though all content is retained — which is wrong for
members using their own keys.
Verified before writing, rather than restating policy language: a request with a
distinctive marker was sent through the gateway and searched for afterwards. It
appears nowhere in the gateway's logs, `litellm_settings` enables no content
logging, and the request records carry usage fields only (model, tokens, cost, key).
- privacy-policy.md §2: content retention now depends on the surface, with a pointer.
- privacy-policy.md §4: new subsection on API requests, including the two limits that
stop it being read as more than it says — we can still link a request to a key and
therefore a member, and "not stored by us" says nothing about the provider.
- api-access.md: a "what we store" section, since that is the page an API user reads
before worrying about it.
- models.md: the privacy-limitations section now distinguishes chat from API.
Records the completed state: room power and the portal's invite credential now both
belong to @inference-admin; the old account's admin flag is dropped, it is
deactivated, and its devices and tokens are deleted. Notes the two corrupt rooms it
cannot leave, so the next person does not re-investigate.
Four automated accounts touch the homeserver and two of them differed by one
character, which caused real confusion (a bot appearing to announce itself; the
steward unable to post in a room it had been asked to report to). This records
the distinction rather than leaving it to be rediscovered.
Power belongs to @inference-admin, voice belongs to @inferencebot, and
@inference-admin never speaks in a member room. @inference-bot is being retired:
its room power was transferred to @inference-admin, which is now administrator of
record for #general, #community, #design, #skillshare, #updates and
#infrastructure.
Also records the one live dependency that blocks finishing: the portal's member
invites still authenticate as the old account via MATRIX_BOT_TOKEN, so that has
to be repointed before its credentials are revoked. Without this written down the
next person would revoke the token and silently break member invites.
Verified while writing: the portal's token hashes to the old account's device
VYQDYBXULA, which is also the unexplained python-httpx client seen using that
account today.
The provider's published catalog says this model takes 30K input and has no
vision support. Both of those are wrong. Sent through our own gateway:
- an image returns the right answer ("Red" for a solid red PNG)
- prompts of 32K, 70K, 130K and 262K input tokens are all accepted, with
Regolo's own usage accounting reporting prompt_tokens=262021
Our own description of the v1.5 generation — image understanding and a 262K
context window — matches what the model demonstrably does, which is why the
figure goes to 262K. The old 65K undersold the model by roughly 4x.
Regolo's meta-model for qwen3.5-122b says 120K; it processed 262,023 tokens and
still followed the instruction at the end, so that row's 262K is confirmed as
written and is left alone.
Adds a line under the table recording that context figures here are measured
through the gateway rather than copied from provider catalogs, because that
difference is exactly what caused a member to report this as an error.
- §4: the exact boundary — enclave models protect inference, stored
history is operator-readable; recommend Temporary Chat + enclave model
for sensitive material. New subsection on Temporary Chat (per-
conversation toggle, Default Temporary Chat setting, trade-offs,
what it does and does not change).
- §8: temporary chats are never retained.
- getting-started: add Temporary Chat to the chat feature list with
the Settings → Conversations default toggle.
No engineering change; documents what the deployed chat (1.4.5) already
supports. Member request 2026-10-28.
The write-up has sat on incident/2026-10-02-gateway-cve-upgrade since the
outage and that branch is now far behind main. This lifts just the incident
text and the CVE note into incidents/, attributed to InferenceBot with the
substance unchanged, and links it from infrastructure.md under a new
Incident log section.