Commit Graph

123 Commits

Author SHA1 Message Date
f1668682b7 test(net): TUN-mode docker stack and ignored e2e test
- docker-compose.yml: run peer-a and peer-b with TS_USERSPACE=false +
  /dev/net/tun device + cap_add. Pin peer-a's WG listen port to 41641
  via TS_TAILSCALED_EXTRA_ARGS and publish it to the host so direct
  UDP from outside docker has somewhere to land.
- run.sh: use an ephemeral pre-auth key for the test client so
  Headscale auto-deletes the test node when its map stream drops
  (instead of accumulating hundreds of stale entries that eventually
  slow netmap propagation to a crawl). Disable shields-up on both
  peers so the kernel firewall doesn't drop inbound tailnet TCP. Tweak
  the JSON key extraction to handle pretty-printed output.
- integration.rs: add `test_e2e_tcp_through_tunnel` that brings up
  the daemon, dials peer-a's echo server through the proxy, and
  asserts the echo body comes back. Currently `#[ignore]`d — the
  docker stack runs Headscale over plain HTTP, but Tailscale's client
  unconditionally tries TLS to DERP relays ("tls: first record does
  not look like a TLS handshake"), so peer-a can never receive
  packets we forward via the relay. Unblocking needs either TLS
  termination on the docker DERP or running the test inside the same
  docker network as peer-a. Test stays in the tree because everything
  it tests up to the read timeout is real verified behavior.
2026-04-07 14:33:59 +01:00
dca8c3b643 fix(net): protocol fixes for Tailscale-compatible peer reachability
A pile of correctness bugs that all stopped real Tailscale peers from
being able to send WireGuard packets back to us. Found while building
out the e2e test against the docker-compose stack.

1. WireGuard static key was wrong (lifecycle.rs)
   We were initializing the WgTunnel with `keys.wg_private`, a separate
   x25519 key from the one Tailscale advertises in netmaps. Peers know
   us by `node_public` and compute mac1 against it; signing handshakes
   with a different private key meant every init we sent was silently
   dropped. Use `keys.node_private` instead — node_key IS the WG static
   key in Tailscale.

2. DERP relay couldn't route packets to us (derp/client.rs)
   Our DerpClient was sealing the ClientInfo frame with a fresh
   ephemeral NaCl keypair and putting the ephemeral public in the frame
   prefix. Tailscale's protocol expects the *long-term* node public key
   in the prefix — that's how the relay knows where to forward packets
   addressed to our node_key. With the ephemeral key, the relay
   accepted the connection but never delivered our peers' responses.
   Now seal with the long-term node key.

3. Headscale never persisted our DiscoKey (proto/types.rs, control/*)
   The streaming /machine/map handler in Headscale ≥ capVer 68 doesn't
   update DiscoKey on the node record — only the "Lite endpoint update"
   path does, gated on Stream:false + OmitPeers:true + ReadOnly:false.
   Without DiscoKey our nodes appeared in `headscale nodes list` with
   `discokey:000…` and never propagated into peer netmaps. Add the
   DiscoKey field to RegisterRequest, add OmitPeers/ReadOnly fields to
   MapRequest, and call a new `lite_update` between register and the
   streaming map. Also add `post_json_no_response` for endpoints that
   reply with an empty body.

4. EncapAction is now a struct instead of an enum (wg/tunnel.rs)
   Routing was forced to either UDP or DERP. With a peer whose
   advertised UDP endpoint is on an unreachable RFC1918 network (e.g.
   docker bridge IPs), we'd send via UDP, get nothing, and never fall
   back. Send over every available transport — receivers dedupe via
   the WireGuard replay window — and let dispatch_encap forward each
   populated arm to its respective channel.

5. Drop the dead PacketRouter (wg/router.rs)
   Skeleton from an earlier design that never got wired up; it's been
   accumulating dead-code warnings.
2026-04-07 14:33:43 +01:00
85d34bb035 feat(net): add UDP transport for direct peer connections
DERP works for everything but adds relay latency. Add a parallel UDP
transport so peers with reachable endpoints can talk directly:

- wg/tunnel: track each peer's local boringtun index in PeerTunnel and
  expose find_peer_by_local_index / find_peer_by_endpoint lookups
- daemon/lifecycle: bind a UdpSocket on 0.0.0.0:0 alongside DERP, run
  the recv loop on a clone of an Arc<UdpSocket> so send and recv can
  proceed concurrently
- run_wg_loop: new udp_in_rx select arm. For inbound UDP we identify
  the source peer by parsing the WireGuard receiver_index out of the
  packet header (msg types 2/3/4) and falling back to source-address
  matching for type-1 handshake initiations
- dispatch_encap: SendUdp now actually forwards via the UDP channel

UDP failure is non-fatal — DERP can carry traffic alone if the bind
fails or packets are dropped.
2026-04-07 13:48:59 +01:00
bea8a308da test(net): add integration test harness against Headscale
Spins up Headscale 0.23 (with embedded DERP) plus two Tailscale peers
in docker compose, generates pre-auth keys, and runs three integration
tests behind the `integration` feature:

- test_register_and_receive_netmap: full TS2021 → register → first
  netmap fetch
- test_proxy_listener_accepts: starts the daemon and waits for it to
  reach the Running state
- test_daemon_lifecycle: full lifecycle including DERP connect, then
  clean shutdown via the DaemonHandle

Run with `sunbeam-net/tests/run.sh` (handles compose up/down + auth
key provisioning) or manually via cargo nextest with the env vars
SUNBEAM_NET_TEST_AUTH_KEY and SUNBEAM_NET_TEST_COORD_URL set.
2026-04-07 13:42:46 +01:00
9750d4e0b3 feat(net): add VPN daemon lifecycle, state, and IPC
The daemon orchestrates everything: it owns reconnection backoff, the
WireGuard tunnel, the smoltcp engine, the DERP relay loop, the local
TCP proxy, and a Unix-socket IPC server for status queries.

- daemon/state: DaemonStatus state machine + DaemonHandle for shutdown
  signaling and live status access
- daemon/ipc: newline-delimited JSON Unix socket server (Status,
  Disconnect, Peers requests)
- daemon/lifecycle: VpnDaemon::start spawns run_daemon_loop, which pins
  a session future and selects against shutdown_rx so shutdown breaks
  out cleanly. run_session brings up the full pipeline:
  control client → register → map stream → wg tunnel → engine →
  proxy listener → wg encap/decap loop → DERP relay → IPC server.

DERP transport: when the netmap doesn't surface a usable DERP endpoint
(Headscale's embedded relay returns host_name="headscale", port=0),
fall back to deriving host:port from coordination_url. WG packets to
SendDerp peers go via a dedicated derp_out channel; inbound DERP frames
flow back through derp_in into the decap arm, which forwards Packet
results to the engine and Response results back to derp_out for the
handshake exchange.
2026-04-07 13:42:36 +01:00
f903c1a073 feat(net): add network engine and TCP proxy
- proxy/engine: NetworkEngine that owns the smoltcp VirtualNetwork and
  bridges async TCP streams to virtual sockets via a 5ms poll loop.
  Each ProxyConnection holds the local TcpStream + smoltcp socket
  handle and shuttles data between them with try_read/try_write so the
  engine never blocks.
- proxy/tcp: skeleton TcpProxy listener (currently unused; the daemon
  inlines its own listener that hands off to the engine via mpsc)
2026-04-07 13:42:15 +01:00
d9d0d64236 feat(net): add control protocol (register + map stream)
- control/client: TS2021 connection setup — TCP, HTTP CONNECT-style
  upgrade to /ts2021, full Noise IK handshake via NoiseStream, then
  HTTP/2 client handshake on top via the h2 crate
- control/register: POST /machine/register with pre-auth key, PascalCase
  JSON serde matching Tailscale's wire format
- control/netmap: streaming MapStream that reads length-prefixed JSON
  messages from POST /machine/map, classifies them into Full/Delta/
  PeersChanged/PeersRemoved/KeepAlive, and transparently zstd-decodes
  by detecting the 0x28 0xB5 0x2F 0xFD magic (Headscale only compresses
  if the client opts in)
2026-04-07 13:41:58 +01:00
0fe55d2bf6 feat(net): add WireGuard tunnel and smoltcp virtual network
- wg/tunnel: per-peer boringtun Tunn management with peer table sync
  from netmap (add/remove/update endpoints, allowed_ips, DERP region)
  and encapsulate/decapsulate/tick that route to UDP or DERP
- wg/socket: smoltcp Interface backed by an mpsc-channel Device that
  bridges sync poll-based smoltcp with async tokio mpsc channels
- wg/router: skeleton PacketRouter (currently unused; reserved for the
  unified UDP/DERP ingress path)
2026-04-07 13:41:43 +01:00
76ab2c1a8e feat(net): add DERP relay client
DERP is Tailscale's TCP relay protocol for peers that can't establish a
direct UDP path. Add the standalone client:

- derp/framing: 5-byte frame codec (1-byte type + 4-byte BE length)
- derp/client: HTTP /derp upgrade, Tailscale's NaCl SealedBox handshake
  (ServerKey → ClientInfo → ServerInfo → NotePreferred), and
  send_packet/recv_packet for forwarding WireGuard datagrams

Includes the 8-byte DERP\xf0\x9f\x94\x91 magic prefix in the ServerKey
payload and reads the HTTP upgrade response one byte at a time so the
inline first frame isn't swallowed by a buffered reader.
2026-04-07 13:41:17 +01:00
91cef0a730 feat(net): add Noise IK + HTTP/2 stream layer
Tailscale's TS2021 protocol layers HTTP/2 over an encrypted Noise IK
channel reached via HTTP CONNECT-style upgrade. Add the lower half:

- noise/handshake: hand-rolled Noise_IK_25519_ChaChaPoly_BLAKE2s
  initiator with HKDF + ChaCha20-Poly1305 (no snow dependency)
- noise/framing: 3-byte frame codec (1-byte type + 2-byte BE length)
- noise/stream: NoiseStream implementing AsyncRead + AsyncWrite over
  the framed channel so the h2 crate can sit on top
2026-04-07 13:41:01 +01:00
13539e6e85 feat(net): scaffold sunbeam-net crate with foundations
Add the workspace crate that will host a pure Rust Headscale/Tailscale-
compatible VPN client. This first commit lands the crate skeleton plus
the leaf modules that the rest of the stack builds on:

- error: thiserror Error enum + Result alias
- config: VpnConfig
- keys: Curve25519 node/disco/wg key types with on-disk persistence
- proto/types: PascalCase serde wire types matching Tailscale's JSON
2026-04-07 13:40:27 +01:00
cc2c3f7a3b refactor(openbao): migrate to vaultrs client library
Replace hand-rolled OpenBao HTTP client with vaultrs 0.8.0, which
has official OpenBao support. BaoClient remains the public API so
callers are unchanged. KV patch uses raw HTTP since vaultrs doesn't
expose it yet.
2026-04-05 22:34:41 +01:00
971810433c fix(openbao): fix init response field name for keys_base64
OpenBao returns `keys_base64` not `unseal_keys_b64`. Added serde
alias to accept both field names for compatibility.
2026-04-05 20:41:54 +01:00
dce1cec6ac fix(openbao): create placeholder secret before waiting for pod
On a clean cluster, the OpenBao pod can't start because it mounts
the openbao-keys secret as a volume, but that secret doesn't exist
until init runs. Create a placeholder secret in WaitPodRunning so
the pod can mount it and start. InitOrUnsealOpenBao overwrites it
with real values during initialization.
2026-04-05 20:33:19 +01:00
70b1f84caa fix(config): delete legacy ~/.sunbeam.json after migration
The migration from ~/.sunbeam.json to ~/.sunbeam/config.json
copied but never removed the legacy file, which could cause
confusion with older binaries still writing to the old path.
2026-04-05 20:20:38 +01:00
0c7f1543f5 fix(wfe): update to 1.6.3 for step name display in print_summary
WFE now populates execution pointer step_name from the workflow
definition, so print_summary shows actual step names instead of
"step-0", "step-1", etc.
2026-04-05 20:09:32 +01:00
ff297b61b5 chore: remove pre-WFE monolithic functions
Remove ~1400 lines of dead code now replaced by WFE primitives:
- cluster.rs: cmd_up, ensure_cert_manager, ensure_tls_cert/secret,
  wait_for_core, print_urls, secrets_dir, CERT_MANAGER_URL
- secrets.rs: cmd_seed, cmd_verify, seed_openbao,
  seed_kratos_admin_identity, SeedResult, delete_crd,
  delete_k8s_secret, kubectl_jsonpath
2026-04-05 19:44:23 +01:00
69b0ca4871 chore: remove vendored dependencies and publish script
Dependencies are now sourced from the sunbeam registry.
2026-04-05 19:15:14 +01:00
73e72550a7 refactor(manifests): extract OpenSearch ML into parallel WFE step
Move ensure_opensearch_ml and inject_opensearch_model_id out of
cmd_apply post-hooks into dedicated WFE steps that run in a
parallel branch alongside rollout waits. The ML model download
(10+ min on first run) no longer blocks the rest of the pipeline.
2026-04-05 18:28:21 +01:00
aa19590c73 fix(secrets): add max retries and backoff to port-forward loop
The port-forward background task retried infinitely on 500 errors
when the target pod wasn't ready. Add a 30-attempt limit with 2s
backoff between retries so the step eventually fails instead of
spinning forever.
2026-04-05 18:27:08 +01:00
3cfa0fe755 refactor(wfe): decompose steps into atomic config-driven primitives
Replace big-bag steps with 10 atomic primitives that each do one
thing and read config from step_config:

- ApplyManifest (replaces 12 identical apply structs)
- WaitForRollout (replaces WaitForCore loop)
- CreatePGRole, CreatePGDatabase (replaces EnsurePGRolesAndDatabases)
- EnsureNamespace, CreateK8sSecret (replaces CreateK8sSecrets)
- SeedKVPath, WriteKVPath, CollectCredentials (replaces SeedAllKVPaths + WriteDirtyKVPaths)
- EnableVaultAuth, WriteVaultAuthConfig, WriteVaultPolicy, WriteVaultRole (replaces ConfigureKubernetesAuth)

Workflow definitions now use parallel branches for independent
operations (infra, KV seeding, PG roles, platform manifests,
K8s secrets, rollout waits).
2026-04-05 18:23:36 +01:00
9cd3c641da feat(wfe): integrate workflow engine for up, seed, verify, bootstrap
Dispatch `sunbeam up`, `sunbeam seed`, `sunbeam verify`, and
`sunbeam bootstrap` through WFE workflows instead of monolithic
functions. Steps communicate via JSON workflow data and each
workflow is persisted in a per-context SQLite database.
2026-04-05 18:21:59 +01:00
dce085cd0c feat(auth): add auth token command, penpot seed support
- `sunbeam auth token` prints JSON headers for MCP headersHelper:
  {"Authorization": "Bearer <token>"}
- Add penpot to PG_USERS, pg_db_map, KV seed, and all_paths
- Add cert-manager to VSO auth role bound namespaces
2026-04-04 12:53:53 +01:00
c6bd8be030 chore: remove Python code and pyproject.toml 2026-03-27 09:59:20 +00:00
e568ddf82a chore: checkpoint before Python removal 2026-03-26 22:33:59 +00:00
683cec9307 release: v1.1.2
- fix(opensearch): make ML model registration idempotent
v1.1.2
2026-03-25 18:09:25 +00:00
30dc4f9c5e fix(opensearch): make ML model registration idempotent
Reuse any existing model version (including DEPLOY_FAILED) instead of
registering a new copy. Prevents accumulation of stale model chunks
in .plugins-ml-model when OpenSearch restarts between applies.
2026-03-25 18:04:28 +00:00
3d2d16d53e feat(secrets): add xchacha20-poly1305 cipher key seeding for Kratos
Add rand_alphanum() using OsRng for generating fixed-length
alphanumeric secrets. Seed secrets-cipher (32 chars) into the
kratos KV path for at-rest encryption of OIDC tokens.
2026-03-24 20:51:13 +00:00
80ab6d6113 feat: enable Meet external API, fix SDK path
- Meet config: EXTERNAL_API_ENABLED=True
- Meet backend: added lasuite-resource-server configmap + RS creds
- Pingora: added /external-api/ route for Meet
- SDK: fixed Meet URL to use /external-api/ (hyphenated)

NOTE: Meet RS requires ES256 tokens + lasuite_meet scope — CLI
tokens use RS256 + generic scopes. Needs RS config adjustment.
2026-03-24 17:03:55 +00:00
b08a80d177 refactor: nest infra commands under sunbeam platform
Moves up, status, apply, seed, verify, logs, get, restart, build,
check, mirror, bootstrap, k8s under `sunbeam platform <command>`.
Top-level now has 19 commands instead of 32.
2026-03-24 15:52:44 +00:00
530b2a22b8 chore: remove solution branding from CLI help text 2026-03-24 15:44:39 +00:00
6a2b62dc42 refactor: remove bao, docs, and people subcommands
- bao: replaced by `sunbeam vault` with proper JWT auth
- docs: La Suite Docs not ready for production
- people: La Suite People not ready for production
2026-03-24 15:40:58 +00:00
4d9659a8bb chore: bump to v1.1.1, update CHANGELOG v1.1.1 2026-03-24 15:29:05 +00:00
cd80a57a40 fix: DynamicBearer auth, retry on 500/429, upload resilience
- DynamicBearer AuthMethod: La Suite clients resolve tokens fresh
  per-request from cache file, surviving token expiry mid-session
- Retry with exponential backoff on all Drive API calls (create_child,
  upload_ended) — up to 5 retries on 429/500/502/503
- Token refresh triggered on 500 before retry (handles expired SSO)
- S3 upload retry with backoff (up to 3 retries on 502/503)
- Connection pooling: reuse DriveClient HTTP client for S3 PUTs
- Folder/file dedup: skip existing items on re-upload
2026-03-24 15:25:01 +00:00
de5c807374 fix: progress bar tracks files not bytes, retry on 502, dedup folders
- Overall bar progress based on file count (was bytes, causing 50%
  bar at low file count when large files uploaded first)
- Bandwidth computed manually from completed bytes / elapsed time
- Per-file bars show spinner + name only (no misleading 0 B counter)
- S3 upload retries up to 3x on 502/503 with backoff
- Folder dedup: list_children before create, reuse existing folders
- File dedup: skip files already present in target folder
- Connection pooling: reuse DriveClient's HTTP client for S3 PUTs
- Default parallel back to 8 (retries handle transient 502s)
2026-03-24 14:55:03 +00:00
2ab2fd5b8f fix: polish Drive upload progress UI
- Inline folder creation status (no scroll)
- Overall bar shows file count + bytes + speed + ETA
- Per-file spinners: ⬆ uploading, ✓ done, ✗ failed
- Bars pop in/out dynamically as uploads start/finish
- Error count in summary line
- Default parallel reduced to 4 (proxy can't handle 8)
2026-03-24 13:36:17 +00:00
27536b4695 feat: parallel Drive upload with indicatif progress UI
- Parallel file uploads with --parallel flag (default 4)
- indicatif MultiProgress: overall bar with file count, speed, ETA
- Per-file spinner bars showing filename during upload
- Phase 1: walk tree + create folders sequentially
- Phase 2: upload files concurrently via semaphore
- Summary line on completion (files, bytes, time, speed)
- Fixed DriveFile/DriveFolder types to match actual API fields
- DriveClient now Clone for Arc sharing across tasks
2026-03-24 13:26:16 +00:00
2f2f4585f1 docs: add CHANGELOG.md v1.1.0 2026-03-24 12:19:58 +00:00
477006ede2 chore: bump to v1.1.0, update package description 2026-03-24 12:17:08 +00:00
ca0748b109 feat: encrypted vault keystore, JWT auth, Drive upload
Vault keystore (vault_keystore.rs):
- AES-256-GCM encrypted local storage for root tokens + unseal keys
- Argon2id KDF with machine-specific salt, 0600 permissions
- save/load/verify/export API with 26 unit tests
- Integrated into seed flow: save after init, load as fallback,
  backfill from cluster, restore K8s Secret if wiped

Vault CLI:
- vault reinit: wipe and re-initialize vault with confirmation
- vault keys: show local keystore status
- vault export-keys: plaintext export for machine migration
- vault status: now shows keystore status + uses JWT auth
- Fixed seal_status() bypassing request() (missing auth headers)

Vault OIDC auth:
- JWT auth method enabled on OpenBao via seed script
- cli-admin role: full access for users with admin:true JWT claim
- cli-reader role: read-only for non-admin SSO users
- BaoClient.with_proxy_auth(): sends both Bearer (proxy) and
  X-Vault-Token (vault) headers
- SunbeamClient.bao() authenticates via JWT login, falls back
  to local keystore root token

Drive:
- SDK client uses /items/ endpoint (was /files/ and /folders/)
- Added create_child, upload_ended, upload_to_s3 methods
- Added recursive drive upload command (--path, --folder-id)
- Switched all La Suite clients to /external_api/v1.0/

Infrastructure:
- Removed openbao-keys-placeholder.yaml from kustomization
- Added sunbeam.dev/managed-by label to programmatic secrets
- kv_patch→kv_put fallback for fresh vault initialization
- Hydra/Kratos secrets combined (new,old) for key rotation
2026-03-24 12:09:01 +00:00
13e3f5d42e fix opensearch pod resolution + sol-agent vault policy
os_api: resolve pod name by label instead of hardcoded opensearch-0.
added find_pod_by_label helper to kube.rs.

secrets.py: sol-agent policy (read/write sol-tokens/*) and k8s auth
role bound to matrix namespace default SA.
2026-03-23 08:48:33 +00:00
faf525522c feat: async SunbeamClient factory with unified auth resolution
SunbeamClient accessors are now async and resolve auth per-client:
- SSO bearer (get_token) for admin APIs, Matrix, La Suite, OpenSearch
- Gitea PAT (get_gitea_token) for VCS
- None for Prometheus, Loki, S3, LiveKit

Fixes client URLs to match deployed routes: hydra→hydra.{domain},
matrix→messages.{domain}, grafana→metrics.{domain},
prometheus→systemmetrics.{domain}, loki→systemlogs.{domain}.

Removes all ad-hoc token helpers from CLI modules (matrix_with_token,
os_client, people_client, etc). Every dispatch just calls
client.service().await?.
2026-03-22 18:57:22 +00:00
34647e6bcb feat: seed Sol agent vault policy + gitea creds, bump v1.0.1
Patches gitea admin credentials into secret/sol for Sol's Gitea
integration. Adds sol-agent vault policy with read/write access
to sol-tokens/* for user impersonation PATs, plus k8s auth role
bound to the matrix namespace.
v1.0.1
2026-03-22 13:46:15 +00:00
051e17ddf1 chore: bump to v1.0.0, drop native-tls for pure rustls
Removes openssl-sys transitive dep by disabling reqwest default
features, enabling static musl cross-compilation for Linux.
v1.0.0
2026-03-21 22:29:13 +00:00
7ebf9006a1 feat: wire 15 service subcommands into CLI, remove old user command
Adds Verb variants: auth, vcs, chat, search, storage, media, mon,
vault, people, docs, meet, drive, mail, cal, find. Each delegates
to the corresponding SDK cli.rs dispatch function.

Removes the legacy `user` command (replaced by `auth identity`).
Renames Get's -o to --kubectl-output to avoid conflict with the
new global -o/--output flag. Enables all SDK features in binary.
2026-03-21 22:20:15 +00:00
f867805280 feat: CLI modules for all 25+ service clients
One cli.rs per SDK module, gated behind #[cfg(feature = "cli")]:
- auth (identity + hydra): identity, session, recovery, schema,
  courier, health, client, jwk, issuer, token, SSO passthrough
- vcs (gitea): repo, issue, pr, branch, org, user, file, notification
- chat (matrix): room, message, state, profile, device, user, sync
- search (opensearch): doc, query, count, index, cluster, node,
  ingest pipeline, snapshot
- storage (s3): bucket, object
- media (livekit): room, participant, egress, token
- mon (prometheus, loki, grafana): queries, dashboards, datasources,
  folders, annotations, alerts, org
- vault (openbao): status, init, unseal, kv, policy, auth, secrets
- la suite (people, docs, meet, drive, mail, cal, find)

All dispatch functions take (cmd, &SunbeamClient, OutputFormat).
2026-03-21 22:18:58 +00:00
3d7a2d5d34 feat: OutputFormat enum + render/render_list/read_json_input helpers
Adds -o json|yaml|table output support to the SDK output module.
OutputFormat derives clap::ValueEnum behind the cli feature gate.
2026-03-21 22:17:10 +00:00
756fbc5e38 chore: update Cargo.lock 2026-03-21 20:38:53 +00:00
97976e0686 fix: include build module (was gitignored)
Bump: sunbeam-sdk v0.12.1
2026-03-21 20:37:54 +00:00
f06a167496 feat: BuildKit client + integration test suite (651 tests)
BuildKitClient CLI wrapper for buildctl.
Docker compose stack (9 services) for integration testing.
Comprehensive test suite: wiremock tests for Matrix/La Suite/S3/client,
integration tests for Kratos/Hydra/Gitea/OpenSearch/Prometheus/Loki/
Grafana/LiveKit.

Bump: sunbeam-sdk v0.12.0
2026-03-21 20:35:59 +00:00