docs: establish canonical documentation architecture (#1210)
ci/woodpecker/push/publish Pipeline failed

This commit was merged in pull request #1210.
This commit is contained in:
2026-08-13 17:56:13 +00:00
parent f82307c4dc
commit 7a6fb024b4
241 changed files with 4722 additions and 1579 deletions
+50
View File
@@ -0,0 +1,50 @@
# Administrator Guide
> **Status:** Partially migrated. Current SSO and local upgrade/recovery procedures are available; held procedures are labeled non-operative.
This book is the canonical home for installation, configuration, deployment, routine operations, security controls, incident response, and recovery. User workflows belong in [`USER-GUIDE/`](../USER-GUIDE/); implementation and contributor material belongs in [`DEVELOPER-GUIDE/`](../DEVELOPER-GUIDE/).
## Start here
- [Documentation atlas](../README.md) — placement rules and source-of-truth boundaries.
- [Documentation sitemap](../SITEMAP.md) — resolvable current navigation and authority-gated migration summary.
- [Product requirements](../PRD.md) — normative requirements, currently marked draft.
- [Operations index](operations/README.md) — current local procedures and explicitly held operational outlines.
- [Security index](security/README.md) — current SSO provider configuration.
## Chapter map
| Chapter | Scope | Status |
| ------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `installation/` | Prerequisites, installation, and first deployment. | Scaffold only. |
| `configuration/` | Environment, provider, tier, and runtime configuration. | Scaffold only. |
| `deployment/` | Topologies, rollout, migration, and upgrade procedures. | Scaffold only. |
| [`operations/`](operations/README.md) | Health, observability, routine operation, and maintenance. | Local upgrade/recovery is current; connector lease operations are held. |
| [`security/`](security/README.md) | Authentication, authorization, SSO, secrets, and security controls. | SSO provider guide is current; other pages are planned. |
| `recovery/` | Incident response, backup, rollback, and recovery. | Scaffold only. |
Every promoted page must be added to this index and to [`SITEMAP.md`](../SITEMAP.md) in the same migration slice.
## Evidence — not current operator guidance
- [`P8-003 performance report`](../reports/qa/p8-003-performance-optimization.md) — historical performance evidence; implementation alignment is partial, and production metrics remain unverified. It is not an operational SLO or runbook.
## Migration backlog — not current operator guidance
These are source candidates, not verified runbooks:
- `_old_structure/guides/admin-guide.md` — quarantined historical source; verify claims before promotion.
- `_old_structure/guides/deployment.md` — quarantined historical source; deployment commands and assumptions remain held.
- See the [documentation catalog](../reports/documentation/2026-08-10-docs-catalog-audit.md) for file-level dispositions.
Do not treat a migration candidate as current until its commands, paths, permissions, and safety status are checked against source and tests.
## Authoring boundary
New administrator documentation belongs under one of the chapter directories above. Operationally sensitive pages must identify prerequisites, ownership, source-of-truth dependencies, and whether any procedure is current, illustrative, held, or non-operative.
## Related
- [[README|Documentation contract]]
- [[PRD|Product requirements]]
- [[DEVELOPER-GUIDE/architecture/README|Architecture index]]
+19
View File
@@ -0,0 +1,19 @@
# Administrator Operations
> **Status:** Partially migrated. Procedures explicitly identify whether they are current or held.
## Current procedures
- [Upgrade safety and recovery](upgrade-safety-and-recovery.md) — installed-CLI and local-PGlite upgrade, rollback, and framework-configuration recovery.
## Held procedures
- [Mos connector lease operations](mos-connector-lease-operations.md) — non-operative M1 outline while the gateway policy remains deny-all and no connector is activated.
A held page is architecture and readiness context, not command authority. PostgreSQL, federated, bare-metal, Compose, Gateway/Web activation, and migration-runner procedures remain outside the current local route unless a later page explicitly removes the hold with verified evidence.
## Related
- [[ADMIN-GUIDE/README|Administrator guide]]
- [[ADMIN-GUIDE/security/README|Administrator security]]
- [[DEVELOPER-GUIDE/architecture/README|Architecture index]]
@@ -0,0 +1,79 @@
# Mos Connector Lease Operations — M1
> **Status:** Held / non-operative.
> **Operational authority:** None. This page is a future runbook outline, not a current command, endpoint, migration, or activation procedure.
> **Hold condition:** The gateway's `DenyConnectorLeasePolicy` remains the default policy and rejects every lease and grant operation. No connector is activated by this page.
## Current state
M1 provides an implemented lease, fencing, audit, and gateway policy boundary. It does not currently provide an operator-facing lease endpoint, activate a connector, cut over a channel, or connect an existing runtime provider to `ConnectorExecutionContext`. The default gateway module is deny-all, so operators must not treat the schema or internal service as an available lease-control surface.
There is no current operator command sequence to acquire, renew, take over, release, or grant connector authority. Do not attempt to operate the held procedure through direct database writes or by bypassing the gateway policy. Any future activation requires a separately approved server-side policy, concrete adapter, downstream fencing design, and an updated operational runbook.
## Held future procedure — not current command authority
The following records the intended shape of a later runbook. It is deliberately non-operative while deny-all remains:
### Events and evidence to monitor later
If an authorized policy and adapter are activated in a future work package, correlation IDs may be used to inspect `connector_lease_audit_log` events:
| Event | Meaning |
| ---------- | ---------------------------------------------------------------------------------- |
| `acquire` | First holder inserted for an unused binding |
| `renew` | Current holder heartbeat extended the TTL |
| `takeover` | Authorized compare-and-swap replaced the holder and incremented epoch |
| `release` | Current holder explicitly relinquished authority |
| `expiry` | An expired current lease was observed |
| `reject` | Policy, compare-and-swap, expiry, scope, or fencing validation denied an operation |
Audit records are metadata-only. Raw grant objects, connector payloads, scopes, tokens, approval references, and credentials must never be added to audit output. The current schema does not independently enforce append-only storage; database-level protection remains a future hardening requirement.
### Held incident-review outline
For a future suspected duplicate or stale connector effect, an approved operator procedure would:
1. correlate the attempted operation with its `reject`, `takeover`, or `expiry` event;
2. compare the durable row's connector ID, lease UUID, epoch, expiry, and release time with the adapter's normalized execution context;
3. treat an old epoch, old lease UUID, expired lease, or released lease as non-authoritative rather than retrying it as the old holder;
4. use only the authorized takeover path with the observed expected epoch, never ordinary acquire, for an expired or released row; and
5. preserve evidence without assuming lease fencing provides exactly-once replay safety if an external effect may already have occurred.
These are held review requirements, not instructions to bypass the current deny-all policy.
### Held migration and rollback notes
The checked-in `0016_salty_morlocks.sql` artifact is additive: it creates the lease and audit tables and indexes without changing existing authorization/session tables. A future database rollout would still require the repository's approved migration, backup, verification, and rollback controls. Application rollback would leave additive lease/audit tables in place; dropping them would destroy evidence and is not an automatic rollback step.
This page does not authorize running migrations, connecting to PostgreSQL, initializing PGlite, or starting a connector. Those activities remain outside this held procedure and subject to repository/runtime gates.
### Held security prerequisites
Before this page could become operative, the activation work would need to demonstrate at least:
- tenant authority derived from authenticated gateway context, not connector request fields;
- authorized policy decisions over normalized identity and scopes, with policy TTL handling based on the requested input and coordinator hard caps still applied;
- explicit takeover authorization and expected-epoch compare-and-swap;
- application-boundary normalization plus any separately approved database constraints or protections;
- validation and rejection audit before adapter side effects;
- a concrete adapter that consumes and propagates the normalized epoch/context; and
- existing authorization and exact-action approval controls remaining in force.
M1 currently satisfies the boundary contract and default-deny posture, not these activation prerequisites.
## Explicit non-goals while held
This page does not authorize or claim:
1. lease administration from the dashboard, CLI, HTTP, SQL console, or a connector;
2. production connector or channel activation;
3. exactly-once delivery, side-effect journaling, checkpoint/handoff recovery, or replay safety;
4. automatic failover, rollback, or stale-effect recovery across Claude, Pi, Codex, Matrix, tmux, or provider sessions; or
5. database-enforced audit immutability or database-enforced application normalization.
The page may be promoted to an operative runbook only after deny-all is intentionally replaced, concrete adapters are reviewed, activation evidence exists, and this hold is explicitly removed by the owning work package.
## Related contract
- [M1 logical identity and fencing decision](../../DEVELOPER-GUIDE/architecture/decisions/mos-runtime-portability-m1.md)
- [MOS-PORT requirements](../../PRD.md#mos-runtime-portability-workstream-mos-port)
@@ -0,0 +1,248 @@
# Upgrade safety and recovery
> **Supported route:** an already installed `mosaic` CLI using the local PGlite
> configuration. This is a filesystem and CLI runbook; it does not activate a
> service or connect to a database.
This page covers the supported upgrade, rollback, health, and recovery checks for
Mosaic framework configuration under `MOSAIC_HOME`. It deliberately does not
turn the repository's deployment or PostgreSQL material into an operative
procedure.
## Support boundary
Use this runbook only when all of the following are true:
- `mosaic` resolves to the installed Mosaic CLI (`command -v mosaic`).
- The active application configuration is the local tier: `tier: "local"`,
`storage.type: "pglite"`, and `queue.type: "local"`.
- `DATABASE_URL` is unset. An inherited PostgreSQL DSN is outside this route and
must be removed before continuing.
- `MOSAIC_STORAGE_TIER` is unset or `local`; standalone and federated overrides
are outside this route.
- No Gateway, Web, Compose, or other service activation is required.
The following routes are **held/non-operative** in this guide:
| Route or operation | Status |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| PostgreSQL-backed `standalone` storage | Held; do not activate or probe it here. |
| Federated storage and peers | Held; do not activate or probe it here. |
| Bare-metal deployment | Held; no service installation or lifecycle action is authorized. |
| Gateway/Web activation or HTTP health checks | Held; `/health`, `/health/ready`, and `mosaic gateway ...` are not evidence for this route. |
| Compose startup | Held; do not start a Compose profile or the full stack. |
| Migration runners and tier migration | Held; do not run `mosaic-db-migrator`, `pnpm --filter @mosaicstack/db db:migrate`, `mosaic storage migrate`, or `mosaic storage migrate-tier`. |
A command being present in the CLI does not make a held route operative.
## Storage and path terminology
Keep these locations separate:
- **`MOSAIC_HOME`** — the framework/operator configuration directory. It
defaults to `~/.config/mosaic` and can be overridden with `MOSAIC_HOME`.
- **PGlite data** — the local, in-process database. The checked-in local config
uses `.mosaic/storage-pglite` as its storage `dataDir`; `.mosaic/queue` is the
local queue directory. These are project data, not framework configuration.
- **Durable upgrade snapshots** — operator-file snapshots stored under
`${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/backups/`. They are outside
`MOSAIC_HOME` and do not contain a PostgreSQL dump.
The configuration tier is named `local`; the storage CLI reports the backend as
`pglite`. PGlite is not PostgreSQL and does not require a PostgreSQL server.
Local PGlite schema setup is adapter-owned; this page does not authorize a
separate migration runner.
## Pre-upgrade health gate
Run these checks from a shell that has no inherited database DSN:
```bash
command -v mosaic
mosaic --version
mosaic config path
test -d "$(mosaic config path)"
env -u DATABASE_URL -u MOSAIC_STORAGE_TIER mosaic storage tier show
env -u DATABASE_URL -u MOSAIC_STORAGE_TIER mosaic storage status
mosaic restore --list
```
For the supported route, the storage checks should report the `pglite` backend
and say that no network check is needed. `mosaic storage status` reports
`PGLITE_DATA_DIR` when that variable is set; otherwise it reports the CLI's
`:memory:` fallback. That output is an inspection of the CLI environment, not a
claim that PGlite contents are healthy or durable.
If `DATABASE_URL` is set, stop. Do not point it at a local PostgreSQL instance
to make the check pass. If the active configuration is not local/PGlite, stop;
no operative route is documented here.
The supported health gate is intentionally limited to CLI resolution, framework
configuration path, local storage selection, and available upgrade snapshots.
It does not prove Gateway/Web readiness, provider connectivity, queue health, or
PGlite data integrity.
## Safe upgrade procedure
1. **Record the baseline.** Save the output of `mosaic --version`,
`mosaic config path`, and `mosaic restore --list`. Do not copy secrets into a
ticket or report.
2. **Check for updates without installing them:**
```bash
mosaic update --check
```
Exit status `0` means no update was reported; status `2` means an update is
available. Other failures are not a successful health result. The check is a
package-registry check and does not start Mosaic services or access storage.
3. **Run the installed-CLI upgrade when approved:**
```bash
mosaic update
```
The command updates the installed `@mosaicstack/*` packages through npm and,
when the framework package changed or framework drift is detected, re-seeds
framework files in keep mode. Leave the default re-seed enabled. Do not use
`--relaunch` in this runbook: that option can restart durable fleet agents
and is outside the supported no-activation route. `--no-reseed` is also not a
normal upgrade path because it intentionally leaves framework files stale.
4. **Repeat the health gate.** Confirm the CLI version, the same
`MOSAIC_HOME`, the local/PGlite storage selection, and the snapshot listing.
A successful framework upgrade must not be used as evidence that a held
Gateway, Web, PostgreSQL, or federated route is ready.
Do not replace this procedure with a direct edit of `~/.config/mosaic`, a
repository `tools/install.sh` invocation, a Compose startup, or a migration
command. The supported operator entry point for this page is the installed
`mosaic update` command.
## What an upgrade protects
For an existing keep-mode installation, the framework installer takes two
separate snapshots before the file sync:
1. An ephemeral whole-directory snapshot under `${TMPDIR:-/tmp}/` is used to
restore the previous framework directory if the sync is interrupted or
fails. If that restore cannot complete, the installer prints the retained
temporary snapshot path for manual recovery.
2. A durable snapshot captures the existing operator-owned files before the
upgrade at:
```text
${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/backups/pre-update-<UTC timestamp>/
```
Snapshot directories are mode `0700`; captured files are mode `0600`.
Retention is five snapshots by default and can be changed with
`MOSAIC_BACKUP_RETENTION`. A durable snapshot failure is a warning and does
not replace the manifest and crash-rollback protections.
After the keep-mode sync, the installer compares each captured operator file
with its target. If a file was changed or removed unexpectedly, it restores the
snapshot copy and emits a warning. A symlinked parent is not followed; that
case is left for manual recovery from the snapshot path.
Keep mode is manifest-driven and fail-safe for operator paths, but it does not
mean every file under `MOSAIC_HOME` is user-owned. The framework contract files
`CONSTITUTION.md`, `AGENTS.md`, and `STANDARDS.md` are refreshed by upgrades;
keep local policy in the supported local overlays rather than editing those
contract files as rollback data. A durable snapshot is for operator-owned
configuration, not for the installed npm package, framework-owned contracts, or
PGlite data.
## Rollback and recovery
### If the upgrade fails or is interrupted
The framework sync attempts an automatic rollback from its ephemeral snapshot.
Do not delete `MOSAIC_HOME`, the PGlite data directory, or the temporary snapshot
named in the error. Preserve the command output, then run the health gate.
If the installed framework reports that the automatic restore did not complete,
use the durable snapshot procedure below. The durable snapshot is the recovery
pointer that survives a successful upgrade and the removal of the temporary
snapshot.
### Restore operator configuration from a durable snapshot
`mosaic restore` is confirmation-gated and reports counts and relative paths,
not file contents. First list snapshots, then preview the selected timestamp:
```bash
mosaic restore --list
mosaic restore --from <UTC-timestamp> --dry-run
```
If the preview is correct, run the interactive restore:
```bash
mosaic restore --from <UTC-timestamp>
```
Use `--yes` only when the overwrite has been explicitly approved:
```bash
mosaic restore --from <UTC-timestamp> --yes
```
The timestamp is the value printed by `mosaic restore --list`; the command also
accepts the full `pre-update-<timestamp>` name. For a non-default configuration
home, pass the same target explicitly:
```bash
mosaic restore --mosaic-home "$MOSAIC_HOME" --from <UTC-timestamp>
```
After restoring, repeat the health gate. Restore writes only the operator
surface represented by that snapshot. It does not downgrade the installed CLI,
restore framework-owned contract files, restore `.mosaic/storage-pglite`, or
run any migration.
### If local PGlite data is missing or corrupt
Do not use a framework snapshot as a database backup. Do not run a migration
runner, start PostgreSQL, start Compose, or activate Gateway/Web to investigate.
The current CLI does not provide a wired PGlite export/import operation;
`mosaic storage export` and `mosaic storage import` only print direct-copy
guidance. Preserve the configured PGlite data directory and route data
restoration through the held data-layer procedure rather than improvising a
recursive delete or copy.
### Recovery decision tree
- **CLI or framework path is wrong:** stop, verify `command -v mosaic`,
`mosaic --version`, `MOSAIC_HOME`, and `mosaic config path`; do not activate a
held service route.
- **Upgrade failed during framework sync:** use the installer's automatic
rollback first; if it reports incomplete recovery, list and preview the
durable snapshot, then restore it interactively.
- **An operator file changed after a reported-successful upgrade:** select the
pre-upgrade snapshot with `mosaic restore --list`, preview it with `--dry-run`,
and restore only after reviewing the relative-path plan.
- **PGlite data is affected:** preserve the data directory and stop at the
boundary described above. Framework rollback cannot recover database rows.
- **Output requests PostgreSQL, federated, bare-metal, Compose, Gateway/Web, or
a migration runner:** stop; that is a held/non-operative route, not a next
command for this runbook.
## Post-recovery verification
Run the non-mutating checks again:
```bash
mosaic --version
mosaic config path
env -u DATABASE_URL -u MOSAIC_STORAGE_TIER mosaic storage tier show
env -u DATABASE_URL -u MOSAIC_STORAGE_TIER mosaic storage status
mosaic restore --list
```
A green result here means the installed CLI resolves, the framework path is
present, the CLI selects local PGlite without a network probe, and snapshots can
be enumerated. It does not certify database contents or any held deployment
route.
+20
View File
@@ -0,0 +1,20 @@
# Security
> **Status:** Partially migrated. The SSO provider and Discord ingress security pages are current.
This chapter will contain authentication, authorization, SSO, secrets, RBAC, and security-control guidance for administrators.
## Planned pages
- [`sso-providers.md`](sso-providers.md) — current provider configuration, discovery, callbacks, and failure modes.
- [`discord-ingress.md`](discord-ingress.md) — current Discord service authentication, allowlists, bindings, roles, replay, and failure controls.
- `secrets.md` — document general secret handling after source/configuration verification.
- `rbac.md` — document roles and permissions from the canonical implementation.
The current SSO page reflects dynamic provider discovery. The Discord page documents only the verified Discord compatibility boundary; it does not claim Telegram or Matrix parity. Do not revive retired root documents or add frontend feature flags that the web flow does not consume.
## Related
- [`Administrator guide`](../README.md)
- [`API documentation`](../../API/README.md)
- [[DEVELOPER-GUIDE/architecture/README|Architecture index]]
@@ -0,0 +1,137 @@
# Discord ingress security
> **Status:** Current Discord behavior only. Telegram shared-contract parity, Matrix channel ingress, and a gateway-wide shared adapter registry are not implemented or are not proven by the current source/tests.
>
> **Last verified:** 2026-08-10 against the Discord plugin, gateway ingress/authentication code, and focused tests linked in [Evidence](#evidence).
>
> **Audience:** Administrators provisioning the Discord remote-control boundary.
This page documents the security boundary that exists today. It is not a deployment recipe for Telegram or Matrix, and it does not turn the gateway's lifecycle plugin list into a universal channel registry.
## Security model
Discord ingress has two current layers:
1. **Native Discord admission** in `@mosaicstack/discord-plugin` applies guild/channel/user allowlists, pairing, role, rate, and thread rules before a thread is created or a message is dispatched.
2. **Gateway compatibility admission** authenticates the Discord Socket.IO service, verifies the signed envelope again, re-checks the allowlists and binding, validates the conversation route, rejects replayed native message IDs, and then dispatches the message to the trusted agent configuration.
The current gateway namespace is `/chat`. The Discord plugin connects with a Socket.IO handshake value named `discordServiceToken`; this is distinct from the environment variable name `DISCORD_SERVICE_TOKEN` that supplies the value to the plugin and gateway.
### Admission and authorization order
For an inbound guild message, the current implementation:
1. Ignores bot-authored messages and messages without a guild. Discord DMs are therefore not handled by this ingress path, even though the client requests a direct-message intent.
2. Uses the configured thread parent as the authorization channel for a thread. A normal Discord category parent is never substituted for a text channel.
3. Requires the guild, authorization channel, and user to appear in their respective allowlists.
4. Resolves a configuration-owned binding and paired user. `viewer` cannot send a turn. Ordinary `send` requires `operator` or `admin`; `approve` and `stop` require `admin`.
5. Applies the message and mention-thread rate limits before any thread creation or gateway dispatch.
6. Derives the route from the binding's logical-agent instance and the response channel/thread. The route does not accept a provider, model, harness, process, or runtime-session selector from Discord.
7. Creates or reuses a thread only after the checks above pass.
The gateway then verifies the HMAC-SHA-256 envelope with `DISCORD_SERVICE_TOKEN`, re-applies the allowlists and binding/role check, requires the conversation ID to match the bound logical agent and channel/thread, and claims the native Discord message ID in a bounded replay cache. The default replay cache is in-process, retains IDs for 15 minutes, and is bounded at 10,000 entries; it is not a durable inbox.
For ordinary chat, the gateway attempts persistence and dispatch using `DISCORD_SERVICE_USER_ID`; `DISCORD_SERVICE_TENANT_ID` is used when configured and otherwise ordinary chat falls back to the service user ID as its tenant. The Discord external route is not a UUID, while persisted conversation IDs are UUIDs; no current route-to-UUID mapping proves that ordinary Discord persistence succeeds. The gateway may continue dispatch after a persistence/binding failure, so successful live delivery is not durable-history evidence.
Privileged approval and stop additionally require a configured tenant, a paired `mosaicUserId`, and a previously enrolled durable session. The durable session's logical agent must match the binding before approval or stop is accepted. The approval is consumed once against the exact runtime target; the Discord service account is not substituted for the approving paired user.
The trusted `agentConfigId` in each binding is resolved by the gateway. Its provisioned agent name must exactly equal `instanceId`. Discord cannot choose an arbitrary agent, provider, or model in the message payload, and Discord ingress does not use the general routing engine for a new session.
## Required configuration
The gateway's current plugin factory is in [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts). When `DISCORD_BOT_TOKEN` is present, the following Discord values are required or validated as shown:
| Name | Required/current behavior |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DISCORD_BOT_TOKEN` | Enables the Discord plugin and supplies the Discord bot credential. |
| `DISCORD_SERVICE_TOKEN` | Required when the bot is enabled. Authenticates the Socket.IO service handshake and signs/verifies ingress envelopes. Treat as a high-entropy secret. |
| `DISCORD_SERVICE_USER_ID` | Required when the bot is enabled. Provisioned Mosaic service principal used for ordinary Discord dispatch and attempted persistence; durable history is not guaranteed. |
| `DISCORD_SERVICE_TENANT_ID` | Not required to start ordinary Discord chat, but required for the `/approve` and `/stop <approval>` control path. Use the provisioned tenant for the service boundary. |
| `DISCORD_GATEWAY_URL` | Base gateway URL. The plugin connects to `${DISCORD_GATEWAY_URL}/chat`; the gateway factory default is `http://localhost:14242`. |
| `DISCORD_GUILD_ID` | Optional guild ID used only by the current project-channel provisioning helper. It is not the message authorization allowlist. |
| `DISCORD_ALLOWED_GUILD_IDS` | Required, comma-separated guild IDs. Empty or missing values fail closed during plugin creation. |
| `DISCORD_ALLOWED_CHANNEL_IDS` | Required, comma-separated parent text-channel IDs. Thread messages are checked against their configured parent. |
| `DISCORD_ALLOWED_USER_IDS` | Required, comma-separated Discord user IDs. This allowlist is checked in addition to `pairedUsers`. |
| `DISCORD_INTERACTION_BINDINGS` | Required, non-empty JSON array of configuration-owned bindings. Malformed or empty data fails plugin creation. |
| `DISCORD_MESSAGE_RATE_LIMIT_PER_MINUTE` | Optional positive integer; default is `30` authorized turns per guild/channel/user window. Zero, negative, and non-integer values are rejected. |
| `DISCORD_THREAD_RATE_LIMIT_PER_MINUTE` | Optional positive integer; default is `5` mention-triggered thread routes per guild/channel/user window. Invalid values are rejected. |
`MOSAIC_AGENT_NAME` and `MOSAIC_AGENT_CONFIG_ID` are not substitutes for a Discord binding. The current Discord binding uses `instanceId` and `agentConfigId` inside `DISCORD_INTERACTION_BINDINGS`; do not invent a different environment-based routing contract.
### Binding shape
Use placeholders for identifiers and keep credentials out of the JSON:
```json
[
{
"instanceId": "interaction-agent",
"agentConfigId": "provisioned-agent-config-id",
"guildId": "guild-id",
"channelId": "parent-channel-id",
"pairedUsers": {
"discord-user-id": {
"role": "operator",
"mosaicUserId": "provisioned-mosaic-user-id"
}
}
}
]
```
Each binding requires `instanceId`, `agentConfigId`, `guildId`, `channelId`, and a non-empty `pairedUsers` object. Pairing roles are `viewer`, `operator`, and `admin`. A role-only pairing remains accepted for ordinary non-privileged compatibility, but it has no `mosaicUserId` and cannot authorize the privileged approval/stop path. The guild and parent channel must also be present in their allowlists.
The bot needs permission to view and send messages in the configured channels and to create and send public threads. A category parent is not an authorization boundary. A thread inherits authorization only from its configured parent text channel.
### Secret handling
Supply `DISCORD_BOT_TOKEN` and `DISCORD_SERVICE_TOKEN` through the approved runtime secret mechanism. Do not commit them, put them in binding JSON, or pass them on a command line.
The current `mosaic gateway config --set KEY=VALUE` implementation writes the gateway `.env` file and prints the value in its confirmation; its mask list does not include `DISCORD_SERVICE_TOKEN`. Do **not** use that command for the service token. `mosaic gateway config --edit` exists for local configuration, but production secret provisioning must remain outside the repository and follow the approved secret path.
## Applying configuration safely
These are the current CLI commands exposed by `@mosaicstack/mosaic`; they manage the gateway daemon and do not constitute a Discord protocol:
```bash
mosaic gateway install
mosaic gateway config --edit
mosaic gateway status
mosaic gateway verify
mosaic gateway restart
mosaic gateway logs --lines 50
```
The daemon reads its environment from `~/.config/mosaic/gateway/.env` by default; `MOSAIC_GATEWAY_HOME` can change that home. `mosaic gateway config --set KEY=VALUE` and `--unset KEY` are also implemented for non-secret values. After changing Discord configuration, restart the gateway so the plugin factory is rebuilt. `mosaic gateway status` and `mosaic gateway verify` check the gateway daemon/health surfaces; the current lifecycle host does not expose a channel-specific `healthAll` command, so a green gateway check alone is not proof that Discord is connected.
## Failure and abuse behavior
| Condition | Current result |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Missing service token/user, allowlist, or interaction bindings when Discord is enabled | Gateway plugin creation fails rather than enabling an unconfigured remote-control surface. |
| Invalid optional rate limit | Plugin creation fails; values must be positive integers. |
| Unallowlisted guild/channel/user, unpaired user, or insufficient role | Message is ignored before thread creation and gateway dispatch. |
| Mentioned message cannot create or fetch its requested thread | The message is not dispatched because its response target cannot be honored. |
| Invalid HMAC, malformed envelope, route mismatch, wrong binding, or replayed native message ID | Gateway rejects the ingress without dispatch. |
| Unsafe attachment metadata or URL | Gateway rejects the message before acknowledgement/dispatch. Current bounds include at most 10 attachments, HTTPS URLs without credentials, query strings, or fragments, and bounded ID/name/URL/metadata lengths. |
| Missing `DISCORD_SERVICE_TENANT_ID` for approval/stop | The privileged control handler returns without creating or consuming an approval. |
| Agent configuration ID does not resolve or its name differs from `instanceId` | Gateway refuses to create the Discord-bound session. |
| External route cannot be persisted as a UUID conversation | Gateway may still dispatch live output; durable history and restart/resume continuity are not guaranteed and must not be inferred from delivery. |
## Explicitly not current
- **Telegram:** `TELEGRAM_BOT_TOKEN` and `TELEGRAM_GATEWAY_URL` can instantiate the raw legacy Telegram plugin, but that plugin does not use the shared channel DTOs, Discord-style service authentication, allowlists, pairing, route validation, or a tested gateway security boundary. Do not treat these variables as a secured Telegram equivalent of the Discord configuration above.
- **Matrix:** No current gateway channel adapter, binding, authentication path, or focused channel test establishes Matrix ingress. Matrix-related fleet/runtime code is not evidence of a Matrix channel deployment procedure.
- **Shared registry parity:** The current gateway `PLUGIN_REGISTRY` hosts lifecycle wrappers (`name`, `start`, `stop`, and optional project provisioning). It does not expose a universal channel health/ingress/egress registry. That is follow-up work, not an administrator capability today.
## Evidence
- [`plugins/discord/src/index.ts`](../../../plugins/discord/src/index.ts) — Discord allowlists, bindings, roles, thread routing, signed envelope, typed ingress/egress, limits, retry, and health.
- [`plugins/discord/src/index.test.ts`](../../../plugins/discord/src/index.test.ts) — authorization ordering, thread behavior, attachments, stable routes, egress, rate limits, and health.
- [`apps/gateway/src/chat/chat.gateway.ts`](../../../apps/gateway/src/chat/chat.gateway.ts) — `/chat` authentication, envelope validation, replay, trusted agent selection, ordinary dispatch, approval, and stop.
- [`apps/gateway/src/chat/chat.gateway-auth.ts`](../../../apps/gateway/src/chat/chat.gateway-auth.ts) — timing-safe service-token and BetterAuth session validation.
- [`apps/gateway/src/plugin/discord-ingress.security.spec.ts`](../../../apps/gateway/src/plugin/discord-ingress.security.spec.ts) — signature, allowlist, replay, attachment, binding, approval, stop, and logical-agent checks.
- [`apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts`](../../../apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts) — control-flow evidence with explicit durable-session pre-enrollment, not proof of ordinary Discord persistence.
- [`packages/mosaic/src/commands/gateway.ts`](../../../packages/mosaic/src/commands/gateway.ts) and [`gateway/config.ts`](../../../packages/mosaic/src/commands/gateway/config.ts) — verified gateway CLI command names and configuration behavior.
- [Channel protocol architecture](../../DEVELOPER-GUIDE/architecture/channel-protocol.md) — canonical shared-contract and parity boundary.
- [Discord conversation workflow](../../USER-GUIDE/workflows/discord-conversations.md) — end-user behavior.
+192
View File
@@ -0,0 +1,192 @@
---
title: SSO Providers
type: runbook
audience: admin
status: current
source_of_truth: false
---
# SSO Providers
Configure optional enterprise single sign-on for Mosaic Stack through Better Auth's generic OAuth integration. The gateway owns provider configuration and discovery; the web application renders only providers reported by the gateway.
> **Current behavior:** Authentik, WorkOS, and Keycloak are supported in the checked-in implementation. Authentik and WorkOS use OIDC. Keycloak supports OIDC and an optional direct SAML login URL. The web application does **not** read `NEXT_PUBLIC_WORKOS_ENABLED` or `NEXT_PUBLIC_KEYCLOAK_ENABLED`; provider buttons are discovered dynamically from the gateway.
## Prerequisites
Before configuring a provider, establish these gateway settings:
- `BETTER_AUTH_URL` — the public base URL used to construct OAuth callback URLs.
- `BETTER_AUTH_SECRET` — a strong secret for Better Auth sessions and tokens.
- `GATEWAY_CORS_ORIGIN` — the web origin or comma-separated origins allowed by the gateway.
Keep client secrets and Better Auth secrets in the deployment secret store. Do not commit them to `.env` files or expose them to the web bundle.
## Provider configuration
### Authentik OIDC
Set all three variables to enable Authentik:
```bash
AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic
AUTHENTIK_CLIENT_ID=...
AUTHENTIK_CLIENT_SECRET=...
```
The implementation derives OIDC discovery and endpoint URLs from `AUTHENTIK_ISSUER`. An optional team-claim label can be exposed in provider discovery:
```bash
AUTHENTIK_TEAM_SYNC_CLAIM=groups
```
The default reported claim is `groups`. The discovery payload reports this claim; verify any downstream membership-sync behavior separately before treating it as an authorization guarantee.
Register this redirect URI with the Authentik application:
```text
{BETTER_AUTH_URL}/api/auth/oauth2/callback/authentik
```
### WorkOS OIDC
Set all three variables to enable WorkOS:
```bash
WORKOS_ISSUER=https://your-company.authkit.app
WORKOS_CLIENT_ID=client_...
WORKOS_CLIENT_SECRET=...
```
Use the WorkOS AuthKit issuer or custom authentication domain, not a raw WorkOS REST API hostname. Mosaic derives the OIDC discovery URL by appending `/.well-known/openid-configuration` to the issuer. WorkOS uses PKCE and issuer validation in the current auth configuration.
An optional team-claim label can be exposed in provider discovery:
```bash
WORKOS_TEAM_SYNC_CLAIM=organization_id
```
The default reported claim is `organization_id`. The discovery payload reports this claim; verify any downstream membership-sync behavior separately before treating it as an authorization guarantee.
Register this redirect URI with the WorkOS application:
```text
{BETTER_AUTH_URL}/api/auth/oauth2/callback/workos
```
### Keycloak OIDC
Use either an explicit issuer or the URL-plus-realm form. The client ID and secret are required in both forms.
Explicit issuer:
```bash
KEYCLOAK_ISSUER=https://auth.example.com/realms/mosaic
KEYCLOAK_CLIENT_ID=mosaic
KEYCLOAK_CLIENT_SECRET=...
```
Derived issuer:
```bash
KEYCLOAK_URL=https://auth.example.com
KEYCLOAK_REALM=mosaic
KEYCLOAK_CLIENT_ID=mosaic
KEYCLOAK_CLIENT_SECRET=...
```
`KEYCLOAK_ISSUER` takes precedence when both forms are present. Keycloak uses PKCE and issuer validation in the current auth configuration.
An optional team-claim label can be exposed in provider discovery:
```bash
KEYCLOAK_TEAM_SYNC_CLAIM=groups
```
The default reported claim is `groups`. The discovery payload reports this claim; verify any downstream membership-sync behavior separately before treating it as an authorization guarantee.
Register this redirect URI with the Keycloak client:
```text
{BETTER_AUTH_URL}/api/auth/oauth2/callback/keycloak
```
### Keycloak direct SAML fallback
The current web flow supports a direct SAML link for Keycloak when `KEYCLOAK_SAML_LOGIN_URL` is configured:
```bash
KEYCLOAK_SAML_LOGIN_URL=https://auth.example.com/realms/mosaic/protocol/saml
```
This creates a configured Keycloak provider with `loginMode: saml`. The web login button links directly to the supplied URL as `Continue with Keycloak (SAML)`; there is no Better Auth OIDC callback for this mode. The URL must be the provider's valid SAML launch URL for the deployment.
A SAML-only Keycloak configuration does not require the Keycloak OIDC client variables. Do not combine an incomplete OIDC variable set with SAML-only configuration: partial OIDC configuration is rejected during auth setup.
## Callback and discovery contract
Better Auth is mounted at `/api/auth`. OIDC callbacks use:
```text
{BETTER_AUTH_URL}/api/auth/oauth2/callback/{providerId}
```
The gateway exposes provider discovery at:
```text
GET /api/sso/providers
```
The response includes `authentik`, `workos`, and `keycloak` records with:
- `configured` — whether a usable OIDC or Keycloak SAML configuration is present.
- `protocols` — supported protocols for the provider record.
- `loginMode``oidc`, `saml`, or `null`.
- `callbackPath` — the OIDC callback path, or `null` for SAML-only mode.
- `teamSync` — the configured/default claim label exposed to the UI.
- `samlFallback` — whether a direct Keycloak SAML URL is configured.
- `warnings` — partial OIDC configuration warnings when reported.
The web login page filters this response to configured providers. OIDC buttons use Better Auth's `signIn.oauth2` flow. A configured Keycloak SAML fallback is rendered as a direct link. No provider-specific `NEXT_PUBLIC_*_ENABLED` flag is required or consumed.
## Configuration procedure
1. Set `BETTER_AUTH_URL` to the public gateway URL that the identity provider can reach.
2. Set `BETTER_AUTH_SECRET` and the correct `GATEWAY_CORS_ORIGIN` values.
3. Choose one provider configuration above and set its complete required variable group.
4. Register the exact OIDC callback URI with the identity provider, when using OIDC.
5. Restart or redeploy the gateway so it loads the changed environment.
6. Inspect discovery without exposing secrets:
```bash
curl "$BETTER_AUTH_URL/api/sso/providers"
```
7. Open the web login page and confirm that only configured providers are shown.
8. Complete a sign-in and verify the callback returns to the configured application.
## Partial configuration and failure modes
Provider configuration is optional. If no provider variables are set, the gateway can run without SSO providers and the web login page renders no SSO section.
For Authentik and WorkOS, setting only part of the issuer/client ID/client secret group raises a configuration error. For Keycloak OIDC, the client ID, client secret, and either an explicit issuer or a complete URL-plus-realm pair are required. Empty or whitespace-only values are treated as unset.
Common failures:
| Symptom | Check |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Provider does not appear on the login page | Query `/api/sso/providers`; verify the complete provider variable group is present in the gateway environment. |
| OAuth callback is rejected | Compare the registered redirect URI character-for-character with `BETTER_AUTH_URL` and the provider callback path. |
| Provider is shown but sign-in cannot start | Check `loginMode`, issuer discovery, client credentials, and gateway logs. |
| Keycloak SAML button is absent | Set `KEYCLOAK_SAML_LOGIN_URL` to the provider's direct launch URL and reload the gateway. |
| Startup/auth initialization reports missing variables | Remove the partial provider configuration or provide the complete required group. |
| SSO succeeds but team membership is unexpected | Treat `teamSync.claim` as discovery metadata and verify the actual claim mapping and membership-sync implementation. |
Do not enable a provider by adding the obsolete `NEXT_PUBLIC_WORKOS_ENABLED` or `NEXT_PUBLIC_KEYCLOAK_ENABLED` variables. They are not read by the current web application.
## Related
- [Administrator guide](../README.md)
- [Security chapter](README.md)
- [API documentation index](../../API/README.md)
- [Documentation atlas](../../README.md)