Skip to content

Access request flow

The access-request flow converts an applicant’s submission on app.steelmoth.ai/request-invite into a row in steelmoth.access_requests, lets an admin review it from the dashboard, and on Approve issues an Authentik invitation bound to the access-enrollment flow. The applicant uses the invitation URL to enrol into the steelmoth-clients group.

The flow exists because the public form was inert (PRD 0041): the marketing site sent submissions nowhere prior to this redo. PRD 0041 + RFC 0078 + ADR 0108 (stage-then-promote) + ADR 0109 (secrets via prontera broker) settle the design. This page documents the broker-only redo — a prior attempt (PRD 0040 / RFC 0077) was reverted by PR #189 because it stored four credentials as local files on the steelmoth VPS, violating INV-024 + HARD RULE 10.

  1. Applicant loads app.steelmoth.ai/request-invite. The route is public in dashboard middleware and renders a route-scoped copy of the marketing request-invite visual design, not a redirect.
  2. Form POSTs JSON to /api/v1/public/access-request through the dashboard proxy. The visible app fields are name, email, and useCase (“What do you want to use Steelmoth for?”). The wire body still includes agentName: null for runtime-contract compatibility and includes turnstileToken.
  3. Runtime parses the body into AccessRequestSubmissionWire. In the current private compose config, STEELMOTH_ACCESS_REQUEST_TURNSTILE_ENABLED=false, so the dashboard form sends a non-empty placeholder token and the runtime uses the noop verifier. The runtime still checks per-IP rate-limit, runs per-email collapse, HMAC-hashes IP/UA (key via secret_value("ACCESS_REQUEST_IP_HASH_SECRET")), inserts a row with status='pending', and returns {ok: true, ref: "ar_<ulid>"}.
  4. Admin opens app.steelmoth.ai/admin/access-requests (dashboard-admins group gated). Approve → POST /v1/admin/access-requests/{id}/approve → Authentik invitation API (token via secret_value("AUTHENTIK_INVITE_TOKEN") → broker). Row flips to status='invited'.
  5. Auto-email (ADR 0119): immediately after the invitation commits, the service best-effort SMTP-sends the URL to the applicant from Moth <…broker-resolved address…> (sender login via secret_value("MOTH_OWN_GOOGLE_EMAIL"), App Password via secret_value("MOTH_OWN_GOOGLE_APP_PASSWORD") → broker → AV client-mark; both aliases currently map to the Raven Google account). Send result is stashed in metadata.invitation_email_sent_at / …_last_error (JSONB) and surfaced in the dashboard as a ✓ Emailed or ⚠ Email failed pill; the admin page’s default filter includes invited rows and shows a page-level alert whenever any listed invite has a recorded email failure, and the runtime logs the dispatch at WARNING on failure (a failed send must never be silent — 2026-07 incident: a dead app password went unnoticed for days). Send failures do not roll back the Authentik invitation — operator can copy the URL manually or click Re-send email (POST /v1/admin/access-requests/{id}/resend-invitation-email) which re-issues the same URL without minting a new Authentik invitation.
  6. Applicant clicks the URL → Authentik resolves the invitation token → access-enrollment flow → password + MFA prompts → user-write stage creates the account in steelmoth-clients → auto-login.

Secret-resolution path (the redo-defining detail)

Section titled “Secret-resolution path (the redo-defining detail)”

All four credentials live in Agent Vault staging on prontera. steelmoth-api resolves them through the existing credentials.py:_BROKER_SECRET_ALIASES extension point:

steelmoth env-name broker alias AV vault AV key
TURNSTILE_SECRET turnstile-secret staging TURNSTILE_SECRET
TURNSTILE_SITE turnstile-site staging TURNSTILE_SITE
AUTHENTIK_INVITE_TOKEN authentik-invite-token staging AUTHENTIK_INVITE_TOKEN
ACCESS_REQUEST_IP_HASH_SECRET access-request-ip-hash-secret staging ACCESS_REQUEST_IP_HASH_SECRET
MOTH_OWN_GOOGLE_EMAIL moth-own-google-email client-mark MARK_RAVEN_GOOGLE_EMAIL
MOTH_OWN_GOOGLE_APP_PASSWORD moth-own-google-app-password client-mark MARK_RAVEN_GOOGLE_APP_PASS

