The marker vocabulary had six entries - REQ, PREF, PROVEN, ASSUMED, DEFERRED, INSTANCE. They could say how strong a requirement was and where it came from. None could say it was not being met. That gap is why five unmet REQs in this document went unrecorded for weeks: there was no notation to write them in, so nobody wrote them.
DIVERGENCE is added as the seventh, with the rule attached. A REQ that stops being met does not become a PREF and is not rewritten to describe what was built. It stands, the gap is recorded, the correction is owed by the thing. A specification that agrees with whatever exists specifies nothing. No requirement in this revision was lowered.
DIV-001 at section 14 item 11: the AGPL section 13 source link is a numbered constraint the application code will follow, and it is not met on a public deployment. DIV-002 at section 9 twice and section 8.3: the FastAPI control plane, the worker unit and the SQLite queue do not exist, and five declared dependencies are imported by nothing. DIV-003 at sections 1.3 and 5.2: the instance is publicly reachable against three REQs. DIV-004 at section 11: eight declared keys are read by nothing.
Section 11 also gains MECHCOMP_MAX_EXPORT_MM, read by the code since cdde394 and declared nowhere. That is the divergence running the other way and the harder one to notice, because a missing key looks like nothing at all.
Section 5.2 records that the divergence may be a misfiling rather than a violation. WORK-ORDER-004 section 3 says dev abbreviates Mechanical Compiler Developers, that the name is production, and that it appears on printed material. If that reading holds, 5.2 never applied to this instance and 5.3 does. Written as a decision, not a correction.
Section 1.1 gets a note that it was right about CadQuery all along. DIV-005 was opened against this paragraph rather than against HANDOFF section 5, which had it backwards for three weeks.
Section 15 gate 3 now states its status: dependencies, toolchain image and scaffolding all met, worker unit not met, gate does not pass. Section 19 records the port complete since 20 AUG, retained because the rejected cases being part of the contract is why the oracle means anything.
STAGING-STATE specification reference updated from revision 5 to 5.4.
Applied by anchored patcher. The first attempt failed on one anchor - the env block comment column was 35, not 39 - and nothing was written, including the thirteen correct patches. Suite 643 passed, oracle intact. Documentation only.
1005 lines
40 KiB
Markdown
1005 lines
40 KiB
Markdown
# ENVIRONMENT.md
|
|
|
|
Specification for a Mechanical Compiler instance.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Revision | 5.4 (2026-09-13) |
|
|
| Supersedes | Revisions 1 through 4 |
|
|
| Basis | Revision 5.3, reconciled against the running instance during the documentation audit of 2026-09-13. Divergences recorded; no requirement lowered. |
|
|
| Scope | **Host-agnostic.** Applies to any instance. |
|
|
| Instance state | `STAGING-STATE.md`, and later `PRODUCTION-STATE.md` |
|
|
| Failure evidence | `FAILURES.md` |
|
|
| Project sequence | `ROADMAP.md` |
|
|
| Working method | `PROCESS.md` |
|
|
|
|
---
|
|
|
|
## 0. How to use this document
|
|
|
|
This describes what an instance must **be**. It does not describe what any
|
|
particular host currently **is** — that lives in the corresponding state file.
|
|
|
|
Revision 4 mixed the two, which is why it aged badly the moment a real host
|
|
disagreed with it. Everything specific to `srv-b` has been removed.
|
|
|
|
### Authority order
|
|
|
|
0. **`PROCESS.md`** — how work is done. Read before issuing any command.
|
|
1. **The instance state file** — factual, wins on any question of what is true.
|
|
2. **`FAILURES.md`** — evidence from contact with hosts. Read this *before*
|
|
writing automation, not after.
|
|
3. **This document** — the specification, corrected whenever proven facts
|
|
invalidate it.
|
|
4. **`ROADMAP.md`** — product sequence. Environment work never invents
|
|
application code.
|
|
|
|
### Markers
|
|
|
|
| Marker | Meaning |
|
|
|---|---|
|
|
| **REQ** | Required. The work is blocked or wrong without it. |
|
|
| **PREF** | Preferred. Substitute freely, but record the substitution. |
|
|
| **PROVEN** | Established by a real build. Do not re-derive; see the cited failure. |
|
|
| **ASSUMED** | Decided without confirmation. Listed in §16. |
|
|
| **DEFERRED** | Deliberately postponed. Not a defect, not a gap. |
|
|
| **INSTANCE** | A value supplied per instance, not fixed here. |
|
|
| **DIVERGENCE** | The requirement stands and is **not currently met**. Cites its entry in `DIVERGENCES.md`. |
|
|
|
|
**DIVERGENCE is never a reason to lower a requirement.** A REQ that stops being
|
|
met does not become a PREF, and it is not rewritten to describe what was built.
|
|
It stands, the gap is recorded, and the correction is owed by the thing rather
|
|
than by this document. A specification that agrees with whatever exists
|
|
specifies nothing.
|
|
|
|
Added 2026-09-13. Its absence is why five unmet requirements in this document
|
|
went unrecorded for weeks — there was no notation to write them in, so nobody
|
|
wrote them. The six markers above could say how strong a requirement was and
|
|
where it came from, and none could say it was not being met.
|
|
|
|
### Provisioning method
|
|
|
|
**REQ — manual first.** One command group at a time. Failures are recorded
|
|
before they are corrected. The smallest corrective experiment is preferred over
|
|
the one that also fixes three adjacent worries.
|
|
|
|
Automation is **derived from a proven manual procedure**, never written ahead of
|
|
one. Revision 4 said the opposite. `FAILURES.md` is why it changed: three of
|
|
this document's assumptions were wrong in ways only a real host revealed, and
|
|
one of them (F-003) produced a container that booted, reported success, and had
|
|
four broken services.
|
|
|
|
---
|
|
|
|
## 1. Five decisions that shape everything below
|
|
|
|
### 1.1 The 2D path and the 3D path are separable
|
|
|
|
The catalogue's product is a 2D cross-section rendered to SVG. Shapely plus our
|
|
own code covers that completely. CadQuery/OCCT is needed only for STEP and mesh
|
|
export.
|
|
|
|
They stay separate — separate requirements files, separate code paths, and a CI
|
|
job running the suite with the CAD dependencies absent. Three reasons, none of
|
|
which is packaging politics:
|
|
|
|
1. Part II §16 requires the control-plane / execution-plane separation
|
|
regardless of distribution.
|
|
2. A slow OCCT import must never land in the request path for a page that only
|
|
draws a cross-section.
|
|
3. Test-suite speed — the difference between running 123 fixtures on every save
|
|
and only before commit.
|
|
|
|
**CadQuery is a default-installed dependency.** Revision 1 restricted it as a
|
|
defence against YunoHost's "resource-hungry" criterion; that was overcalibrated.
|
|
The criterion reads "compared to their features" and is aimed at marginal apps.
|
|
The boundary stays; the apology is gone.
|
|
|
|
**Verified 2026-09-13** — `cadquery` and `OCP` both import in CT 100. This
|
|
document was correct; `HANDOFF.md` §5 had said the opposite for three weeks, and
|
|
DIV-005 was opened against this paragraph rather than against that one. Closed,
|
|
and recorded there as having been written backwards.
|
|
|
|
### 1.2 YunoHost and Docker are parallel targets, not sequential
|
|
|
|
YunoHost apps install natively — apt, venv, systemd, nginx — and the project
|
|
does not want Docker inside YunoHost. Neither blocks the other, and neither
|
|
should be built "in order to" reach the other.
|
|
|
|
### 1.3 Every instance mirrors production topology, not production exposure
|
|
|
|
An instance runs **two containers**: an application container that never
|
|
terminates TLS, and a reverse proxy that does. A single container serving
|
|
directly would never exercise the `X-Forwarded-Proto` path.
|
|
|
|
Staging is not publicly reachable and does not use the public FQDN. That forces
|
|
the promotion path to be exercised rather than assumed.
|
|
|
|
**DIVERGENCE (DIV-003)** — the `srv-b` instance has been publicly reachable at
|
|
`dev.mechcomp.kane-il.us` since 2026-09-11. The requirement stands and the
|
|
instance does not meet it. §5.2 carries the same divergence with its reasoning.
|
|
|
|
### 1.4 Directory layout mirrors YunoHost conventions
|
|
|
|
`install_dir`, `data_dir`, a dedicated system user, a port treated as data. The
|
|
eventual `manifest.toml` resource block then describes what already exists.
|
|
|
|
### 1.5 Configuration comes from the environment, never the filesystem
|
|
|
|
No path, port, URL or secret is hardcoded or discovered by convention. One env
|
|
file; the same variables become `ENV` in a Dockerfile and `ynh_add_config`
|
|
substitutions in a package.
|
|
|
|
The check: moving an instance to a different hostname is a one-variable change.
|
|
|
|
---
|
|
|
|
## 2. Host requirements
|
|
|
|
**REQ** — Proxmox VE 8.x, Debian 12 base.
|
|
|
|
**REQ** — Two bridges (§3). **REQ** — Directory storage for backups and
|
|
templates; LVM-thin or equivalent for container volumes.
|
|
|
|
**PREF** — At least 12 CPU threads and 24 GiB RAM if the instance is also a
|
|
development host. The geometry solvers are single-threaded, so single-thread
|
|
performance governs fixture regeneration while thread count governs test
|
|
parallelism. An older CPU without AVX is acceptable; all compiled wheels target
|
|
an SSE2 baseline.
|
|
|
|
**REQ** — Time zone and NTP configured; clock synchronised.
|
|
|
|
**INSTANCE** — Hostname, addressing, storage pool names, template path.
|
|
|
|
---
|
|
|
|
## 3. Network
|
|
|
|
### 3.1 Two bridges
|
|
|
|
**REQ** — A **management bridge** with a physical port, carrying the host's own
|
|
address. The host uses it for egress and administration.
|
|
|
|
**REQ** — A **service bridge with no physical port**, carrying the host at
|
|
`.1`. This is the only network the containers sit on.
|
|
|
|
A portless bridge needs no cable, no switch configuration and no spare NIC. It
|
|
exists so the containers cannot be reached from the physical network at all.
|
|
|
|
### 3.2 Containers have exactly one interface
|
|
|
|
**PROVEN (F-017)** — Each container has a single interface on the **service
|
|
bridge only**, with the host as gateway. No management-network interface.
|
|
|
|
Revision 4 gave each container an address on both bridges, on the unstated
|
|
assumption that a workstation on the physical LAN would browse the instance.
|
|
That assumption was wrong and put both containers on the operator's home
|
|
network.
|
|
|
|
**PROVEN (F-021)** — After any interface change, the guest's
|
|
`/etc/network/interfaces` must be inspected. `pct set --delete netN` removes the
|
|
LXC interface but leaves the stanza in the guest, and `ifup -a` then fails at
|
|
boot on a device that no longer exists. See §15 gate 1 for the assertion that
|
|
catches this.
|
|
|
|
### 3.3 Isolation is routing, not interfaces
|
|
|
|
**PROVEN (F-018)** — Removing an interface removes an address, not a route. With
|
|
forwarding enabled and a masquerade for internet access, containers reach the
|
|
physical LAN through the host, translated to its address.
|
|
|
|
**REQ** — These rules, in this order:
|
|
|
|
```
|
|
nat POSTROUTING
|
|
-s <SVC_NET> -d <LAN_NET> -j RETURN # before any masquerade
|
|
-s <SVC_NET> -o <WG_IF> -j MASQUERADE
|
|
-s <SVC_NET> -o <MGMT> -j MASQUERADE
|
|
|
|
filter FORWARD
|
|
-s <SVC_NET> -d <WG_NET> -p tcp
|
|
-m multiport --dports 25,465,587 -j DROP # containers do not send mail
|
|
-s <SVC_NET> -d <SVC_NET> -j ACCEPT
|
|
-s <SVC_NET> -d <LAN_NET> -j DROP
|
|
```
|
|
|
|
**PROVEN (F-025)** — The SMTP rule is not optional where the host masquerades
|
|
containers onto a network carrying a mail relay. Without it the containers
|
|
inherit whatever relay authority the host's source address holds. The host's own
|
|
alerting is unaffected: it originates mail locally, so it takes OUTPUT and never
|
|
enters FORWARD. Prove that rather than assuming it.
|
|
|
|
**REQ** — Rule order is load-bearing. `RETURN` must precede both masquerades.
|
|
Rules must be persisted, and the persisted set verified to match the live set.
|
|
|
|
**REQ** — The isolation must be asserted **negatively**: the LAN gateway is
|
|
unreachable from both containers. Asserting that the interface is gone is not
|
|
sufficient — that is exactly the mistake F-018 records.
|
|
|
|
**Expected and correct:** containers *can* reach the host itself. A container
|
|
addressing its own gateway takes the INPUT path and never enters FORWARD. This
|
|
is required — the host is their router and their SSH entry point — and is not a
|
|
leak.
|
|
|
|
### 3.4 Names
|
|
|
|
**REQ** — Names resolve through `/etc/hosts` on the host and both containers.
|
|
|
|
**REQ — do not stand up an internal DNS server.** Two containers and one host do
|
|
not justify one, and it would create a second source of truth for names that
|
|
production will not share.
|
|
|
|
**REQ** — The service FQDN names **the proxy**, which has exactly one address.
|
|
Guest hostnames are separate entries.
|
|
|
|
```
|
|
<SERVICE_FQDN> -> <proxy service address>
|
|
<app hostname> -> <app service address>
|
|
<proxy hostname> -> <proxy service address>
|
|
```
|
|
|
|
**PROVEN (F-020)** — This value tracked topology through three states before
|
|
settling. The reasoning matters more than the value.
|
|
|
|
### 3.5 No DHCP
|
|
|
|
**REQ** — Static addressing. Two containers on a portless bridge is the entire
|
|
address space; a DHCP server would add a daemon, a lease database and a failure
|
|
mode in exchange for nothing.
|
|
|
|
### 3.6 Firewall
|
|
|
|
**ASSUMED** — The Proxmox firewall may remain disabled where the service bridge
|
|
is portless and the application has no listener outside it. **REQ** — Production
|
|
must revisit this; see §15 gate 5.
|
|
|
|
---
|
|
|
|
## 4. Containers
|
|
|
|
### 4.1 Roles and features
|
|
|
|
| | Application | Proxy |
|
|
|---|---|---|
|
|
| Role | application, worker | reverse proxy, TLS |
|
|
| Unprivileged | yes | yes |
|
|
| Features | `nesting=1,keyctl=1` | `nesting=1` |
|
|
|
|
**PROVEN (F-003)** — **Every Debian 12 unprivileged container requires
|
|
`nesting=1`**, including one running nothing but nginx. Without it,
|
|
`systemd-logind`, `systemd-networkd`, `systemd-timedated` and
|
|
`systemd-networkd.socket` fail with `226/NAMESPACE`. Revision 4 said the proxy
|
|
needed no features.
|
|
|
|
`keyctl=1` is required only where Docker runs. Tested separately rather than
|
|
copied across; it is genuinely unnecessary on the proxy.
|
|
|
|
### 4.2 Sizing
|
|
|
|
**INSTANCE.** Sizing is a host capacity decision, not a specification value.
|
|
|
|
**REQ** — The application container's data volume is a **real mount point** with
|
|
`backup=0`. Backup of application data is a separate tier (§10), and including a
|
|
large data volume in a container snapshot is how a backup store fills.
|
|
|
|
**REQ** — Do not over-commit a thin pool. Thin-pool exhaustion is an ugly
|
|
failure mode.
|
|
|
|
**PROVEN (F-026)** — Every container that must survive a host restart carries
|
|
`onboot: 1`. This is not the default. It is also invisible to `pct reboot`,
|
|
which restarts a guest without exercising host-boot autostart at all — so a
|
|
container proven to survive `pct reboot` is **not** thereby proven to survive a
|
|
host reboot.
|
|
|
|
### 4.3 Base OS
|
|
|
|
**REQ** — Debian 12 bookworm, both containers. It matches YunoHost 12 stable and
|
|
ships Python 3.11, which has the most reliable OCP/CadQuery wheel coverage.
|
|
|
|
**REQ (F-005)** — `apt full-upgrade` immediately after first boot, before
|
|
anything else is installed. Templates are not current, and discovering a
|
|
52-package backlog midway through provisioning is avoidable.
|
|
|
|
**REQ (F-004)** — Locale must be **generated**, not merely configured. Install
|
|
`locales`, enable the locale, run `locale-gen`, then `update-locale`. Setting
|
|
`LANG` alone leaves the system with only `C` and `POSIX`, silently, until
|
|
something depends on collation.
|
|
|
|
**REQ** — Time zone matching the host.
|
|
|
|
---
|
|
|
|
## 5. Names and TLS
|
|
|
|
### 5.1 Three names, never conflated
|
|
|
|
| Name | Scope |
|
|
|---|---|
|
|
| Service FQDN | What users type. Differs per instance. |
|
|
| Infrastructure token | System user, paths, units, YunoHost app id. **Same everywhere.** |
|
|
| Project name | Documents, README, catalog entry |
|
|
|
|
YunoHost app ids accept lowercase alphanumerics and underscores only, so a
|
|
hyphenated FQDN cannot serve as the token.
|
|
|
|
### 5.2 Non-production instances
|
|
|
|
**REQ** — An internal service FQDN. **REQ** — No public DNS record and no
|
|
publicly-trusted certificate: it consumes rate limit against a name production
|
|
needs clean, and puts a development host on the internet.
|
|
|
|
**DIVERGENCE (DIV-003)** — all three are unmet on `srv-b` as of 2026-09-11.
|
|
Public `A` and `AAAA` records exist and a Let's Encrypt certificate terminates on
|
|
the hub. The reasoning above is a live claim nobody has rebutted, and half of it
|
|
is already visible: that certificate expires 2026-12-10 and **no renewal has
|
|
been observed to succeed for this name**.
|
|
|
|
Whether this instance is still *non-production* is itself unsettled.
|
|
`WORK-ORDER-004` §3 records that `dev` abbreviates *Mechanical Compiler
|
|
Developers*, that the name is production, and that it appears on printed
|
|
material. If that reading holds, §5.2 never applied to it and §5.3 does — which
|
|
would make this a misfiled instance rather than a violated requirement. That is
|
|
a decision, not a correction.
|
|
|
|
**PROVEN** — A locally generated CA is sufficient and is the fallback whenever a
|
|
site CA has no discoverable issuance path. The point of the TLS step is
|
|
exercising termination and header forwarding, not the trust chain. Replacing the
|
|
leaf later is two file copies and a reload.
|
|
|
|
**REQ** — The CA root is installed into the trust store of the host, both
|
|
containers, and any client used to browse the instance.
|
|
|
|
**REQ** — Record the leaf's expiry somewhere that gets read. Nothing renews a
|
|
hand-made certificate.
|
|
|
|
### 5.3 Production
|
|
|
|
| | Value |
|
|
|---|---|
|
|
| FQDN | `mechanical-compiler.manufacturing.kane-il.us` |
|
|
| Zone | Labels within `kane-il.us`. **Not delegated.** No new DS record. |
|
|
| Certificate | Let's Encrypt, **per-name, no wildcard** |
|
|
| Challenge | **ASSUMED** HTTP-01. Match existing practice if it is DNS-01; never run both. |
|
|
|
|
`manufacturing.kane-il.us` is a namespace, not a service. `kane-il.us` stays
|
|
reserved for cross-cutting infrastructure and must not acquire project records.
|
|
|
|
The leaf names the service, not the discipline, because the parent already
|
|
carries the discipline and the namespace must hold siblings:
|
|
|
|
```
|
|
mechanical-compiler.manufacturing.kane-il.us this application
|
|
field-metrology.manufacturing.kane-il.us Utility One, later
|
|
stock-processing.manufacturing.kane-il.us Utility Two, later
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Reverse proxy
|
|
|
|
**REQ** — nginx in the proxy container. Terminates TLS, proxies to the
|
|
application container's service address.
|
|
|
|
**REQ** — Debian's `default` site removed. Only the project vhost enabled.
|
|
|
|
**REQ** — `X-Forwarded-Proto` is mandatory. Getting this wrong is what broke
|
|
Hubzilla sessions.
|
|
|
|
**REQ** — `default_server` declared explicitly on both TLS listeners. With one
|
|
vhost the implicit default is correct today; the moment a second is added, which
|
|
block catches unmatched requests becomes a function of file ordering. This is
|
|
independent hardening and **does not close F-012**.
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl default_server;
|
|
listen [::]:443 ssl default_server;
|
|
server_name <SERVICE_FQDN>;
|
|
|
|
ssl_certificate <cert>;
|
|
ssl_certificate_key <key>;
|
|
|
|
location / {
|
|
proxy_pass http://<APP_SVC_ADDR>:<PORT>;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 300s;
|
|
client_max_body_size 8m;
|
|
}
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name <SERVICE_FQDN>;
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
```
|
|
|
|
**REQ** — This vhost differs between instances in exactly two places:
|
|
`server_name` and the certificate paths. Any further divergence means the
|
|
non-production instance has stopped testing production.
|
|
|
|
**PREF** — `proxy_read_timeout 300s`. Compile jobs are asynchronous, but a cold
|
|
synchronous render can take tens of seconds and the 60s default leaves no
|
|
headroom.
|
|
|
|
---
|
|
|
|
## 7. Users, directories, permissions
|
|
|
|
**REQ** — Dedicated system user, no login shell:
|
|
|
|
```
|
|
user:group mechcomp:mechcomp
|
|
shell /usr/sbin/nologin
|
|
home <install_dir>
|
|
```
|
|
|
|
**REQ (F-016 context)** — The service user's home is the install directory, not
|
|
a nonexistent `/home/<user>`. Tooling run as that user writes to `$HOME`;
|
|
`ProtectHome=true` makes it moot at runtime but not during provisioning.
|
|
|
|
**REQ** — Layout, matching what a YunoHost package would provision:
|
|
|
|
| Path | Owner | Mode | YunoHost equivalent |
|
|
|---|---|---|---|
|
|
| `/var/www/mechcomp` | `mechcomp:mechcomp` | `0750` | `install_dir` |
|
|
| `/var/lib/mechcomp` | `mechcomp:mechcomp` | `0750` | `data_dir` |
|
|
| `/var/log/mechcomp` | `mechcomp:mechcomp` | `0750` | — |
|
|
| `/etc/mechcomp/mechcomp.env` | `root:mechcomp` | `0640` | `ynh_add_config` |
|
|
|
|
```
|
|
/var/lib/mechcomp/
|
|
├── db/ SQLite database
|
|
├── artifacts/ generated SVG, STL, STEP, manifests
|
|
├── fixtures/ frozen acceptance oracles, read-mostly
|
|
└── cache/ safe to delete at any time
|
|
```
|
|
|
|
The `cache/` guarantee matters: anything not reproducible from `db/` plus
|
|
`artifacts/` must not live there.
|
|
|
|
**REQ (F-007)** — **Never `chown -R` a mount point root.** `data_dir` is a
|
|
filesystem root containing `lost+found`, owned by `nobody:nogroup` at `0700`,
|
|
created by `mkfs` and not ours to manage. Enumerate the application directories
|
|
explicitly.
|
|
|
|
**REQ (F-008)** — All repository operations run as the owning service user. Do
|
|
not add a root `safe.directory` exception — it would mask every future instance
|
|
of the same mistake.
|
|
|
|
**REQ** — The admin account exists in both containers with an authorized key.
|
|
**REQ (F-014)** — If the admin is placed in the service group, that group must be
|
|
created explicitly in the proxy container, where no service user creates it as a
|
|
side effect.
|
|
|
|
---
|
|
|
|
## 8. Software
|
|
|
|
### 8.1 Application container
|
|
|
|
**REQ**
|
|
|
|
```
|
|
python3 python3-venv python3-dev
|
|
build-essential pkg-config
|
|
git curl ca-certificates
|
|
libgeos-dev
|
|
sqlite3
|
|
```
|
|
|
|
**REQ (F-015)** — The proxy container needs its own minimal toolset, at least
|
|
`curl` and `ca-certificates`. Do not assume packages installed in one container
|
|
exist in the other.
|
|
|
|
**REQ — do not install** `openscad`, `openscad-nightly`, or any Qt or X11
|
|
package. The running application never invokes OpenSCAD, and §8.2 confines it.
|
|
|
|
### 8.2 The pinned reference toolchain
|
|
|
|
**REQ** — One Docker image with the exact toolchain the fixture oracle was
|
|
frozen against, and nothing else:
|
|
|
|
```dockerfile
|
|
FROM debian:12-slim
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
openscad git ca-certificates \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
RUN git clone https://github.com/BelfrySCAD/BOSL2.git /BOSL2 \
|
|
&& git -C /BOSL2 checkout 92d697c2856de2fed93a33e858068589cefc2898
|
|
```
|
|
|
|
Tag `mechcomp/reference-toolchain:8.0.0`. Its only job is to run
|
|
`make_fixtures.py` and prove the oracle reproduces. The container has no
|
|
OpenSCAD, so its version cannot drift.
|
|
|
|
**Acceptance** — must reproduce exactly:
|
|
|
|
```
|
|
sha256 strap-beam-fixtures-8.0.0.json
|
|
= ddd0f1548379205dd0c652ec07285b0dae331e52ff0a0437005dfc6cddcc2cb2
|
|
```
|
|
|
|
A drifting oracle is worse than no oracle.
|
|
|
|
### 8.3 Python
|
|
|
|
**REQ** — A virtualenv inside `install_dir`. Debian 12 enforces PEP 668; do not
|
|
use `--break-system-packages`.
|
|
|
|
**REQ** — `venv/` is in `.gitignore`. It is a legitimate artifact in
|
|
`install_dir` and simply should not be tracked.
|
|
|
|
**REQ** — Pinned, hash-checked dependencies in two files, both installed:
|
|
|
|
| File | Contents |
|
|
|---|---|
|
|
| `requirements-base.txt` | shapely, fastapi, uvicorn, pydantic, sqlalchemy, jinja2, pytest, pytest-xdist |
|
|
| `requirements-cad.txt` | cadquery / build123d |
|
|
|
|
**DIVERGENCE (DIV-002)** — `fastapi`, `uvicorn`, `pydantic`, `sqlalchemy` and
|
|
`jinja2` are installed on every instance and imported by nothing in the composer.
|
|
They describe the architecture §9 specifies and nobody built. The manifest is
|
|
not wrong about what it installs; it is a faithful description of an unbuilt
|
|
design.
|
|
|
|
CI runs the suite twice — with both, then with base alone — and the second run
|
|
must pass. That is the mechanism keeping §1.1 honest.
|
|
|
|
---
|
|
|
|
## 9. Services
|
|
|
|
**REQ** — Two units, split along the control-plane / execution-plane boundary:
|
|
|
|
| Unit | Role |
|
|
|---|---|
|
|
| `mechcomp.service` | FastAPI/uvicorn. Serves the catalogue, accepts jobs, returns cached artifacts. **Never runs geometry.** |
|
|
| `mechcomp-worker.service` | Consumes the queue, runs generators, writes artifacts. |
|
|
|
|
**DIVERGENCE (DIV-002)** — neither unit exists as specified. `mechcomp.service`
|
|
runs a standard-library `http.server` and builds geometry synchronously in the
|
|
request thread, so the control plane *does* run geometry.
|
|
`mechcomp-worker.service` does not exist; `src/mechcomp/worker/` is a 36-byte
|
|
stub.
|
|
|
|
The requirement stands. §1.1 reason 1 calls this separation a requirement
|
|
**regardless of distribution**, and reason 2 gives its purpose: a slow import
|
|
must never land in the request path for a page that only draws a cross-section.
|
|
Geometry itself now does, on a world-reachable service with no authentication.
|
|
|
|
**REQ** — Both:
|
|
|
|
```ini
|
|
[Service]
|
|
User=mechcomp
|
|
Group=mechcomp
|
|
EnvironmentFile=/etc/mechcomp/mechcomp.env
|
|
WorkingDirectory=/var/www/mechcomp
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ReadWritePaths=/var/lib/mechcomp /var/log/mechcomp
|
|
ProtectKernelTunables=true
|
|
ProtectControlGroups=true
|
|
RestrictSUIDSGID=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
**REQ** — Worker only: a `MemoryMax` bound, a `CPUQuota`, `TimeoutStopSec=30`.
|
|
A runaway geometry job must become a clean OOM-kill of one worker, not a dead
|
|
container.
|
|
|
|
**REQ** — Logging to journald. No application-managed rotation.
|
|
|
|
**REQ** — Job queue is SQLite-backed and in-process. No Redis, no Celery, no
|
|
RabbitMQ. A table with a status column and a claim query is correct at this
|
|
scale and adds no services to either packaging target.
|
|
|
|
**DIVERGENCE (DIV-002)** — no queue and no database exist. Nothing has ever
|
|
written to `MECHCOMP_DATA_DIR`.
|
|
|
|
**REQ (F-019)** — **Anything a proof depends on must be supervised.** This
|
|
includes temporary scaffolding. A placeholder backend used to prove the proxy
|
|
chain before the application exists is a systemd unit, obviously named and
|
|
obviously temporary — otherwise the proof expires at the next reboot and the
|
|
following session diagnoses a proxy fault that does not exist.
|
|
|
|
**REQ (F-011)** — Do not use `/tmp` for state shared across users.
|
|
`fs.protected_regular` makes that fail in ways that look like permission bugs.
|
|
Moot under `PrivateTmp=true`.
|
|
|
|
---
|
|
|
|
## 10. Backup
|
|
|
|
**DEFERRED — 2026-08-16, operator decision.** The backup strategy may change.
|
|
This section is retained as a design sketch and is **not authoritative**; no
|
|
acceptance gate depends on it until a strategy is chosen.
|
|
|
|
The sketch, for whoever revisits it:
|
|
|
|
| Tier | What | Retention |
|
|
|---|---|---|
|
|
| Routine | container snapshot, **rootfs only** | measured, not guessed |
|
|
| Application | tar stream of `db/`, `artifacts/`, `fixtures/`, and the env file | short, rolling |
|
|
| Gold | routine + application + manifest, on removable encrypted media | indefinite, offline |
|
|
|
|
Three constraints that survive any strategy change:
|
|
|
|
1. **Data volumes carry `backup=0`.** A container snapshot store on the root
|
|
filesystem is how a Proxmox host fills up.
|
|
2. **Pull, never push.** The container has no access to any backup destination.
|
|
The alternative hands a network-facing container write access to the last
|
|
line of defence, and it avoids idmap ownership problems.
|
|
3. **Encrypt removable media.** The backup set includes the env file, which
|
|
includes the secret key. Media that travels is media that gets lost.
|
|
|
|
**Standing recommendation regardless of strategy:** take one manual snapshot of
|
|
both containers before any significant change. It presupposes nothing and yields
|
|
the archive size that retention decisions need.
|
|
|
|
---
|
|
|
|
## 11. Configuration contract
|
|
|
|
**REQ** — One env file configures the application:
|
|
|
|
```bash
|
|
MECHCOMP_ENV= # staging | production
|
|
MECHCOMP_BIND= # single address — see below
|
|
MECHCOMP_PORT=
|
|
MECHCOMP_BASE_URL=
|
|
MECHCOMP_MAX_EXPORT_MM= # bound on the exported sweep length
|
|
MECHCOMP_DATA_DIR=/var/lib/mechcomp
|
|
MECHCOMP_LOG_LEVEL=info
|
|
MECHCOMP_DB_URL=sqlite:////var/lib/mechcomp/db/mechcomp.sqlite3
|
|
MECHCOMP_WORKER_CONCURRENCY=
|
|
MECHCOMP_CAD_BACKEND=none # none | cadquery
|
|
MECHCOMP_ARTIFACT_RETENTION_DAYS=30
|
|
MECHCOMP_SECRET_KEY= # generated on first provision, never committed
|
|
```
|
|
|
|
**DIVERGENCE (DIV-004)** — the application reads four of these: `MECHCOMP_BIND`,
|
|
`MECHCOMP_PORT`, `MECHCOMP_MAX_EXPORT_MM` and `MECHCOMP_BASE_URL`. The other
|
|
eight are read by nothing. Most belong to the architecture DIV-002 records as
|
|
unbuilt, so this is largely the same divergence seen from the configuration
|
|
side.
|
|
|
|
`MECHCOMP_MAX_EXPORT_MM` was added to this list on 2026-09-13. It had been read
|
|
by the code since `cdde394` and declared nowhere — the divergence running the
|
|
other way, and the harder one to notice, because a missing key looks like
|
|
nothing at all.
|
|
|
|
`MECHCOMP_SECRET_KEY` deserves its own note: a secret that nothing reads is
|
|
protecting nothing, and its presence implies a session or signing mechanism that
|
|
does not exist.
|
|
|
|
**REQ** — `MECHCOMP_BIND` is a **single address**. Loopback is not additionally
|
|
bound.
|
|
|
|
Revision 4 said "the service-network address, plus `127.0.0.1`," which a scalar
|
|
cannot express. Resolved toward one address: `uvicorn` takes one `--host`, and
|
|
two sockets would mean either `0.0.0.0` — defeating the isolation — or
|
|
multi-socket setup for no benefit. Local checks inside the container can use the
|
|
service address. The scalar also generalises correctly: under YunoHost, where
|
|
nginx and the application share a host, the same variable takes `127.0.0.1`.
|
|
|
|
**REQ** — The application fails loudly at startup if a required variable is
|
|
missing, and never falls back to a compiled-in default path.
|
|
|
|
**REQ** — The secret is generated on first provision and never overwritten by a
|
|
re-run. It is never committed and never logged.
|
|
|
|
Promotion changes `MECHCOMP_ENV`, `MECHCOMP_BIND` and `MECHCOMP_BASE_URL`.
|
|
Nothing else.
|
|
|
|
---
|
|
|
|
## 12. Mail and monitoring
|
|
|
|
**REQ** — **SMTP 250 from a relay and an empty local queue prove handoff, not
|
|
delivery.** Alerting acceptance requires demonstrated end-to-end receipt, twice,
|
|
with full headers captured. This is the whole content of F-023: a message can be
|
|
accepted by every hop and still be rejected at the last one.
|
|
|
|
**REQ** — Diagnose relay problems with **RCPT-only probes, over every address
|
|
family, before and after any change**, with no message body sent. A `554`
|
|
before and a `250` after proves the change caused the fix rather than
|
|
coinciding with it.
|
|
|
|
**REQ (F-024)** — On a Proxmox host, inspect mail logs with
|
|
`journalctl -u postfix@-`. There is no `rsyslog` and no `/var/log/mail.log`.
|
|
Automation that greps a logfile path finds nothing silently, which is worse than
|
|
failing.
|
|
|
|
**PROVEN** — Disk monitoring must be configured **explicitly per device** where
|
|
the controller does not present members to automatic scanning. An active
|
|
`smartmontools.service` monitoring zero devices is not monitoring, and is easy
|
|
to mistake for success. Acceptance is a **delivered** test alert, not an active
|
|
unit.
|
|
|
|
Behind an HP Smart Array controller the working pattern is:
|
|
|
|
```
|
|
DEFAULT -a -m root -M exec /usr/share/smartmontools/smartd-runner
|
|
/dev/sda -d cciss,0
|
|
/dev/sda -d cciss,1
|
|
/dev/sda -d cciss,2
|
|
/dev/sda -d cciss,3
|
|
```
|
|
|
|
`-M test` proves the alert path — one message per monitored device — and must be
|
|
**reverted afterwards**. Left in place it produces an alert storm on every
|
|
restart, which trains everyone to ignore disk alerts. That is worse than no
|
|
monitoring.
|
|
|
|
**REQ (F-028)** — Query `smartmontools.service`, not `smartd.service`. The
|
|
latter is an alias and owns no journal; `journalctl -u smartd` returns nothing
|
|
on a host where monitoring is working perfectly.
|
|
|
|
**REQ** — Record each member's **serial number** at configuration time. That is
|
|
the only thing that maps a future alert to a physical drive in a caddy.
|
|
|
|
### 12.1 Downstream infrastructure is out of bounds
|
|
|
|
**REQ** — An instance may configure its own relay client. It may not alter the
|
|
mail infrastructure it relays through. Where a downstream change is genuinely
|
|
required, it is escalated with evidence, not made locally.
|
|
|
|
Two standing constraints apply to this estate:
|
|
|
|
| Host | Constraint |
|
|
|---|---|
|
|
| The final mailbox host | Runs YunoHost. Its mail configuration must not be altered — which is the reason a separate relay exists. |
|
|
| The relay MTA | Uses DANE. Its TLS and certificate configuration must not be broken. Client-trust changes such as `mynetworks` do not interact with DANE; certificate changes do. |
|
|
|
|
**REQ (F-025)** — Record what a trust change **grants**, not only what it
|
|
repairs. `permit_mynetworks` is destination-agnostic: authorising a client to
|
|
fix one rejected recipient authorises it for every destination. Where an
|
|
instance's containers have no reason to originate mail, block SMTP egress at the
|
|
instance boundary rather than relying on downstream policy to refuse it.
|
|
|
|
## 13. Instance parameters
|
|
|
|
**INSTANCE** — Every value below is supplied per instance and recorded in that
|
|
instance's state file, not here.
|
|
|
|
```
|
|
host hostname, storage pools, bridges, template
|
|
management network host address, gateway
|
|
service network network, host address, container addresses
|
|
wireguard interface, address, allowed networks
|
|
containers ids, hostnames, sizing
|
|
service FQDN per instance
|
|
TLS CA and leaf source
|
|
admin username, authorized key
|
|
mail relay endpoint, root alias
|
|
```
|
|
|
|
**REQ** — These live in exactly one file per instance, sourced by every
|
|
provisioning step. No literals scattered through scripts. The file is not
|
|
committed; an example with empty values is.
|
|
|
|
---
|
|
|
|
## 14. Constraints the application code will follow
|
|
|
|
1. No hardcoded absolute paths. Everything derives from `MECHCOMP_DATA_DIR`.
|
|
2. No writes outside `MECHCOMP_DATA_DIR`. Enforced by `ProtectSystem=strict`.
|
|
3. No dependency on systemd from application code. Signal handling only.
|
|
4. The app listens on one TCP address, never `0.0.0.0`.
|
|
5. No OpenSCAD, Qt or X11 dependency in the running application.
|
|
6. The 3D backend is reached only through an interface in `src/mechcomp/cad/`.
|
|
No other module imports CadQuery or OCP directly.
|
|
7. Schema migrations are explicit and forward-only.
|
|
8. Artifacts are addressed by content hash of **inputs** — parameter set plus
|
|
generator revision — never by hash of output bytes. Mesh output is not
|
|
reproducible across toolchain versions; input hashing keeps a qualification
|
|
valid across an upgrade that did not change the geometry.
|
|
9. Every generated artifact carries the generator revision that produced it.
|
|
10. The test suite passes with `requirements-cad.txt` uninstalled.
|
|
11. AGPL-3.0 §13 requires network users be offered the source. The web tier
|
|
carries a visible source link to the repository. A licence obligation.
|
|
**DIVERGENCE (DIV-001) — not met.** The served page carries no such link,
|
|
and the deployment has been public since 2026-09-11. This is the only unmet
|
|
requirement in this document whose consequence falls outside the project,
|
|
and the smallest to correct.
|
|
|
|
### 14.1 Test validity
|
|
|
|
**REQ (F-027)** — A test must be able to distinguish failure of the **tool**
|
|
from failure of the **thing under test**.
|
|
|
|
```bash
|
|
# WRONG — any failure of the wrapper reads as a pass
|
|
run_in_guest "$c" 'connect to X' && echo "reachable — WRONG" || echo "blocked — correct"
|
|
|
|
# RIGHT — establish that the test could have observed the positive case
|
|
if [ "$(guest_state "$c")" != "running" ]; then
|
|
echo "CT $c NOT RUNNING — TEST INVALID"
|
|
elif run_in_guest "$c" 'connect to X'; then
|
|
echo "reachable — WRONG"
|
|
else
|
|
echo "blocked — correct"
|
|
fi
|
|
```
|
|
|
|
This applies to **every negative assertion** — "X is unreachable", "Y is
|
|
absent", "Z is refused". A test whose failure mode is indistinguishable from
|
|
success is worse than no test, because it manufactures confidence. In WO-003 the
|
|
wrong form reported a pass against containers that were not running.
|
|
|
|
---
|
|
|
|
## 14a. Conformance
|
|
|
|
**REQ** — An instance carries an executable **baseline check**: read-only,
|
|
runnable at any time, exiting non-zero when any container diverges from the
|
|
standard.
|
|
|
|
**REQ** — **A property not checked by it is not part of the standard.** This is
|
|
what makes conformance terminate. Without it, divergence is discovered one
|
|
property at a time whenever something behaves oddly, and the process never
|
|
finishes because nothing states what "the same" means. A property that must be
|
|
uniform belongs in the check, not in an operator's memory.
|
|
|
|
**REQ (F-031)** — The check must examine **the thing being standardised**. An
|
|
assertion derived from a related observation is not a check. Postfix
|
|
configuration on the host says what the host does; it says nothing about the
|
|
containers, and a standard asserted that way was wrong on two of three.
|
|
|
|
**REQ** — Each failure names the failure-log entry that established the
|
|
requirement, so the reason survives the person who found it.
|
|
|
|
**REQ (F-032)** — Host-level requirements belong to the host, not to whichever
|
|
project discovered them first. Where several projects share a host, the check
|
|
is host-level and covers every container regardless of owner. Two projects on
|
|
this host independently rediscovered the same two defects before this was done.
|
|
|
|
### Mail
|
|
|
|
**REQ** — **Containers do not originate mail.** Only the host does, and only its
|
|
own operational alerts. A container with a mail agent installed but SMTP egress
|
|
blocked is worse than either alone: it queues indefinitely and delivers nothing,
|
|
while appearing configured.
|
|
|
|
Application mail — participant notification and similar — is a separate design
|
|
problem with its own delivery and inbound requirements. It is not solved by
|
|
leaving a partially configured agent in a container.
|
|
|
|
---
|
|
|
|
## 15. Acceptance gates
|
|
|
|
**REQ** — Five independent gates. Revision 4 ran them as one list, which meant
|
|
an honest infrastructure build appeared to fail because application code did not
|
|
exist yet.
|
|
|
|
### Gate 1 — Infrastructure
|
|
|
|
Host and both containers `running` with **zero failed units**, asserted **after a
|
|
reboot**. Locale generated. Data volume is a real mount. Ownership correct with
|
|
`lost+found` untouched. Forbidden packages absent. Container egress works;
|
|
**the LAN gateway is unreachable from both containers**; the host remains
|
|
reachable from them; the management console remains reachable from the LAN. NAT
|
|
and FORWARD rules present live and persisted, in order. Proxy serves the service
|
|
FQDN over TLS without an insecure bypass, redirects HTTP, and
|
|
**`X-Forwarded-Proto: https` is observed at the backend**. Any supervised
|
|
scaffolding survives a reboot.
|
|
|
|
**PROVEN (F-021)** — the reboot is not optional. Connectivity can be perfect
|
|
while boot is degraded.
|
|
|
|
**PROVEN (F-026)** — it must be a **host** reboot, and every guest must return
|
|
automatically and report `running`. A guest reboot does not exercise autostart.
|
|
|
|
**PROVEN (F-027)** — every negative assertion in this gate must satisfy §14.1
|
|
before its result means anything.
|
|
|
|
### Gate 2 — Operational services
|
|
|
|
Mail delivered **end to end and received twice**, with full headers captured, an
|
|
empty queue and no deferred entries. Disk monitoring reporting a **non-zero
|
|
device count** and a delivered test alert. Container SMTP egress blocked where
|
|
the containers have no reason to originate mail. Not gated on gate 1.
|
|
|
|
### Gate 3 — Application runtime
|
|
|
|
Dependencies installed from committed manifests. Real service and worker units
|
|
active. Reference toolchain image reproduces `ddd0f154…`. Scaffolding removed.
|
|
|
|
**Status 2026-09-13 — the gate does not pass.** Dependencies: met. Toolchain
|
|
image: met, present in CT 100 and the oracle reproduced inside it on
|
|
2026-08-19. Scaffolding: met, `mechcomp-placeholder.service` retired 2026-09-11
|
|
with the unit file left on disk disabled as the rollback path.
|
|
**Worker unit: not met (DIV-002).**
|
|
|
|
### Gate 4 — Backup
|
|
|
|
Undefined pending a strategy decision.
|
|
|
|
**REQ** — **Conformance is a precondition, not a companion.** Backup work does
|
|
not begin until the baseline check (§14a) exits zero, and the check forms part
|
|
of restore verification.
|
|
|
|
A backup of an unverified configuration does not preserve a system; it
|
|
preserves a state of confusion, and a restore returns that state faithfully.
|
|
This was not hypothetical: a backup taken before 2026-08-18 would have restored
|
|
two containers that queue mail forever (F-031).
|
|
|
|
### Gate 5 — Production automation
|
|
|
|
Idempotent scripts derived from a proven manual procedure. Firewall posture
|
|
revisited. Publicly-trusted certificate issued and renewing.
|
|
|
|
---
|
|
|
|
## 16. Assumptions
|
|
|
|
| § | Assumption | If wrong |
|
|
|---|---|---|
|
|
| 3.6 | Firewall may stay disabled on a non-production instance | Enable per-container; production revisits regardless |
|
|
| 5.3 | Production uses HTTP-01 | Switch to DNS-01 if that is existing practice; never both |
|
|
| 11 | `MECHCOMP_BIND` is a single address | Revisit only if a real requirement for two sockets appears |
|
|
| 15 | Five gates are the right split | Merge or split further as evidence warrants |
|
|
|
|
---
|
|
|
|
## 17. Provenance disclosure
|
|
|
|
This project's documents, code and roadmap are LLM-generated under human
|
|
direction. Stated plainly here, in the README, and in any catalog submission.
|
|
|
|
**The YunoHost policy is a quality bar with a disclosure requirement, not a
|
|
ban.** The catalog rejects generated packages *that do not follow the
|
|
`example_ynh` template*, citing verbose code, hallucinated helpers and
|
|
non-standard directory architectures — then explicitly permits AI use where the
|
|
maintainer is transparent and can explain every line. The failure mode guarded
|
|
against is sprawl, not provenance.
|
|
|
|
**Package provenance is not upstream provenance.** The policy concerns the
|
|
`_ynh` repository — a few hundred lines of shell and one TOML file.
|
|
|
|
**Disclosure is right; headlining it is a tactical mistake.** If provenance
|
|
becomes the pitch, the project is judged on that axis rather than on whether it
|
|
makes distributed manufacturing capacity legible. State it in the README; keep
|
|
the pitch about manufacturing.
|
|
|
|
---
|
|
|
|
## 18. Packaging, when it comes
|
|
|
|
**DISCOVER** — Unknown territory, documented as it is walked: `manifest.toml` v2
|
|
with helpers 2.1, the `scripts/` set, `conf/` templates, `tests.toml`,
|
|
`package_check` levels.
|
|
|
|
The useful comparison for acceptable weight is `paperless-ngx_ynh` or
|
|
`fab-manager_ynh`, not a hello-world. The package is drafted against
|
|
`example_ynh` and reviewed by CIVICVS as his own work.
|
|
|
|
---
|
|
|
|
## 19. First application work item
|
|
|
|
**Complete 2026-08-20.** `sb-geom` is ported to Shapely and the suite is green
|
|
against the 123 frozen cases in `strap-beam-fixtures-8.0.0.json`. The ten
|
|
rejected cases are part of the contract: a port that accepts them is wrong, and
|
|
this one does not.
|
|
|
|
Retained rather than deleted because the framing outlived the task — the
|
|
rejected cases being part of the contract is the reason the oracle means
|
|
anything at all. What comes next is in `ROADMAP.md` §4, not here; an environment
|
|
specification should not carry a work queue.
|