HENUSSE Making your company readable by AI. Outside, and in.

Operational Method · Under the bonnet

How our systems are built.

This page is written for the person who will open the bonnet before signing: your CTO, your IT provider, or you. It says what our systems are made of, what in them is deterministic and what is entrusted to a model, where a human signs, what happens when a component fails, and what you receive if we disappear. The systems themselves are described here.

What it is made of

Python 3.13, no heavy framework. About 1,900 modules and 1,090 test files in the house repository, run locally before every commit by pre-commit guards. All the code is under Git, engine and site themes included: every change is a commit, and every deployment starts from one. There is no hosted continuous integration: the tests run on the machine that deploys, and a commit that breaks them does not leave.

Data lives in SQLite, one database per client, in journal mode (WAL), with an explicit write lock. Event logs are append-only JSON files: one line per fact, never rewritten. Queues are updated by copy then atomic replace, never in place.

On the website side, WordPress remains the carrier for pages and articles, so the client keeps an ordinary, transferable tool. Delivery goes through a house PHP plugin; the client approval portal is a separate application, on its own server, with its own database.

The language models are not ours. We use OpenAI’s, Anthropic’s and Google’s through their official tools. We train no model and fine-tune none: our work is around the model, not inside it.

The bricks in service today

Systems: Debian on the house server, Arch on the workstation, which has also served as the test bench for the WordPress mirrors, a Scaleway virtual server for the client portal, clients’ shared hosting at OVH and HostSilo. The private network between machines is Tailscale; crawling leaves through a separate WireGuard network namespace; Caddy is the front door with automatic certificates; Samba shares files to a NAS.

Runtime: Docker Compose for the WordPress staging mirrors, databases and client applications, about thirty containers in service; systemd for the house services and timers, twenty-six units for the engine alone; cron for the scheduled rails.

Languages: Python 3.13 for the engine, the pipelines and the applications; PHP 8.4 for eight in-house WordPress plugins and a PrestaShop module; Node 22 with TypeScript, Vite and Preact for the interfaces that need them; Go for a network tool; Bash for the glue.

Data: SQLite when one database per client is enough; PostgreSQL 16 when more is needed, including pgvector for similarity search; MariaDB behind WordPress; Valkey as a cache; append-only JSON logs for facts.

Documents and images: Tesseract OCR and vision-model reading for photographed invoices, pypdf to read PDFs, ReportLab and WeasyPrint to produce them, Pillow for images, Playwright and Chromium to drive a browser in our readings, take the captures and run the end-to-end tests, ffmpeg and MediaPipe for video.

Integrations: Telegram as the operator channel, mail over SMTP and IMAP, Google’s Search Console and PageSpeed APIs, DataForSEO, the WordPress REST API, FTPS and SFTP delivery to the hosts. Models: OpenAI, Anthropic, Google and Perplexity through their APIs and official tools, with a bubblewrap sandbox for untrusted text.

Backups: internal state and off-site copy by dedicated tools, a snapshot before every change, the PostgreSQL base dumped nightly to the NAS with thirty days of retention, timestamped copies of the SQLite bases, and a restore drill scheduled every quarter.

Agents write, a person decides

Two people do not hold 1,900 modules by hand. The code is written and reviewed by coding agents driven from the command line: OpenAI’s Codex, Anthropic’s Claude Code, Google’s Gemini, and Hermes, an open agent we run ourselves. They work in parallel in named tmux sessions, up to twenty-four at a time, each on a written task, with an orchestrator that holds the context, dispatches, and verifies.

The rule is the same as for our clients: the agent proposes, the person decides. Nothing enters the repository without the pre-commit guards and the tests; nothing goes to production without a snapshot and a way back. Reviews are adversarial: a second agent tries to refute the first one’s work before a person rules, and a wave is only closed when the review comes back empty.

The agents do not hold the keys. Third-party text is read in a sandbox with no tool rights, model access is renewed by system timers rather than stored in code, and a helper that is not on the allowed list is refused.

What is checked with no model at all

