Announce (theta-gateway) + discover/apply/revert (theta-agent) confirmed
working end-to-end over real multicast between real containers, including
two real bugs found and fixed along the way (IPv6 query abort, EBUSY on
rename over a bind-mounted /etc/hosts).
Windows/macOS mDNS is now the ONLY unbuilt piece of the original design
this session set out to implement -- and it's blocked on platform access
this environment doesn't have, not on missing design or effort.
Confirmed the core idea (master terminates a connection, relays over a
spoke's WG mesh IP to a spoke with zero published/inbound ports of its
own) with a standalone test: an external client hit the master's public
port and got a response that could only have come from the spoke,
which had no reachable port except over the tunnel.
Deliberately did NOT wire this into theta-proxy's actual Lua/Redis
routing engine -- that needs its own dedicated pass to do safely, plus a
real service-to-service credential between sso-manager-node and
theta-proxy/theta-gateway that doesn't exist yet. Recorded as verified
mechanism / unbuilt automation, not conflated with either "done" or
"unknown whether it would even work."
Updates the status table and top-of-doc callout to reflect what actually
shipped this pass: live catalog replication, identical-directory signing
key, coordinated master promotion (with two real bugs found + fixed along
the way), and a real, tested gateway-to-gateway WireGuard mesh.
Explicitly calls out what's still NOT true despite all of the above: the
mesh exists as its own transport layer but sso-manager-node's join/
replicate traffic doesn't route over it yet, so the no-inbound-spoke
relay scenario still isn't solved end-to-end. mDNS remains unbuilt.
Extends the shipped CFG_MASTER_DIRECTORY_URL/JOIN_KEY join flow with the
selfUrl a spoke needs to register itself for live catalog replication
(theta-directory v2.4.0's POST /api/site/spokes) -- without this, every
spoke was permanently limited to the one-time join snapshot even after
the master gained the ability to push live updates.
setup.sh already computes CFG_SSO_HOST before this point in the script;
passes https://$CFG_SSO_HOST as bootstrap/site-join.js's third argument,
which forwards it as `selfUrl` in the POST /api/site/join body.
MULTI_SITE_SPEC.md described a WireGuard-mesh + live-replication design as
if unbuilt-but-planned; meanwhile theta-directory v2.2.0-v2.3.0 (rolled up
in theta-suite v2.2.0) already shipped a simpler, real join mechanism
(one-time LDIF/catalog export over a site join key, read-only spoke
enforcement, setup.sh wiring) that this doc didn't mention at all. Added a
callout pointing at docs/site-join.md as the actual current behavior, and
corrected the status table so it no longer implies unbuilt features are
implemented.
Also adds AGENT_LOCAL_DISCOVERY_SPEC.md, a standalone handoff spec for the
mDNS "prefer local discovered directory" optimization -- confirmed not
implemented anywhere in theta-agent. Needs Windows/Mac-native investigation
this environment can't do; written so it can be picked up independently.
Renaming the job broke the master protection rule, which requires a check
named exactly 'Syntax check bootstrap.js'. The job still checks both bootstrap
scripts, just under the protected name.
Multi-site join wiring (server + UI landed in theta-directory v2.3.0):
- bootstrap/site-join.js: runs inside the sso-manager container (same
self-contained rule as bootstrap.js); logs in as the bootstrap admin and calls
/api/site/join. Idempotent: an already-joined node reports 'already a spoke'.
- setup.sh step 5b: if setup.env sets CFG_MASTER_DIRECTORY_URL +
CFG_MASTER_DIRECTORY_JOIN_KEY, run the join after the bootstrap. Only honored
on first run (ensure_config reads setup.env once and ignores it once
./config/ exists), so an already-populated directory can never be merged.
- setup.env.example documents both vars.
- lint.yml also node --check's site-join.js.
- CFG_ORG default was 'SSO Manager', which became the browser tab title / app
name on fresh installs. Default is now 'Theta Directory' (existing deployments
keep their operator-owned ./config/sso-secrets.js name).
- Records theta-agent d937f8d (silent-install server_url + tray autostart +
self-update 404 fixes).
Dockerfile.openldap now pulls ghcr.io/theta42/openldap-nestgroup instead of
compiling from source on every build (~5-6min -> ~1.5min per CI matrix run,
and removes the runtime dependency on git.openldap.org).
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Directory tab admitted every kind:'host' resource regardless of promotion
status (every discovery plugin creates its finds as kind:'host'), and
site-status 500'd on a nonexistent Resource.subType column.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Unifies the GitHub Pages docs site: the SSO/Proxy/Jump Host pages, their
nav labels, and each component's own README now consistently say Theta
Directory / Theta Proxy / Theta Gateway, drop marketing sections ("Why this
over the alternatives", "Get it", "Related projects") that don't apply to a
suite component, remove every standalone/bare-metal install path, and link
to theta42.github.io/theta-suite/... instead of the old per-repo Pages sites.
Bumps submodules: theta-directory v2.0.2, proxy v2.0.1, jump-host v2.0.1.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
- Theta Gateway: Add WireGuard peer management UI (/wireguard) with QR code generator, .conf download, and per-client exit node selection
- Theta Agent: Add desktop tray companion app (theta-agent-tray) with Theta 42 logo, color-coded status (red, yellow, green, blue), and home LAN detection
- Suite Rebuild: Local stack initialized with CFG_DOMAIN=suite.vm42.us and CFG_SITE_NAME=718it
seed_node_conf() (added for sso-manager-node's node-scoped secrets
engine, DESIGN.md §5) had two bugs that made it fail on every call:
- Path had an extra "data/" segment (secret/data/nodes/<id>/<name>).
bao kv put takes the mount-relative path and inserts "data/" itself
for KV v2 -- same convention seed_app_conf already uses just above it
(secret/${vault_path}, not secret/data/${vault_path}). Fixed to
secret/nodes/<id>/<name>, which resolves under the hood to the
secret/data/nodes/<id>/* path api_agent_ops.js's node-scope check
expects.
- It piped "key=value\n" lines to `bao kv put path -`, but `-` there
means "read a JSON object from stdin", not KV lines -- failed with
"invalid key/value pair \"-\"" before ever reaching OpenBao. Fixed to
pass key=value pairs as ordinary CLI args.
Verified against a real OpenBao round-trip (write via the fixed
function, read back both via the CLI and the same HTTP path the SSO's
node-scope check uses).
docs/_config.yml: "theta-suite" -> "Theta Suite" in the Jekyll site title.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bootstrap.js no longer creates host_theta-proxy / host_theta-jump as
synthetic kind:'host' resources. Proxy and jump-host are containers running
on the one real stack host, not machines of their own -- and jump-host
resolves its SSH-reachable-hosts list from exactly kind:'host', so the
mistake wasn't just conceptual, it could offer unreachable SSH targets.
Their services now parent directly onto the stack host, like every other
component. Installs seeded between 2026-08-05 and this release self-heal on
the next ./setup.sh run: existing children are re-parented off the
synthetic hosts and the now-empty synthetic hosts are removed. Validated
live against a running instance carrying the exact bad state.
Bumps submodules to sso-manager-node v1.30.2, proxy v1.35.1,
jump-host v1.19.1.
Also: README's architecture diagram + repo layout were stale (2-service
view predating jump-host/OpenBao, 2 of 5 submodules listed); new
docs/fixtures.md + docs/screenshots.md + bootstrap/seed-demo-users.sh for
consistent, repeatable demo data and screenshot passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCdnfSCuZr6xvPDxTo3D
Test Email and Test SMS could never have worked, and all SMS delivery was
broken underneath them:
- Test Email threw "Email.send is not a function" -- models/email.js
exports {Mail} and the handler called .send on the module.
- Test SMS threw "Unexpected token '<'" -- it POSTed to
api.voip.ms/v1.0/sms/send, which does not exist, and got HTML back.
- models/sms.js called PluginInstance.find(), but the ORM has no find, so
every SMS threw before it could even reach the VoIP.ms fallback.
Both test endpoints now go through the same senders real messages use. A
test that reimplements delivery proves nothing, which is how two broken
paths went unnoticed.
Also: the Install Agent modal now offers the join-key flow that v1.43.0
shipped in the API and documented but never surfaced in the UI.
Tagged during a GitHub Actions major outage; verified locally on the
merged commit (299/299 in the same Docker suite CI runs).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rolls up sso-manager-node v1.30.0, theta-agent v1.5.1, proxy v1.35.0.
The stack's own theta-agent could never connect. setup.sh generated a
random token locally and wrote it into agent.yml, but the SSO only
accepts credentials it issued, so it was rejected on every attempt and
the agent looped on "close 4001: Unauthorized" forever. It now writes a
join key the SSO minted; the agent exchanges it for its own token and the
SSO public key on first connect and rewrites its own config.
agent.yml was also left holding literal REPLACE_WITH_* placeholders once
the seds stopped matching the renamed fields, so a fresh install had no
credential at all. The file is chmod 600 now that it holds one.
A fresh install presented its own five containers as unmanaged
discoveries. The compose project name is passed to the Docker discovery
plugin, which recognises them and links each to the service it
implements. openbao and bao-renewer had no directory entries for their
containers to attach to; both are seeded as services now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rolls up sso-manager-node v1.29.0, theta-agent v1.4.0, proxy v1.34.0 and
jump-host v1.19.0.
Per-host SSO returned "400 redirect_uri is not registered for this
client". The bootstrap registered only the proxy's own management
callback, but per-host SSO calls back to
https://<protected-host>/__proxy_auth/callback -- a different URL per
proxied host, all against that one OAuth client. Now registers the
wildcard + apex patterns, and backfills them onto existing clients so
upgraded stacks are fixed too.
theta-proxy and theta-jump were seeded as hosts and then left childless
while their services hung off the stack host. Services now parent to the
host that runs them; reparent() corrects existing installs, but only when
the current parent is the one the old code set.
The proxy gets a read-only SSO API token (minted before the OpenBao
snapshot so the running proxy receives it) backing the per-host SSO group
autocomplete, and the sso-broker policy grants secret/agent/* for the
SSO's persistent theta-agent signing key.
BREAKING: theta-agents must be re-enrolled, and ./setup.sh must be re-run
for the new OpenBao grant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docker-compose: mount /var/run/docker.sock into sso-manager so the seeded
docker-local plugin can list containers (was ENOENT -> 'Last run: error')
- gitlinks: sso-manager-node 49100c9 (v1.28.0), theta-agent 51750d0 (v1.3.1)
ldap-client no longer POSTs an empty Bearer to /api/directory-admin/resources
(the misleading 'Invalid Credentials, login failed' during setup). Gitlink ->
ldap-client 68fcdb5 (v1.25.0).
docker-compose only published LDAPS (636); plain LDAP (389) was not mapped, so
the stack host's own enrollment (ldap://localhost:389) couldn't reach the LDAP
server. Now both 389 + 636 are published (bind 0.0.0.0; LDAP_BIND/LDAPS_BIND to
lock to host). README updated.
- setup.sh: ldap_host defaults to localhost (the public sso.<domain> can't reach
the 389/636 LDAP ports through NAT); overridable via CFG_LDAPS_HOST
- ldap.vars access groups + ldap-client sssd filter now reference the SSO group
model (site_<loc>_hosts_access, site_<loc>_host_<host>_access, god_admin)
- GROUPS.md §5/§8 updated to the corrected naming
- gitlink: ldap-client ebaac18 (v1.24.0)
- setup.sh: ldap.vars generation read CFG_* first-run vars (unset on re-run);
now reads real values from sso-secrets.js, so LDAP enrollment works on re-runs
- generated ldap_access_groups now references god_admin (app_super_admin gone)
- gitlink: sso-manager-node 8db00f0 (v1.26.1)
proxy v1.33.0 + jump v1.18.0 had package.json synced to their tags; update the
gitlinks so a deploy reports matching versions.
Co-Authored-By: Claude <noreply@anthropic.com>
sso v1.25.0 shipped the group & permission model + the v1.24.0 batch (Agents →
Directory, plugin modal rework, Vault restyle). Update the gitlink for the release.
Co-Authored-By: Claude <noreply@anthropic.com>
Add docs/GROUPS.md — the canonical Group & Permission Model (schema, inheritance
resolver, Directory-only management, multi-site, host-side SSSD mapping, migration)
— link it from the docs index, and note sso v1.25.0 in the changelog.
Co-Authored-By: Claude <noreply@anthropic.com>
The bootstrap now creates theta-proxy and theta-jump as managed host-kind
resources in the Directory (matching the OAuth client identities), alongside
the existing stack host and its service entries, so a fresh install shows them
as first-class hosts.
Co-Authored-By: Claude <noreply@anthropic.com>
cp into a running executable fails with 'Text file busy' on a re-install.
Stop the service before copying the prebuilt binary.
Co-Authored-By: Claude <noreply@anthropic.com>
- theta-svc token role (periodic 768h): SSO/PROXY/JUMP_VAULT_TOKEN now minted
through it; ensure_token renews periodic tokens on every setup.sh re-run and
detects/revokes/re-mints valid-but-non-periodic tokens from older installs.
- bao-renewer sidecar (docker-compose): renews the three service tokens every
12h while the stack runs.
- sso-app token role (periodic 768h) + sso-broker policy grants for
auth/token/create/sso-app and renew/revoke/lookup-accessor.
- docs/secrets.md rewritten around the new lifecycle.
- Bump sso-manager-node gitlink to v1.23.0 (real vault-403 fix + app-token
lifecycle).
Co-Authored-By: Claude <noreply@anthropic.com>
setup.sh's 'full control' edit set service_control: true, but that field is a
[]string allowlist, so theta-agent failed YAML decode and crash-looped. Remove
the invalid edit; leave the operator's allowlist (or [] default = deny all).
Co-Authored-By: Claude <noreply@anthropic.com>
On a re-run where OpenBao is already unsealed, the unseal block is skipped and
UNSEAL_KEY is never set; line 778 then referenced it under set -u and aborted
with 'UNSEAL_KEY: unbound variable'. Guard with ${UNSEAL_KEY:-} so the
VAULT_UNSEAL_KEY upsert is simply skipped when there's no key this run.
Co-Authored-By: Claude <noreply@anthropic.com>
- Add --reset-openbao: full clean OpenBao reset (re-init store, flush the
Redis vault-token cache) to clear stale policies/tokens causing recurring
vault 403s.
- Fix theta-agent install: copy the prebuilt theta-agent-linux-amd64 from the
submodule instead of a broken go build; write config to /etc/theta42/agent.yml
(the path the agent reads), not /etc/theta/agent.yml.
- Bump sso-manager-node gitlink to v1.21.0 (shared secrets + durable vault 403 fix).
Co-Authored-By: Claude <noreply@anthropic.com>
The v1.20.2 / v1.32.0 release tags were created but their package.json
versions lagged (1.20.1 / 1.14.3), so the deployed apps' update-check
banner falsely reported a newer version. Repoint the sso-manager-node and
proxy gitlinks to the corrected commits and release v1.35.9.
Co-Authored-By: Claude <noreply@anthropic.com>