Skip to content

module-kit 0.3.0: appAccess, who came to the App and with which application role (decision 0231) - #4

Merged
immakermatty merged 5 commits into
mainfrom
agent/LZD-0009-signed-in-person
Nov 6, 2026
Merged

immakermatty merged 5 commits into
mainfrom
agent/LZD-0009-signed-in-person

Conversation

@immakermatty

@immakermatty immakermatty commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Status: ready for review

Taken over on 2026-11-05 by the access-control thread (plan LZD-0009, decision 0231, merged in Lazurio/LazurioPlatform#885) from the thread that opened it. The unreleased signedInPerson is replaced by the contract both threads agreed on.

What 0.3.0 adds

appAccess(request, id = "app", env) returns who came to the App and with which application role. Python has app_access(headers, listener_id). Every answer has the same five fields:

entry appRole person source reason When
environment admin subject, username gateway null the person has the whole Environment
app admin / user subject, username gateway null this App only, with the role the Organization gave
local admin null null not-hosted, not-through-gateway, local-entry no external origin (a person's computer), a loopback Host (a sibling, a script, an agent), or the gateway's local entry
unknown null null null no-identity through the gateway without a valid statement, or a Host that is neither the App's hostname nor loopback
  • Host rule (fail-closed). A Host naming the App's external hostname in any letter case, with any port or a trailing dot counts only by the gateway's statement. Only a loopback Host (127.x, localhost, [::1]) is local; any other Host is unknown, never a role.
  • source says who stated the person: gateway today, auth reserved for an identity signed by Lazurio Auth. It is null without a person.
  • Stability. The fields and values never change meaning or disappear; new information comes as new fields.
  • Missing statement. A missing, repeated or malformed statement never grants a role. The headers (GATEWAY_HEADERS) are internal transport, exported only for a Module's own tests.

Depends on

HumanAndMachine-ai/Machines#1103: the gateway sets the subject, the username and X-Lazurio-Entry. Until it is released, a hosted request through the gateway reads unknown. Modules adopt 0.3.0 after the gateway release.

Verified

  • bun run check, bun test: 64 pass. They cover:

    • a Host in another letter case, with :443 or :8443, with a trailing dot, and combinations, with and without identity,
    • loopback and other Hosts,
    • local entry, including repeated or extra headers,
    • a missing or repeated statement, an invalid role and an invalid username.
  • Python: ruff format --check, ruff check, pyright (0 errors), pytest: 64 passed. A repeated subject, entry or Host is never one value.

  • Through the real gateway. bun proof/gateway.ts <Machines checkout> renders the gateway from HumanAndMachine-ai/Machines#1103 (1ecab8f6), runs it with Caddy v2.11.4 in front of a TypeScript and a Python App, and checks 12 Host forms:

    • as is, upper case, :443 and another port: environment/admin with a person, unknown without one, nothing without a session,
    • a trailing dot: Caddy refuses it with 421 and the App is never reached.

    A client's forged X-Auth-Request-User, X-Lazurio-Entry: app and X-Lazurio-App-Role: admin never count, and no request is local. The proof is not in bun test, since it needs Caddy, OpenSSL and uv. The app-only entry (app) cannot be proved through the gateway yet: it arrives with the gateway's app-only entry in LZD-0009 M5.

🤖 Generated with Claude Code

…request (Lazurio/LazurioPlatform#474)

Reads X-Auth-Request-User (subject) and X-Auth-Request-Preferred-Username
(username), which the Environment's gateway sets from its own admission
answer, only behind the gateway: the listener has an external origin and the
request's Host is its host. Everywhere else it is unavailable with a reason
(not-hosted, not-through-gateway, no-person). Python gets signed_in_person
with the same rules (lazurio-module-kit 0.2.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bez blokujících nálezů.

Non-blocking: V README prázdný řádek před novými funkcemi rozděluje tabulku API; odstraňte jej při nejbližší úpravě dokumentace.

Order and migrations: Pro dostupnou identitu na hostovaném vstupu je třeba nejprve dokončit review a rollout brány https://github.com/HumanAndMachine-ai/Machines/pull/1103; do té doby helper vrací unavailable. Poté ověřit skutečnou cestu na Prostředí.

— Pablo

@immakermatty
immakermatty marked this pull request as draft November 5, 2026 23:19
@immakermatty

Copy link
Copy Markdown
Contributor Author

Vracím do Draft. Rozhraní pro moduly se sjednocuje s návrhem řízení přístupů (role aplikace, sdílení aplikace, podepsané potvrzení identity), který vzniká v jiném vlákně. Cílem je, aby moduly měly jen jeden kontrakt, který se později nezmění. Do rozhodnutí sem nic nepřidám. Pokud se rozhraní změní, upravím tento PR, jinak ho vrátím do Ready beze změny.

Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · T3 thread 84e912d4-a4e2-4dec-b58a-a33b94b221aa

…cation role (decision 0231)

Replaces the unreleased signedInPerson with the stable contract both threads
agreed on: entry (environment, app, local, unknown), appRole (admin, user,
none with unknown), person (subject, username) and source (gateway, later
auth). A request from inside the Environment is local and an application
admin without a name; a request through the gateway without a valid statement
is unknown and has no role. The headers are internal transport, exported as
GATEWAY_HEADERS for a Module's own tests. Python gets app_access with the
same rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty immakermatty changed the title module-kit 0.3.0: signedInPerson — who is signed in to Lazurio for a request (LazurioPlatform#474) module-kit 0.3.0: appAccess, who came to the App and with which application role (decision 0231) Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty marked this pull request as ready for review November 6, 2026 00:10

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking:

  1. appAccess and app_access treat any Host unequal to the literal external-origin host as local admin. Caddy matches hostnames case-insensitively and accepts an explicit port, forwarding the original Host: e.g. EXAMPLE.TEST or example.test:443 reaches the example.test gateway route, but the helper returns local/admin even when the gateway states app/user (or no identity). This lets an app-only visitor gain application-admin privileges. Distinguish gateway traffic without granting local admin for an equivalent or ambiguous Host, and test both implementations through the actual gateway with case and port variants, including missing identity.
  2. In Python, duplicate subject headers are collapsed to None; with X-Lazurio-Entry: local they are then accepted as a valid local statement and grant admin. A repeated or malformed statement through the gateway must return unknown, not a role. Preserve the distinction between absent and repeated values and add a regression test.

Non-blocking: None.

Order and migrations: Land the decision Lazurio/LazurioPlatform#885 first; release and roll out https://github.com/HumanAndMachine-ai/Machines/pull/1103 before Modules adopt the hosted identity capability.

— Pablo

… Host is local (review)

Review (Pablo):
- A Host naming the external hostname in another letter case, with a port
  or a trailing dot reaches the App through the gateway, yet read as local
  admin. Now the hostname is normalized as the gateway matches it: such a
  request counts only by the gateway's statement (unknown without one), a
  loopback Host (127.x, localhost, [::1]) is local, any other Host unknown.
- Python collapsed a repeated subject to absent, so with a local entry it
  read as local admin. Absent and repeated now differ; a local entry
  carries no person, username or role, otherwise it is unknown.
- source and reason are present in every answer (source null and reason
  set without a person, reason null with one), in TypeScript and Python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking:

  1. The PR description and the cited LZD-0009 plan (https://github.com/LazurioDevelopment/mission-control-data/pull/25, decision of 2026-11-05) still say that another Host gives local/admin, while both implementations and README now give unknown for a non-loopback Host. Align the PR description and the plan with the intended fail-closed contract; otherwise consumers and implementers receive contradictory access rules.
  2. The previous security review asked for both implementations to be exercised through the actual gateway with hostname case and port variants, including an app-only user and missing identity. The new tests construct headers directly; neither this PR nor the gateway PR records that end-to-end proof. Add or link the gateway integration proof so it establishes that real routing and forwarded Host values cannot turn those requests into local admin.

Non-blocking: None.

Order and migrations: Land Lazurio/LazurioPlatform#885 first; release and roll out https://github.com/HumanAndMachine-ai/Machines/pull/1103 before Modules adopt hosted identity.

— Pablo

@immakermatty

Copy link
Copy Markdown
Contributor Author

Addressed in de029b7 (and e6f8895 for CI):

  1. Host: the hostname is normalized as the gateway matches it (letter case, port, trailing dot). A request for the App's hostname counts only by the gateway's statement (unknown without one); only a loopback Host (127.x, localhost, [::1]) is local, any other Host is unknown. Tests in both implementations cover upper case, :443, :8443, a trailing dot and their combination, with and without identity. HumanAndMachine-ai/Machines#1103 (1ecab8f6) adds the gateway side to proofs/ingress.mjs: Caddy routes those Host forms to the Module with the Host as sent and with the statement, and an answer without a person states nothing.
  2. Python: absent and repeated headers now differ (_REPEATED); a local entry counts only without any subject, username or role. Regression tests cover a repeated subject next to local, a repeated entry and a repeated Host.
    Also: source and reason are present in every answer (TypeScript and Python), as the #874 review asked.

…achines-rendered Caddy, for every Host form (review)

bun proof/gateway.ts <Machines checkout> renders the workspace gateway from
Machines, runs it with Caddy in front of a TypeScript and a Python App and
checks the Host forms a client can send: as is, upper case, :443, another
port (a person is environment/admin, an admitted session without a person is
unknown, nothing without a session) and a trailing dot (Caddy refuses it with
421 and the App is never reached). A client's forged statement never counts
and no request through the gateway is local. Typechecked and linted with the
package; not part of bun test, since it needs Caddy, OpenSSL and uv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Contributor Author

Addressed in 4341986:

  1. The PR description now states the fail-closed Host rule (only a loopback Host is local; any other non-App Host is unknown). The plan LZD-0009 gets the same correction in LazurioDevelopment/mission-control-data#25.
  2. proof/gateway.ts exercises both implementations through the actual gateway (Machines#1103 rendered, Caddy v2.11.4) with case and port variants and a trailing dot, with a person and with an admitted session without one: no request is local, a forged statement never counts, the trailing dot is refused by Caddy (421). Output of the run is in the description. An app-only user cannot pass the gateway yet; that proof comes with the app-only entry (LZD-0009 M5).

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: None. The plan and PR description now agree on the fail-closed Host rule, and the gateway proof exercises both implementations. I reproduced the 12 Host cases with Caddy 2.11.4 against Machines 1ecab8f6; app-only gateway entry remains for M5.

Non-blocking: The proof treats any non-200 response to the first request as an acceptable refusal, including for Host variants expected to reach the App, and does not check Python App reachability on refusal. Consider asserting the expected statuses and App reachability per case so a future routing failure cannot produce a misleading PASS.

Order and migrations: Release and roll out https://github.com/HumanAndMachine-ai/Machines/pull/1103 before Modules adopt hosted identity; app-only entry follows in LZD-0009 M5.

— Pablo

@immakermatty
immakermatty merged commit 3a733c4 into main Nov 6, 2026
3 checks passed
@immakermatty

Copy link
Copy Markdown
Contributor Author

Merged as 3a733c4 on the approved head and released as v0.3.0 (https://github.com/Lazurio/module-kit/releases/tag/v0.3.0), on Matěj's instruction to publish after Pablo's approval. The non-blocking point (assert the expected status and the Python App's reachability per Host case in proof/gateway.ts) goes into a follow-up pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants