Files
mechanical-compiler/docs/IDENTITY-CONTRACT.md
T
TheRON 0c220f449b ROADMAP: section 7 is binding. PRECISION: prismatic-only is a limit
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.
2026-09-14 04:45:04 -05:00

13 KiB

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.