ROADMAP opened by saying nothing in it is a promise. Section 7 holds eighteen standing principles cited by number as settled law from commit messages and from other documents - 4 governs what is hashed, 13 defines what a standard is, 17 governs what may write to a record, 18 governs how a contribution is admitted. A reader taking the header at its word would conclude Principle 17 is optional. The disclaimer now says what it always meant: it covers section 4 sequence and section 5 assumptions, not section 7.
Principle 5 gets the same qualification PROCESS section 8 got. A permanent deviation is the specification is right about facts on a host and would be licence to lower a REQ if read without limit.
PRECISION section 10 requires every section 7 entry to say whether it is a limit that work may lift or a boundary that was chosen. The prismatic-only entry said neither, which is the single most consequential ambiguity in the document. It is a limit, and the reason is now recorded: the oracle is a two-dimensional oracle with a scalar length multiplier - every recorded value is a property of the cross-section except LENGTH_MM, VOLUME_MM3 and MASS_G, and volume equals rounded section area times length to within 3.6e-12 across all 113 accepted cases. Lift it carelessly and that relation stops holding; nothing fails, it just stops meaning what it means. Anything keeping each station two-dimensional preserves it.
Also in PRECISION: STL export landed and is no longer planned. An absent export format is a limit; the refusal to emit toolpaths is a boundary.
ROADMAP section 4a corrected to match STAGING-STATE section 3a: nothing here is implemented on Kane Fabric, and SASE was used and never defined. The port is recorded done. The seed commit and test count are removed as derivable.
IDENTITY-CONTRACT: the rewrap owed from a76879f.
Applied by anchored patcher. Suite 643 passed, oracle intact. Documentation only.
305 lines
13 KiB
Markdown
305 lines
13 KiB
Markdown
# IDENTITY-CONTRACT.md
|
|
|
|
How the Mechanical Compiler learns who a visitor is, and the boundary that keeps
|
|
it from learning anything else.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Created | 2026-09-12 |
|
|
| Status | **Specified, not implemented.** See §8. |
|
|
| Audience | This project, and whoever builds the membership system |
|
|
| Companions | `PROCESS.md`, `STAGING-STATE.md`, `deploy/README.md` |
|
|
| Open concerns | `CONSUMER_INTERFACE_GATES.md` |
|
|
|
|
---
|
|
|
|
## 0. Why this document exists separately
|
|
|
|
It is the only document in this repository written to be read by someone who
|
|
does not work on this repository.
|
|
|
|
The membership system is a different application, in a different container, with
|
|
a different release schedule and a different author. Two systems that must agree
|
|
on an interface need that interface written down once, somewhere both of them
|
|
can point at. Burying it in `ENVIRONMENT.md` — which describes how *this* host
|
|
is built — guarantees the other side never finds it.
|
|
|
|
The compiler is expected to catalogue a very large number of applications over a
|
|
very long time. An interface that is small enough to state on one page is the
|
|
only kind that survives that.
|
|
|
|
**This document says what crosses the boundary. It does not say what anyone
|
|
outside this repository must build.** Where a concern has surfaced that is not
|
|
settled, it is recorded in `CONSUMER_INTERFACE_GATES.md` rather than resolved
|
|
here by assertion.
|
|
|
|
---
|
|
|
|
## 1. The shape: relying party, not integration
|
|
|
|
**The compiler does not authenticate anyone. It is told.**
|
|
|
|
Authorisation is decided upstream, against the membership system. By the time a
|
|
request reaches the application, the decision is already made. The application
|
|
reads an identity and acts; it never asks a question about eligibility, because
|
|
it has nothing to ask the question with.
|
|
|
|
This is the same rule the project applies to physical claims. `PRECISION.md`
|
|
puts codes and permitting out of scope — the compiler records what is true and
|
|
never issues a verdict. "Who may do this" is a verdict. It belongs to whoever
|
|
holds the membership roll, and not here.
|
|
|
|
The practical consequence is that the two systems share nothing but a decision.
|
|
Either can be rewritten entirely without the other being read.
|
|
|
|
---
|
|
|
|
## 2. The chain as it actually is
|
|
|
|
```
|
|
browser
|
|
-> wg-pk public TLS for dev.mechcomp.kane-il.us
|
|
-> WireGuard tunnel
|
|
-> DNAT on srv-b scoped to the hub as source
|
|
-> CT 101 nginx local staging-CA TLS; the deciding hop -- see below
|
|
-> CT 100 :8770 the application
|
|
```
|
|
|
|
**Nothing decides anything today.** CT 101 terminates TLS and proxies. It sets no
|
|
`X-Mechcomp-Auth-*` header, because §3 is not implemented — see §8. The label
|
|
marks where the decision belongs once it exists, not a duty CT 101 currently
|
|
performs. An earlier version of this diagram read `DECIDES`, which an outside
|
|
implementer would reasonably have taken as a description of what runs.
|
|
|
|
Three things about this that are easy to get wrong:
|
|
|
|
**CT 101 does not know the public name.** Its `server_name` is
|
|
`mechanical-compiler.dev.infra` and its certificate is local. See
|
|
`deploy/README.md`.
|
|
|
|
**The decision belongs at the last hop, never at the hub.** `wg-pk` carries
|
|
twenty peers, is estate infrastructure this project does not own, and every
|
|
change there is an escalation under `PROCESS.md` §7. Putting authorisation at
|
|
the hub would make every future adjustment to who-may-do-what an estate change,
|
|
and would teach a shared transport about one project's membership roll. The hub
|
|
stays transport.
|
|
|
|
**The chain is expected to grow.** Additional containers, owned by other
|
|
projects, may insert themselves between the browser and this application. CT 101
|
|
is named above because it is what exists today — it is not the invariant. The
|
|
invariants are in §4 and are written as properties of the chain, not duties of a
|
|
named container.
|
|
|
|
---
|
|
|
|
## 3. What crosses the boundary
|
|
|
|
Two request headers, set by the deciding hop, and nothing else.
|
|
|
|
| Header | Meaning |
|
|
|---|---|
|
|
| `X-Mechcomp-Auth-Id` | The identity: a stable identifier for the person the decision was made about. |
|
|
| `X-Mechcomp-Auth-Method` | How it was established. Recorded verbatim, never interpreted. |
|
|
|
|
The headers name the *receiver*, not any deployment. A jurisdiction, a domain or
|
|
an operator in a header name would be a deployment fact in an invariant place.
|
|
|
|
**The identifier is not required to be an email address.** An address is what the
|
|
current deployment uses. An opaque or epoch-scoped token is equally valid and
|
|
needs no change here. The application stores the string, renders it, and makes no
|
|
claim about its form. See `CONSUMER_INTERFACE_GATES.md` G-3, where the tension
|
|
between durable provenance and non-durable identity is recorded and left open.
|
|
|
|
**Method has a shape, not a vocabulary.** Enough to say who established the
|
|
identity, by what mechanism, and what was actually checked — for example
|
|
`<issuer>/<mechanism>`, with whatever detail the issuer considers meaningful. No
|
|
value is enumerated here, because this document refuses to know what mechanisms
|
|
exist upstream. It is recorded verbatim so that a reader years from now sees what
|
|
was claimed rather than this project's interpretation of it.
|
|
|
|
These map onto `design_record.Author.email` and `Author.method`, which exist and
|
|
are tested today. When they arrive, `verified` becomes true and the rendered
|
|
design record line changes from
|
|
|
|
```
|
|
author someone@example.org (self-declared, unverified)
|
|
```
|
|
|
|
to
|
|
|
|
```
|
|
author someone@example.org (verified: <method, verbatim>)
|
|
```
|
|
|
|
Nothing in `design_record.py` changes shape. The parser already refuses to
|
|
recover `verified` from text — a record read back is always self-declared,
|
|
because a file cannot attest to its own verification. That asymmetry was built
|
|
for this moment.
|
|
|
|
`Author.email` is named for what it holds in this deployment, not for what the
|
|
contract requires. Renaming it is a format change to every stored record and is
|
|
deliberately not done.
|
|
|
|
**A handle is not the identity.** A person may later choose a display name. It
|
|
does not replace the identifier and it does not travel in this contract.
|
|
|
|
---
|
|
|
|
## 4. Four invariants
|
|
|
|
**I-1. Exactly one hop decides, and it is the last one before the application.**
|
|
Two intermediaries both setting `X-Mechcomp-Auth-*` is a forgery vector wearing
|
|
the costume of a deployment change: the later one wins, the earlier one believes
|
|
it decided, and nothing reports the conflict. However long the chain becomes,
|
|
one hop holds this responsibility.
|
|
|
|
**I-2. Inbound `X-Mechcomp-Auth-*` is stripped at every location, before
|
|
anything else.** A proxy forwards headers it was given. Without the strip, any
|
|
client can send an identity header and be whoever it likes. This is the
|
|
invariant the others exist to support, and it is worth landing before there is
|
|
anything to forge against.
|
|
|
|
**I-3. The application is reachable only from the deciding hop.** It binds
|
|
`10.20.0.10:8770` on a portless service bridge with LAN traffic dropped.
|
|
`web/app.py` defaults to loopback rather than `0.0.0.0` for the same reason:
|
|
behind a proxy, binding too narrowly fails loudly as a 502, and binding too
|
|
widely fails silently as an open service nobody notices.
|
|
|
|
**I-4. Anything that is not an affirmative permission is a refusal.** Unreachable,
|
|
timed out, malformed, `5xx` — all deny. Fail-open and fail-closed are both
|
|
defensible and they are not the same system; discovering which one was built
|
|
during an outage is the worst way to find out. The consequence is that the
|
|
eligibility endpoint is a hard dependency of every gated route, which is recorded
|
|
as an open concern rather than designed around (`CONSUMER_INTERFACE_GATES.md`
|
|
G-5).
|
|
|
|
**And a fifth that belongs to the application:** an absent header means
|
|
unauthenticated. It is never an error and never a default identity. The composer
|
|
must run from a bare checkout with no proxy in front of it, and an anonymous
|
|
visitor is an ordinary, expected caller. A missing header that produced a 500, or
|
|
that silently became somebody, would be worse than no authentication at all.
|
|
|
|
---
|
|
|
|
## 5. One gated prefix
|
|
|
|
Everything requiring membership lives under `/m/`. Everything else is public.
|
|
|
|
```
|
|
/ the composer page public
|
|
/api/build build and view a section public
|
|
/m/stl export members
|
|
/m/... anything gated later members
|
|
```
|
|
|
|
**This table is the scheme, not the current state.** `/m/` is not gated today and
|
|
`/m/stl` ships open (§8). The right-hand column says where the line falls once
|
|
the `location /m/` block exists, not where it falls now. `web/app.py` states the
|
|
same thing in its own docstring.
|
|
|
|
Stated explicitly because `CONSUMER_INTERFACE_GATES.md` G-1 records the standing
|
|
lesson for this document: an interface document should contain no statement a
|
|
reader will take as fact when it is not yet one.
|
|
|
|
The proxy gets **one** `location /m/` block, written once and not edited again.
|
|
Gating a new endpoint afterwards is choosing a URL in Python — no proxy change,
|
|
no shared-infrastructure change, no escalation. Ungating one is the same move in
|
|
reverse.
|
|
|
|
That cheapness is the point. It means the placement of the line is a reversible
|
|
decision rather than a structural one.
|
|
|
|
**Where the line falls once it is enforced: export is gated; looking is not.**
|
|
The catalogue and the composer are open to anyone. What requires membership is
|
|
producing an artifact that carries a design record, because the record names an
|
|
author, and an author only means something once somebody established who they
|
|
are. The site is
|
|
the shopfront; the wall goes where the claim is made.
|
|
|
|
---
|
|
|
|
## 6. What the compiler must never be told
|
|
|
|
This list is the boundary. It is short on purpose, and it is the part most
|
|
likely to erode.
|
|
|
|
- **What a building is.** Membership attaches to buildings in the current
|
|
design. The compiler has no representation of one and must not acquire it.
|
|
- **Parcels, delivery points, or any other geography.** Same rule, and it holds
|
|
regardless of which primitive the membership layer settles on.
|
|
- **What a membership level is.** `CURRENT RESIDENT`, `HOA MEMBER`, `3D PRINTER`
|
|
— the compiler does not know these strings exist. By the time a request
|
|
arrives, the level has already been resolved into "this request may proceed".
|
|
- **The membership roll.** No lookup, no directory, no list of who exists.
|
|
- **Group names in any form**, including as a configuration value. A
|
|
`REQUIRED_GROUP` setting in the application would be the leak arriving by the
|
|
side door.
|
|
|
|
If the application ever needs one of these to answer a request, the design has
|
|
failed and the fix is upstream, not here.
|
|
|
|
The test: the membership system must be able to restructure its entire model —
|
|
rename every level, change what a building is, replace its storage — without a
|
|
line of this repository being read.
|
|
|
|
---
|
|
|
|
## 7. What the deciding hop needs from upstream
|
|
|
|
Minimum, and deliberately not more. How it is implemented is that project's
|
|
business, and this document names no system that does not exist.
|
|
|
|
- **An eligibility endpoint** reachable from the deciding hop, which answers a
|
|
request with `2xx` when the caller may proceed and `401` when not. Whatever
|
|
session or token the caller carries is between that endpoint and the browser;
|
|
the deciding hop forwards and does not interpret. Per I-4, anything else is a
|
|
refusal.
|
|
- **Two response headers on a `2xx`**, carrying the identifier and the method,
|
|
for the deciding hop to promote onto the proxied request.
|
|
|
|
That is the whole of it. The endpoint's path, its session mechanism, its login
|
|
page and its storage are not this contract's concern and must not be constrained
|
|
by it.
|
|
|
|
**This is not a requirement on any named project.** Nothing here obliges the
|
|
geography layer to become an identity provider, and it should not. The
|
|
authorisation decision belongs to a membership or participation system sitting
|
|
between geography and this application — a shape both projects arrived at
|
|
independently (`CONSUMER_INTERFACE_GATES.md` §4).
|
|
|
|
---
|
|
|
|
## 8. Current state
|
|
|
|
**Not implemented.** Specified here so that it is designed once, before the
|
|
endpoints that will depend on it exist.
|
|
|
|
What is true today: the service is world-reachable and unauthenticated, and
|
|
`/m/` is not yet gated. The first endpoint under it, STL export, will therefore
|
|
ship open. That is a deliberate, recorded, temporary state and not an oversight
|
|
— it is the same posture the whole service already has, and it changes when a
|
|
membership system offers §7's endpoint.
|
|
|
|
An earlier version of `WORK-ORDER-004` and of `HANDOFF.md` §4 called the open
|
|
service "acceptable for a development name". That reasoning was wrong:
|
|
`dev.mechcomp.kane-il.us` is production, `dev` abbreviates *Mechanical Compiler
|
|
Developers*, and the name is on printed material. The question is carried openly
|
|
instead.
|
|
|
|
**I-2, the inbound strip, does not depend on any of this** and should land on its
|
|
own. It costs one directive and removes a forgery that would otherwise become
|
|
possible the moment §3 is implemented.
|
|
|
|
---
|
|
|
|
## 9. What this removes
|
|
|
|
`HANDOFF.md` §4 listed an access-control layer inside the compiler as the last
|
|
priority. **It is deleted, not deferred.** The compiler will not have an ACL, a
|
|
user table, a login form, or a session. Authorisation lives upstream, and an item
|
|
leaves the roadmap rather than moving down it.
|
|
|
|
Persistence — the priority above it — changes shape rather than disappearing. A
|
|
saved design belongs to a verified person, and the verified person now comes from
|
|
outside, so it should be built knowing the identity is external.
|