Skip to content

Vault: Nastavení → Trezor and the Správci of the Environment's own vault (DEV-6631 tasks 648, 638b) - #402

Merged
immakermatty merged 11 commits into
mainfrom
agent/DEV-6631-trezor-page
Oct 14, 2026
Merged

immakermatty merged 11 commits into
mainfrom
agent/DEV-6631-trezor-page

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Root decision 0198 points 2, 3 and 5 (plan DEV-6631 tasks 648 and 638b): every Environment owns its vault (#391), whoever has access to the Environment is a Správce (an Admin) of its vault and has all of its passwords, and people enter the vault from the Launchpad's Nastavení → Trezor. Until now nobody but the Environment's own account was a member of its vault, and the Launchpad had no way into it besides the Tools row's link.

Target state

  • Nastavení → Trezor (/settings/vault, a Settings section after Nástroje), following the published wireframe (EnvVault of prototypes-lazurio 794f3735): the rule "Kdo má přístup do Prostředí, má všechna jeho hesla.", one row with the vault's name (vault.name) and its state (the item count, "Zakládání trezoru…" while a setup runs, "Neodpovídá" when the vault does not answer), and the actions the person's membership decides:
    • with an account in the network's vault: "Přidat položku" → <server_url>/#/vault?organizationId=<vault>&collectionId=<Agenti> (phase 1: the web vault's New then preselects both) and "Otevřít trezor" → <server_url>/#/vault?organizationId=<vault>;
    • without an account (or without a master password yet): only "Vytvořit účet v trezoru" → <server_url>/#/login, and only where the network's vault takes the Lazurio sign-in (its public GET /identity/sso/prevalidate answers with a token, kept a minute); otherwise no account button;
    • while a setup runs, or while the vault's creation is not confirmed yet (Vault: each Environment owns its vault (root decision 0198, DEV-6631 task 638a) #391's state creating): "Zakládání trezoru…" and no way in yet;
    • a 0197 pilot: "Otevřít trezor" to its collection; this computer: "Trezor na tomto počítači zatím není."; a handover without vault: one line; a Team or Automated Environment: "Trezor tohoto Prostředí zatím používají jen jeho agenti.";
    • the Launchpad never lists items or shows a value; a hidden slot above the row waits for "Žádosti agentů" (task 651, another PR).
  • The person at the browser, server side only: the hosted admission already revalidates the exact session cookie against the gateway's configured /oauth2/auth; that same answer now yields the admitted session's subject from the gateway's own X-Auth-Request-User (oauth2-proxy with set_xauthrequest = true, user claim sub; Caddy strips only X-Auth-Request-Access-Token there). Only a lowercase UUID counts; a header of the request never names anyone. This is a narrow amendment of decision F11 ("admission is not identity"), recorded in docs/decisions.md: used only for vault membership, never for repository rights, an Operator or a role.
  • Správci membership (src/vault/members.ts, under the vault lock, as the vault's only Owner): the person is <sub>@vaultwarden-identity.lazurio.ai. On a personal or work Environment, POST /api/vault/membership lists the members (bw list org-members), invites an absent person as Admin (type 1) through the vault's API (one API-key sign-in only when needed), maps "User does not exist" to no-account, keeps an invitation to an account without a master password Invited (awaiting-password; the vault accepts it when the person sets the password), confirms an accepted member (bw confirm org-member) and reports member.
  • The invitation gate (after the review of 2026-10-14): the Environment invites and confirms only where the network's vault refuses every password path of the person's address (SSO_ONLY, the address not exempt). The vault itself answers this: a password grant naming only the address gets "SSO sign-in is required". Elsewhere, or when the vault doesn't answer, nobody is invited or confirmed (sign-in-not-required, and the page says only the agents use the vault). Removal needs no gate.
  • Removal: a work Environment keeps the fingerprint of the owner.assignment its Správci were added under (lazurio-assignment.json, a SHA-256 of the Organization and the assignment's kind and GitHub id, never a person); when it changes (or the file is lost), every Admin-type member leaves; the Owner and User/Manager/Custom members are never touched. Checked at every membership look and, in the installed hosted Launchpad, after its start's setup and every five minutes (local reads; the vault is asked only after a change). A personal Environment never removes; a Team Environment neither adds nor removes.
  • No stored access list: the vault's membership is the only state.

What changes

  • src/launchpad/hosted-trust.ts: Admission is {ok: true, subject: string | null}; the subject from the auth endpoint's answer, cached with the admission.
  • src/vault/members.ts (new): vaultMembership, vaultAudience, vaultSignIn, personAddress.
  • src/vault/bw.ts: orgMembers (bw list org-members --organizationid), confirmMember (bw confirm org-member <id> --organizationid), parseOrgMembers (only id, email, status, type are kept).
  • src/vault/register.ts: fixed reasons no-account ("User does not exist"), already-member and sso-required ("SSO sign-in is required").
  • src/vault/context.ts: VaultContext.assignment (assignmentOf); src/vault/store.ts: lazurio-assignment.json; src/vault/flow.ts: exports connectedAccount, deviceOf, the journal operation members.
  • Launchpad: vault-page-routes.ts (GET /api/vault, POST /api/vault/membership), vault-page-view.ts (pure), vault-page-panel.ts (DOM), routes.ts (section vault), index.html (first-paint list, hide rules, nav item, section, slot), ui.ts, messages.ts (en, cs formal), server.ts (subject passed from the admission, routes, the audience look), vault-routes.ts (running()).
  • Docs: F11's amendment and F53's addendum of 2026-10-14 (docs/decisions.md), hosted-entry.md, environment-access.md, environment-tools.md, launchpad-development.md.

Deviation from the shaping (please decide)

The invitation gate is SSO_ONLY for the person's address, not "SSO declared". The review asked for a network-side registration gate. The coordinator's design gated on /identity/sso/prevalidate (SSO declared) plus Machines #508's INVITATIONS_ALLOWED=false and SIGNUPS_ALLOWED=false. That rested on "an org invite to an existing SSO stub creates no Invitation row", which the fork source and the real image contradict:

So the gate is the vault's own is_sso_required answer. Consequence: until a network runs SSO_ONLY, its Environments add nobody; people use the vault through the agents. The earlier invite-then-withdraw is gone: a failed withdrawal left a registrable invitation (review P1). Details and evidence are in the PR comment replying to the review.

Other choices to note:

  • Automated Environments are treated like Team ones (no add, no remove): their audience has no defined removal source either.
  • Czech wording: the page uses "Prostředí" as the wireframe and the task's rule do; the rest of the Launchpad's Czech copy still says "Environment" (decision 0170). Worth one consistent decision.
  • The page reuses the Tools row's refresh (POST /api/tools/bitwarden/refresh) for the fresh count and "Neodpovídá" rather than a new route.

What deliberately does not change

  • Secret requests and the Folder texts (tasks 651, 652, another agent), the Dashboard, Machines, releases.
  • The Tools row and its routes (only running() is exposed).
  • A person who already holds a shared collection as a User is not made a Správce automatically (never touched).
  • Nothing is stored about people; no Platform-side access list.

Verification

  • Full gate bun run check on head adbaafa6f7e1948185060429350986e43481eaa3 (on main dace17d7): Biome (693 files), tsc, the public guard (850 files), the proof build and smoke, then 3511 pass, 8 skip, 1 fail (601 s). The one failure is the known race of Flaky: launchpad-prompts expects no GitHub call before admission, but the Launchpad's start peeks the Personalspace holder in the background #413: tests/launchpad-prompts.test.ts asserts no GitHub call before its first 401, while the Launchpad's start peeks the Personalspace holder in the background. It is untouched here, passes alone 5/5 and next to this PR's tests 3/3, and failed in both sequential full runs on this machine. GitHub CI on this head (run 38574972736): static checks, tests 1/2 and 2/2, and the ubuntu check all pass.

  • Contract tests on the real consumer: tests/launchpad-vault-page.test.ts (13), run against startLaunchpad over HTTP behind a fake gateway auth endpoint. The fake Vaultwarden and fake bw cover org members, invite, delete, confirm, SSO prevalidate, and SSO_ONLY in Vaultwarden's own order. Covered:

    • no identity means no membership action; forged identity headers (X-Auth-Request-User, X-Auth-Request-Email, X-Forwarded-User/-Email) are ignored;
    • no-account, with no invitation row left;
    • behind the gate, a pending invitation is kept, unregistrable, and confirmed after the password;
    • with SSO declared alone or no SSO, nobody is invited or confirmed, not even an already-accepted member;
    • a failed invitation, or one whose answer is lost, leaves nothing registrable, and the next look goes on from the vault;
    • accepted when confirmation fails, then confirmed; a member is left as is; a User stays a User;
    • reassignment removes only Admin-type members (Owner, User and Custom stay) and re-adds the person with access; the start's look does this without any page visit;
    • personal never removes; Team does nothing (no bw call, no API request); SSO off means no account button;
    • links carry the exact organization and collection IDs;
    • no subject, address, master password, API key, session or access token in the console log, the journal, bw's argv or any answer;
    • a workstation's page.
  • Also: tests/launchpad-hosted-trust.test.ts (the subject; forged headers for named, anonymous and denied sessions), tests/vault-page-view.test.ts (5, including creating and the closed gate), tests/vault-members.test.ts (only a lowercase UUID names a person; a vault not set up is asked nothing), tests/vault-context.test.ts (the fingerprint), routes and first-paint.

  • Mutation checks (each reverted): no gate → 2 tests fail; a gate on prevalidate alone → the SSO-declared case fails; trusting a forwarded X-Auth-Request-User → 2 fail; removing every non-Owner → 1 fails; Team adding → 1 fails; never removing → 3 fail.

  • The premise, on the real image ghcr.io/lazurio/vaultwarden:1.37.4-lazurio.1@sha256:68da93bb…f492 (amd64 under Rosetta, own container prefix, port and TLS CA):

    Settings prevalidate Probe for the person's address Owner's invitation to a passwordless account Its address registrable?
    Machines Qualification: the canary of v0.1.9-rc.6 and its evidence (DEV-6668) #508's contract 200 client_id cannot be blank status 0 yes (HTTP 200)
    the same plus SSO_ONLY 200 SSO sign-in is required status 0 no (HTTP 400)
  • End to end on the real image with the real pinned bw 2026.7.0, driving this branch's core (run on a73cf45; since then the vault code is unchanged, only the base, comments and docs moved) in phases on one data volume:

    • Qualification: the canary of v0.1.9-rc.6 and its evidence (DEV-6668) #508's settings alone: nobody is invited, and the stub stays unregistrable;
    • SSO_ONLY: "User does not exist" maps to no-account; a password account is invited, accepted and confirmed by the real bw confirm org-member; the stub is invited and kept at status 0 and cannot be registered (400); no second invitation; a reassignment removes only Admins and keeps the User and the Owner; the person with access is re-added; no address, subject or secret in 17 bw argv lists or 8 journal entries;
    • back without SSO_ONLY, the confirmed person decrypts the organization key bw encrypted to them, down to "Agenti".
    • Containers, volume and state were removed afterwards. The harness stays outside the repository.
  • In a browser (decision 0178): a throwaway hosted Launchpad of this branch per scenario (fixtures, Czech) behind a stand-in gateway proxy, driven in the installed Chrome (playwright-core). Scenarios: a work member (name, "12 položek", "Přidat položku" + "Otevřít trezor" with the exact hrefs), no account ("Vytvořit účet v trezoru" → #/login), no account with SSO off (no buttons), Team (agents-only line, no buttons), creating ("Zakládání trezoru…"), no vault, workstation, and a 390 px viewport. 40/40 checks on adbaafa, no page errors (creating without a running setup, and the closed gate, are covered by tests/vault-page-view.test.ts, not in the browser). A real Remote Environment, a live gateway and a live SSO sign-in are not part of it.

Risks, open questions, follow-ups

  • Live gateway: the subject header is proven by Machines' own oauth2-proxy proof (organization-host/proofs/oidc-consumer: X-Auth-Request-User is the opaque OIDC subject) and by reading the workspace-vm config; it has not been read on a live Environment gateway yet. Please verify on a canary: the page's membership must answer for the person, not no-identity.
  • Issuer: the gateway's sub and the vault client's sub must come from the same realm (workspace, contract C1); otherwise people stay no-account (fails closed).
  • Network vault config: Správci are added only where the network runs SSO_ONLY (Machines turns it on per network; DEV-6631's staged rollout). Qualification: the canary of v0.1.9-rc.6 and its evidence (DEV-6668) #508's INVITATIONS_ALLOWED=false keeps "User does not exist" for a person without an account. With true, an invitation creates an empty account, which is safe under SSO_ONLY and is taken over at the person's first sign-in. SSO_ONLY must never be turned off on a network with pending invitations: that reopens their addresses to registration. The gate then stops confirming, but the account is lost to its person.
  • Rate limit: each look at a person without an account costs one API-key sign-in (the vault's login limit counts them per IP); members cost none.
  • Revocation: a removed Správce keeps what their clients synced; people change the passwords they knew.
  • Known gap (issue draft below): Team and Automated Environments, and sharing withdrawn without reassignment.

Issue draft (Lazurio/LazurioPlatform)

Vault: Správci of Team and Automated Environments, and sharing withdrawn without reassignment

Under root decision 0198 point 2 whoever has access to an Environment is a Správce of its vault. The Platform adds the person its gateway admits on personal and work Environments and removes every Admin-type member when a work Environment is reassigned (F53's addendum of 2026-10-14). It does not add or remove anyone on a Team or Automated Environment, because nothing tells it when a person's access ends there (leaving the Team, a new responsible operator), and it does not remove a person whose shared access to a work Environment was withdrawn without a reassignment. Until then those people use the vault through the Environment (agents, the requests of task 651). Needed: one live answer "who may enter this Environment" from Auth or the Dashboard (DEV-6631 contract C4's target), consumed through the relay or a workload client; then the Environment removes every Admin-type member who is not in it. No Platform-side list.

Base

#391 is merged into main (rebase-merge, dace17d7), and this pull request now targets main with only its own 11 commits:

Plan and work context

  • Mission Control: DEV-6631 tasks 648 and 638b, plan; contracts C3/C4; follows Vault: each Environment owns its vault (root decision 0198, DEV-6631 task 638a) #391 (merged).
  • Worktree: HumanAndMachine-ai_GEN3/.worktrees/productionspace/LazurioPlatform/DEV-6631-trezor-page, branch agent/DEV-6631-trezor-page.
  • Agent / thread: Claude Code (Opus 5.5), subagent of session b643d1c5-bc04-44a8-a2c5-d80707571628 (claude --resume b643d1c5-bc04-44a8-a2c5-d80707571628).
  • Machine: the Admin's development workstation.

Status: ready for review

🤖 Generated with Claude Code

immakermatty added a commit that referenced this pull request Oct 13, 2026
… and a request's page shows alone

The Trezor page (DEV-6631 task 648, #402) leaves the slot
`<section class="group" id="vault-requests" hidden>` above its vault. The
requests' panel now fills that slot with its head and rows instead of a
group of its own, and this change's scaffold of the section is the same
element, so the panel works with either markup. On a request's page the
panel marks the Trezor section (`data-request`) and only `#vault-request`
shows of it, whatever else the Trezor page holds. The settings comment and
the section's page-action rule match the Trezor page's lines, so both
changes merge there without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
immakermatty added a commit that referenced this pull request Oct 13, 2026
…rezor page's change

The Trezor page's change (#402) extracts the same function under this name;
with one name both changes merge there without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty
immakermatty force-pushed the agent/DEV-6631-trezor-page branch from ac51c73 to bdb33cc Compare October 13, 2026 23:57
immakermatty added a commit that referenced this pull request Oct 14, 2026
… and a request's page shows alone

The Trezor page (DEV-6631 task 648, #402) leaves the slot
`<section class="group" id="vault-requests" hidden>` above its vault. The
requests' panel now fills that slot with its head and rows instead of a
group of its own, and this change's scaffold of the section is the same
element, so the panel works with either markup. On a request's page the
panel marks the Trezor section (`data-request`) and only `#vault-request`
shows of it, whatever else the Trezor page holds. The settings comment and
the section's page-action rule match the Trezor page's lines, so both
changes merge there without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
immakermatty added a commit that referenced this pull request Oct 14, 2026
…rezor page's change

The Trezor page's change (#402) extracts the same function under this name;
with one name both changes merge there without a conflict.

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.

@immakermatty — CHANGES_REQUESTED on bdb33cc (stacked on #391 at 3425e71).

P1 — The claimed immediate withdrawal of a pending invitation is not failure-safe. In src/vault/members.ts:293-321 and :329-332, settlePerson first creates an Admin invitation for an SSO account without a master password, then tries to delete it. If the DELETE fails (e.g. a 503 or process interruption), underLock returns unreachable but leaves the status-0 member/invitation in Vaultwarden. With SSO_ONLY off, that address remains registrable by anyone who knows it; subsequent confirmation can grant the organization key and all passwords to the wrong account. A disposable HTTP fixture probe on this exact checkout made only the withdrawal DELETE return 503 and observed vault-member-unknown/unreachable, member status 0, and registerable(address) === true. The existing happy-path test checks only successful withdrawal. This is a security gate, not a UI retry condition: do not create such invitations on a network where unauthenticated registration through pending invitations is possible. Require the network-side SSO_ONLY/registration gate first, or establish a safe pre-invite existence/master-password check and a recovery strategy for interrupted withdrawals; add a failure/interruption regression test before approval.

Local evidence: 24 focused tests pass, static checks/typecheck/public guard/proof build and smoke pass, CI run 38487371020 green. The added probe was disposable and is not part of the PR. The new diff names no client Organization or person (the sole Organization repository reference is the product-team prototypes-lazurio). I did not merge.

@immakermatty

Copy link
Copy Markdown
Contributor Author

P1, invite-then-withdraw is not failure-safe: agreed, fixed on a73cf45. The withdrawal is gone. The Environment now invites and confirms only behind a network-side gate.

The registration paths (fork source at v1.37.4-lazurio.1 = 520ea668):

  • send_invite: for an existing account without a master password, with SMTP off, it saves an Invitation row (Invitation::new(email).save(), organizations.rs:1057-1060), whatever INVITATIONS_ALLOWED says. That setting only governs invitations to addresses without an account, which create a stub plus a row when it is true.
  • register_verification_email accepts an address that has an Invitation row (identity.rs:1091-1096). register then takes the row (Invitation::take, accounts.rs:363) and sets the password and keys on the existing account. Its other paths are an org invite token (emailed; with SMTP off it never leaves the server), is_signup_allowed, and emergency access.
  • Only is_sso_required, i.e. SSO_ONLY with the address not exempt, refuses all of them up front (identity.rs:1087, accounts.rs at the top of register, and the password grant at identity.rs:68).

So "SSO declared + INVITATIONS_ALLOWED=false + SIGNUPS_ALLOWED=false" (Machines #508's contract) does not close the path. An invitation to an SSO stub still gets a row. On the real image, same data volume:

Vault settings Owner's invitation to a passwordless account Its address registrable by anyone?
SSO declared (dummy issuer), INVITATIONS_ALLOWED=false, SIGNUPS_ALLOWED=false status 0 yes, HTTP 200
the same plus SSO_ONLY status 0 no, HTTP 400

The gate (signInRequired in src/vault/members.ts) is the vault's own answer. Its token endpoint refuses a password grant that names only the person's address with "SSO sign-in is required" exactly when is_sso_required(address). That check runs before any account lookup, the login rate limit or sign-in events; otherwise the answer is "client_id cannot be blank". Only then does the Environment invite or confirm. Elsewhere, or when the vault does not answer, it invites and confirms nobody (sign-in-not-required; the page says only the agents use the vault). Removal needs no gate and runs either way.

Behind the gate an invitation to an account without a master password stays Invited (awaiting-password). That is safe, because no password can be set on the address but through the Lazurio sign-in. The vault accepts the invitation when the person sets the password, and the next look confirms. A failed or interrupted invitation therefore leaves nothing registrable either way.

Evidence on a73cf45:

  • New contract tests, over HTTP against startLaunchpad, with the fake Vaultwarden now modelling SSO_ONLY in Vaultwarden's own order:
    • a pending invitation is kept, unregistrable, and confirmed after the password;
    • nobody is invited or confirmed with SSO declared alone or with no SSO; an already-accepted member is not confirmed there either;
    • an invitation that fails before it is made leaves nothing, one whose answer is lost leaves a pending but unregistrable invitation, and the next look goes on from the vault.
  • Mutations: no gate fails 2 tests; a gate on /identity/sso/prevalidate alone fails the SSO-declared case.
  • Real-image e2e (1.37.4-lazurio.1 by digest, real bw 2026.7.0), in phases across settings on one volume:
    • Qualification: the canary of v0.1.9-rc.6 and its evidence (DEV-6668) #508's settings alone: nobody is invited, and the stub stays unregistrable;
    • with SSO_ONLY: "User does not exist" maps to no-account; a password account is invited, accepted and confirmed by the real bw confirm org-member; the stub is invited and kept at status 0 and cannot be registered (400); no second invitation; a reassignment removes only Admins;
    • back without SSO_ONLY, the confirmed person decrypts the organization key bw encrypted to them, down to "Agenti".

What this costs: until a network runs SSO_ONLY, its Environments add nobody, and people use the vault through the agents. Recorded in F52's addendum (point 5) with the dependency on Machines turning SSO_ONLY on per network. One caution: SSO_ONLY must never be switched off while invitations are pending. The gate then stops confirming, but the address becomes registrable again.

@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.

@immakermatty — APPROVED on a73cf45, stacked on #391 at 3425e71. The earlier P1 is resolved: the invite-then-withdraw path is gone; invitation and confirmation are gated on the vault token endpoint rejecting a password grant for this specific address with "SSO sign-in is required". I checked the pinned Vaultwarden fork source: this predicate is is_sso_required(address) before other token fields; both registration entry points reject that same predicate, whereas a passwordless-account invitation stores an Invitation row regardless of INVITATIONS_ALLOWED. The pending invitation stays Invited and later confirmation follows acceptance. The gate fails closed without SSO_ONLY and removal on reassignment is unaffected. Local isolated exact-head evidence: 19 focused tests passed; lint, typecheck, public guard, proof build and smoke passed; git diff --check clean. Exact-head CI run 38530218420 succeeded, no unresolved threads or competing review requests. Operational dependency: keep SSO_ONLY enabled while pending invitations exist, as F52 point 5 records. No merge performed.

@immakermatty
immakermatty force-pushed the agent/DEV-6631-environment-own-vault branch from 3425e71 to 7eab155 Compare October 14, 2026 13:15
@immakermatty
immakermatty force-pushed the agent/DEV-6631-trezor-page branch from a73cf45 to fa60777 Compare October 14, 2026 13:37
@immakermatty
immakermatty changed the base branch from agent/DEV-6631-environment-own-vault to main October 14, 2026 13:39
@immakermatty
immakermatty dismissed agentrozjedemeai’s stale review October 14, 2026 13:39

The base branch was changed.

immakermatty and others added 11 commits October 14, 2026 15:48
…n answer

The admission already revalidates the exact session cookie against the
gateway's configured /oauth2/auth. That same answer now also yields the
admitted session's subject, read from the gateway's X-Auth-Request-User
(Machines renders oauth2-proxy with set_xauthrequest and the user claim
sub) and accepted only as a lowercase UUID; anything else names nobody.
It is cached with the admission, keyed by the session. A header of the
request itself never names anyone.

This is the narrow amendment of decision F11 for the membership of the
Environment's own vault (root decision 0198 point 2, DEV-6631 task 638b):
vault access is access to the Environment, which is exactly the admission
fact. It is never a repository right, an operator or a role.

Tests: the existing admission contract now states `subject: null`; a new
test proves the gateway's subject is taken, forged request headers
(X-Auth-Request-User, X-Forwarded-User/-Email, ...) are ignored for named,
anonymous and denied sessions, only an exact lowercase UUID names someone,
and the subject is kept with the cached admission.

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

Root decision 0198 point 2 (DEV-6631 task 638b, contract C4): whoever has
access to the Environment is an Admin of its vault. src/vault/members.ts,
under the account's lock, as the vault's only Owner:

- the person is `<subject>@vaultwarden-identity.lazurio.ai`, the address
  Lazurio Auth's vault client issues; bw lists the members, the vault's
  API invites them as Admin (type 1) with one API-key sign-in only when
  needed, "User does not exist" is `no-account`, an accepted member is
  confirmed with `bw confirm org-member`;
- an invitation to an account without a master password is withdrawn at
  once (`awaiting-password`) and made again once the password is set:
  while such an invitation is pending, Vaultwarden lets anyone who knows
  the address register it with a master password unless the network
  requires SSO of people (SSO_ONLY), and the Environment would then
  confirm that account;
- a work Environment keeps the fingerprint of the assignment its Správci
  were added under (`lazurio-assignment.json`, a SHA-256 of the
  Organization and the assignment's kind and GitHub id, never a person);
  when it differs, or cannot be read, every Admin-type member leaves. The
  Owner and User, Manager and Custom members are never touched. A personal
  Environment never removes; a Team or Automated Environment neither adds
  nor removes until a live answer of who may enter exists;
- `vaultAudience` is the removal alone, for the Launchpad's start and its
  periodic look; `vaultSignIn` asks the vault's public
  /identity/sso/prevalidate whether it takes the Lazurio sign-in.

The vault's membership is the only list; nothing returns, logs or
journals an address, a member id or a token. The fake Vaultwarden and the
fake bw learn the members, the invitation, the removal, the confirmation
and the SSO prevalidation with Vaultwarden's shapes and messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vironment's own vault

Root decision 0198 point 5 and the published wireframe (DEV-6631 task
648): a Settings section after Tools at /settings/vault with the rule
"Kdo má přístup do Prostředí, má všechna jeho hesla.", one row with the
vault's name and its state (the item count, "Zakládání trezoru…" while a
setup runs, "Neodpovídá"), and the actions the person's membership
decides: "Přidat položku" (the vault filtered to its collection Agenti,
where the web vault's New preselects them) and "Otevřít trezor", or
instead "Vytvořit účet v trezoru" (the vault's sign-in) while the person
has no account there, offered only where the vault takes the Lazurio
sign-in. A 0197 pilot opens its collection; a workstation and an
Environment without a vault say so in one line; a Team or Automated
Environment says that only its agents use the vault. The Launchpad never
lists items or shows a value. A hidden slot above the row waits for the
agents' secret requests (task 651).

Routes, behind the admission of every route: GET /api/vault (the
recorded state, the running setup's step, the kind of Environment) and
POST /api/vault/membership {} (where the admitted person stands, after
the Environment adds them; with `signIn` while they have no account).
The person is the gateway's subject from the request's own admission.
An installed hosted Launchpad looks at the audience once its start's
setup ends and every five minutes, so a reassigned work Environment's
Správci leave without anyone opening the page.

Tests over HTTP against startLaunchpad behind a fake gateway, the fake
Vaultwarden and the fake bw: no identity means no membership action;
forged identity headers are ignored; no-account, awaiting-password
(withdrawn, then added once the password is set), accepted, confirmed
and member; reassignment removes only Admin-type members, also at the
start; a personal Environment never removes; a Team Environment does
nothing; SSO off means no account button; the links carry the exact
organization and collection ids; no value, token or address reaches the
logs, the journal, bw's argv or an answer; a workstation's page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/vault-page-view.test.ts states the page's rules on answers of the
server's shapes: answers read only in their exact form; the links by the
vault's exact ids and nowhere else (a 0197 pilot to its collection, a
non-https or non-bare origin to none); a member adds and opens, a person
without an account (or without a master password) gets only the sign-in
and only where the vault takes it; the item count in Czech plural, the
creation line, "Neodpovídá" with the recorded links kept, a gone vault
without a way in; this computer, an Environment without a vault, a 0197
pilot, a Team and an Automated Environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e and its Správci

decisions.md: F11 gains one narrow use of the admission's own answer, the
admitted session's subject (X-Auth-Request-User, a lowercase UUID),
only for the membership of the Environment's own vault, whose access is
access to the Environment (root decision 0198 point 2); never a
repository right, an operator or a role. F53's addendum records
Nastavení → Trezor (the page, its routes, the links), the Správci (add
on presence on personal and work Environments, an invitation never left
pending, removal of every Admin-type member when a work Environment's
assignment changes, no stored list) and the known gaps: Team and
Automated Environments, sharing withdrawn without a reassignment,
INVITATIONS_ALLOWED=true on the network's vault.

hosted-entry.md, environment-access.md, environment-tools.md and
launchpad-development.md say the same where they describe admission,
access, the vault and the Settings routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gap of its own

The empty live region took a slot of the section's 28 px column gap, so
the vault's row stood far below the rule. Inside the head block it keeps
its place for announcements with the block's small gap. Seen in the
browser check of the page (decision 0178).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admission already accepts only a lowercase UUID as the gateway's
subject; the core no longer relies on its caller for that. Anything else
(an uppercase UUID, a login, an address, an empty string) is nobody: no
invitation, no confirmation, only the audience look that needs nobody.
tests/vault-members.test.ts also states that a vault which is not set up
is asked nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… creation is not confirmed

#391 gained the state `creating` (the organization's creation was asked
for and is not confirmed yet; the next setup looks for it again, and the
Launchpad's start runs that setup). The page's row now says the vault is
being created, as for a running setup, with no way in until the vault is
the Environment's; it said "Not available here" before, through a default
branch. The row's words are now an exhaustive switch over the states, so
a state added later fails to compile instead of falling through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… path of the person's address

Review of 2026-10-14 (P1): withdrawing an invitation the vault left
Invited was not failure-safe; a DELETE that failed left a pending
invitation through which anyone who knew the address could register it.
The withdrawal is gone. Instead the Environment invites and confirms only
behind a gate: the network's vault must refuse every password path of the
person's address (SSO_ONLY, the address not exempt).

Why that gate and not "SSO declared": Vaultwarden 1.37.4's send_invite
keeps an invitation row with every invitation to an account without a
master password (SMTP off, whatever INVITATIONS_ALLOWED says), and while a
password can be registered for an address, register takes that row.
Proven against the real 1.37.4-lazurio.1 image: with SSO declared,
INVITATIONS_ALLOWED=false and SIGNUPS_ALLOWED=false (Machines #508's
contract) an invited stub's address registered (HTTP 200); with SSO_ONLY
it did not (HTTP 400).

The vault says which itself: its token endpoint refuses a password grant
naming only the address with "SSO sign-in is required" exactly when
SSO_ONLY covers it, before any account is looked up and before its rate
limit or sign-in events (`signInRequired`, the fixed code `sso-required`
in vaultRequest). Elsewhere, or when the vault does not answer, nobody is
invited or confirmed (`sign-in-not-required`; the page says that only the
agents use the vault); removal needs no gate. Behind the gate an
invitation to an account without a master password stays Invited
(`awaiting-password`); the vault accepts it when the person sets the
password, and the next look confirms. A vault that is not set up is asked
nothing.

Tests: the fake Vaultwarden models SSO_ONLY (password grant and
registration refused for person addresses, exactly Vaultwarden's order)
and an invitation that fails before or after it is made. New and changed
contract tests: a pending invitation kept, unregistrable and confirmed
once the password is set; nobody invited or confirmed with SSO declared
alone or without SSO (an accepted member is not confirmed there); a
failed or interrupted invitation leaves nothing registrable and the next
look goes on from the vault. Mutations: no gate fails 2 tests, a gate on
prevalidate alone fails the SSO-declared case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the network's SSO_ONLY

The Správci are invited and confirmed only where the network's vault
refuses every password path of the person's address (SSO_ONLY), measured
by the vault's own token endpoint; the evidence from the real image
(registrable under Machines #508's settings alone, not under SSO_ONLY);
nobody added until a network runs SSO_ONLY; SSO_ONLY never to be turned
off with pending invitations; the replaced invite-then-withdraw and the
rejected prevalidate-only gate in the alternatives. environment-tools.md
says the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#391 renumbered the Environment's own vault from F52 to F53 when it was
rebased onto main, where F52 is the module generator (#394). This pull
request's references follow it: the addendum of 2026-10-14 is F53's in
decisions.md, hosted-entry.md, environment-access.md, environment-tools.md
and launchpad-development.md (with their anchors), and in the comments of
server.ts, members.ts and the vault page's HTTP tests. No reference to the
module generator's F52 changes.

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.

@immakermatty — APPROVED on adbaafa against main dace17d. Re-review after #391 rebase merge: the new head tree d1756746c6af4b68d6f38ca0f491ff5fb71ffb55 is byte-identical to fa60777; range-diff maps all 11 PR commits one-for-one. The final change corrects vault-decision references to F53 in the F11 amendment, five docs, code comments and the HTTP test; F52 still identifies the module generator, and this PR does not touch src/folder. The earlier security P1 remains resolved by the SSO_ONLY password-path gate. Independent isolated exact-head check: 40 focused tests passed; lint, typecheck, public guard, proof build and smoke passed; git diff --check clean. Exact-head CI run 38574972736 succeeded; no unresolved review threads or competing review requests. The reported local full-suite launchpad-prompts race (#413) is outside this diff; I did not repeat that broad suite. Deployment still depends on keeping SSO_ONLY enabled while invitations remain pending and qualifying the live gateway subject on a canary. No merge performed.

@immakermatty
immakermatty merged commit 1cc2ef6 into main Oct 14, 2026
7 checks passed
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