Each runtime call to secret_value("…") returns the broker-resolved value (cached up to 60 s in _BROKER_SECRET_CACHE). The steelmoth-api session validates against the client-mark vault; the broker uses its own identity (mark-steelmoth-secret-broker) to fetch from staging. Zero secret files on the steelmoth VPS. Zero *_FILE env vars. Zero new compose secrets: block entries.

When the marketing-site form is deployed, TURNSTILE_SITE is fetched at deploy time by scripts/deploy-marketing-site.sh (broker GET → NEXT_PUBLIC_TURNSTILE_SITE_KEY env → build output). The app-hosted /request-invite page does not currently mount Turnstile because the runtime verification path is disabled server-side; a real app Turnstile widget would also need a public dashboard site-key configuration and runtime verification to be enabled.

Claim Area Source
Product intent, goals docs/prds/0041-public-access-request-waitlist.md
Technical design docs/rfcs/0078-access-request-broker-resolved-secrets.md
Stage-then-promote decision docs/adr/0108-access-request-stages-in-steelmoth-promotes-to-authentik-on-approval.md
Broker-only secret architecture docs/adr/0109-access-request-secrets-resolve-via-prontera-broker.md
Prontera broker anchor docs/adr/0105-secret-broker-on-prontera.md
Schema memlink-hybrid/migrations/0051_access_requests.up.sql
Domain types steelmoth-runtime/src/steelmoth_runtime/access_requests/domain.py
Service steelmoth-runtime/src/steelmoth_runtime/access_requests/service.py
Outbound adapters steelmoth-runtime/src/steelmoth_runtime/access_requests/{postgres_repository,authentik_client,turnstile,rate_limit}.py
Broker extension steelmoth-runtime/src/steelmoth_runtime/credentials.py (lines around _BROKER_SECRET_ALIASES)
Wire contracts steelmoth-runtime/src/steelmoth_runtime/web_chat/contracts_access_requests.py
FastAPI routes steelmoth-runtime/src/steelmoth_runtime/web_chat/routes_access_requests.py
Dashboard panel dashboard/app/admin/access-requests/page.tsx
Sidebar dashboard/components/layout/sidebar-nav.ts (Advanced > Operations > Access Requests)
Public app form dashboard/app/request-invite/page.tsx; dashboard/app/request-invite/_components/RequestAccessForm.tsx; dashboard/app/request-invite/RequestInvite.module.css
Marketing-site deploy scripts/deploy-marketing-site.sh
Prontera broker prontera-assets/agent-vault-secret-broker/README.md (reference for alias-JSON format)
  • web-chat-api.md — overall route inventory; access-request routes mounted from app.py:build_app().
  • dashboard.md — admin section pattern + sidebar conventions.
  • security-and-secrets.md — the prontera broker is the canonical secret resolution path; this feature uses it for four credentials.
  • data-and-scope.mdsteelmoth.access_requests follows operator-global RLS pattern.
  • deployment.md — dashboard via scripts/compose-dashboard-deploy.sh; marketing via scripts/deploy-marketing-site.sh (new with this redo).
  • mail-broker.mdfeature_flag.access_request_auto_email flips delivery to the mail broker; default off.
  • Update this page when access-request endpoints, schema, Authentik flow slug, dashboard panel shape, or the app-hosted request-invite form changes.
  • The four AV staging credentials + matching broker aliases are operator-managed on prontera; changes there are not reflected by code diffs and must be recorded here when they happen.
  • Adding a new _BROKER_SECRET_ALIASES entry on steelmoth requires a matching entry in /etc/agent-vault-secret-broker/env on prontera + a broker restart. The PR description must reference both diffs.
  • Rotating an AV credential propagates to steelmoth-api within the broker’s 60 s cache TTL; restart the broker on prontera to flush immediately.
  • Retention policy for status='rejected' and status='expired' rows is not yet defined.
  • Mail-broker auto-email path is scaffold-only; its end-to-end behavior is unverified until the flag flips.
  • Whether the AV credentials API supports scripted set (vs UI-only proposal) — discovered during Phase 2 of the redo deploy.
  • The marketing-site source currently lives outside this repo, while this wiki is repo-source grounded. Marketing link changes must be verified against that sibling source or live output when they are made.