In the editorial pipeline, thirteen checks are pure code, with no model call: internal links, markup, structured data, metadata, citation integrity and coverage, dated claims, voice fingerprint, reading flow, taxonomy stance, pre-publication certification, rich-results verification, final score. A model only steps in where judgement is needed: drafting, polishing, fact-checking, editorial review.

The threshold is written: a score under 60 rejects, between 60 and 75 sends back for revision, above 75 passes. An error-severity defect rejects whatever the score. A hard fact-check failure blocks publication on every path. A text in an unexpected language, or a language scanner that fails, puts the article on hold for a human instead of letting it through.

Em dashes, the mark of machine-written text, are removed by a deterministic rewrite before any check, and an editorial engine that returns one raises an error.

The decision stays with the person

Every client publication goes through a veto window whose states are named: notice sent, pending review, approved, vetoed, approved by silence, expired with no answer. The module that keeps this window never publishes: on expiry it records an intent, and the real deployment sits behind a separate switch, off by default.

The approval portal keeps two families of tables: a projection written by the engine alone (tenants, windows, artifacts) and a ledger written by the portal alone (decisions, views, link requests). The portal’s database rebuilds entirely from the engine: losing it loses nothing canonical.

If an article’s content changes after approval, the approval falls and a new one is required. That is an explicit state, not a convention.

What is planned before it happens

Model calls follow a written order: the OpenAI tool first, the Anthropic tool second, paid APIs last. Paid APIs are closed by default: a stored key is not enough, an explicit switch is needed for them to serve. After three consecutive failures of the first two, they are set aside for ten minutes. A call has a maximum delay (five minutes through the tool, two through the API) and a monthly budget: alarm at 80%, refusal at 100%.

A response that is not valid JSON is treated as no response, at every place a response is read. A corrupt state file on disk is treated as an empty state. Text supplied by a third party (a client, a website) is read in an isolated sandbox, with a delay enforced from outside, by a separate model arm that is not allowed to use tools.

Two runs cannot step on each other: one lock per client, in the database, with a timeout. Logs are append-only and queues are updated by atomic replace, so a job restarted after an outage counts nothing twice. What fails is written to a per-client failure log; an article blocked by a check is materialised into a “needs review” folder instead of vanishing.

Each client in its own box

Each client is a distinct Unix user, unable to log in, whose directory is its domain. Its secrets are in a file readable by it and its group only. Services run with systemd hardening: no privilege escalation, read-only system, home directories invisible, private temporary directory.

A job started by the scheduler must match an allowed list line by line: fixed user, fixed directory, fixed log. A helper not on the list is refused with a dedicated error code. Sudo fragments are named and versioned in the repository.

Knowing what happened

Each client has its logs: scheduled runs, approvals, withdrawals, failures, events. Secrets are redacted before writing. An operator dashboard, restarted automatically if it falls, exposes health, incidents, costs and activity.

Watch rails run at known intervals: heartbeat every minute, health check every fifteen minutes, publication watch every five minutes, missed-slot watch, weekly review-due watch, nightly sync of the published index.

Going back is a written path

Internal state backup, off-site backup, snapshot before any change, and a restore drill with its own tool. Every deployment wave leaves a named revert script behind. A publication can be withdrawn by a dedicated tool, and the delivery plugin has its own rollback procedure.

Twenty-four written procedures cover the ordinary cases: backup and restore, incident response, restarting a service, a stuck scheduled job, updating, onboarding a new client.

If we disappear tomorrow

The repository opens with an entry document, an architecture document, a glossary, and 205 dated architecture decisions, each explaining what was chosen and why. Procedures and session reports sit in the same place. A developer who receives the repository and the secrets has what is needed to run the system without calling us.

On the client side, scoping produces a scope document that belongs to you, and the engagement ends with an exit inventory: data, rules, documentation, access. The support and maintenance terms say what is followed over time: dependencies, access, data flows, business rules, transferability.

On screen, not on slides

One production system, live: the database, the logs, an article that passes the checks and another that stays behind. A handover package from an existing project: the architecture, the dependencies, the access inventory, the takeover procedure. And, with the client’s consent, a system installed on their side, without their data.

Ask for it at the first conversation. These are the pieces we would ask for in your place.

WhatsApp