fix(installer): Theta Directory branding, visible URL/join-key fields, GUI tray, service autostart

User-reported install fixes:

- Branding: every user-facing 'SSO Manager' string now says 'Theta Directory'
  (agent logs, CLI usage, agent.yml.example, installer wizard).
- Wizard page: the URL/join-key text boxes were never shown. The layout used
  Surface.Width (0 at wizard init) instead of SurfaceWidth and combined
  WordWrap with AutoSize (mutually exclusive in VCL). Rewritten with the
  canonical Inno pattern (SurfaceWidth + ScaleY + explicit label height).
- No console window after install: the tray and helper now build as
  GUI-subsystem binaries (-H=windowsgui) in build_all.sh and
  scripts/setup-build-env.ps1. The agent stays a console app for foreground
  debugging (as a service it never shows a console).
- The daemon never came up after install: install-service now starts the
  service immediately, so the tray IPC socket exists right away and the tray
  connects instead of logging 'actively refused' until a reboot.

Verified: go build/vet/test green; tray+helper PE subsystem = GUI (2), agent =
console (3); installer compiles; tray runs silently.
This commit is contained in:
2026-08-09 20:33:06 -07:00
parent da158b0adc
commit 5013148ffe
9 changed files with 70 additions and 50 deletions
+12 -12
View File
@@ -26,7 +26,7 @@ public_key: ""
location: "default" # Location identifier (e.g., site, datacenter) for naming
# Local LDAP byte-pump socket (DESIGN.md §4). The agent forwards raw LDAP bytes
# Local LDAP byte-pump socket (DESIGN.md ??4). The agent forwards raw LDAP bytes
# from this socket to the SSO, which relays them into its OpenLDAP. The agent
# never parses LDAP. Point SSSD at it with:
# ldap_uri = ldapi://%2frun%2ftheta%2fldap.sock
@@ -37,12 +37,12 @@ ldap_socket: "/run/theta/ldap.sock"
# directory WebSocket is up. The tray checkbox persists here too.
auto_vpn: false
# Windows-specific (DESIGN-WINDOWS.md §11). Ignored on Linux.
# Windows-specific (DESIGN-WINDOWS.md ??11). Ignored on Linux.
service_name: "theta-agent" # Windows service name
desktop_helper: "" # theta-agent-helper.exe path (session-0 ops)
public_ip_detect: true # false = air-gap: never call external IP services
# WireGuard mesh client (DESIGN-WINDOWS.md §5). The signed wireguard_apply
# WireGuard mesh client (DESIGN-WINDOWS.md ??5). The signed wireguard_apply
# command pushes the peer config down the WSS channel; these are local paths.
wireguard:
tunnel_name: "theta-mesh"
@@ -54,25 +54,25 @@ capabilities:
# Basic Capabilities (Safe, read-only or infrastructure management)
# ---------------------------------------------------------
# Push CPU, RAM, GPU, and ZFS metrics to the SSO Manager
# Push CPU, RAM, GPU, and ZFS metrics to Theta Directory
telemetry: true
# Allow the SSO Manager to push down SSSD and SSH keys configuration
# Allow Theta Directory to push down SSSD and SSH keys configuration
configure_ldap: true
# Serve the local LDAP byte-pump socket for SSSD/PAM (DESIGN.md §4)
# Serve the local LDAP byte-pump socket for SSSD/PAM (DESIGN.md ??4)
ldap_tunnel: true
# Render OpenBao secrets to local files from /etc/theta/templates (DESIGN.md §5)
# Render OpenBao secrets to local files from /etc/theta/templates (DESIGN.md ??5)
secrets: true
# Apply node IAM (sudo rules, SSH keys, access control, revocation) (DESIGN.md §6)
# Apply node IAM (sudo rules, SSH keys, access control, revocation) (DESIGN.md ??6)
iam: true
# Accept signed wireguard_apply/wireguard_remove commands (DESIGN-WINDOWS.md §5)
# Accept signed wireguard_apply/wireguard_remove commands (DESIGN-WINDOWS.md ??5)
wireguard: false
# Secret templates to render (DESIGN.md §5). Each maps a local template to a
# Secret templates to render (DESIGN.md ??5). Each maps a local template to a
# target file and an optional post-render reload. The template embeds secrets as
# {{ bao "secret/data/nodes/<node-id>/<name>#<key>" }}.
# secrets:
@@ -84,7 +84,7 @@ capabilities:
# Advanced Capabilities (High risk, remote operations)
# ---------------------------------------------------------
# Allow remote system reboots via the SSO Manager
# Allow remote system reboots via Theta Directory
reboot: false
# Allow restarting, starting, or stopping specific systemd services.
@@ -93,6 +93,6 @@ capabilities:
# Setting to true or [] denies all.
service_control: []
# CRITICAL: Allow the execution of raw bash scripts sent from the SSO Manager.
# CRITICAL: Allow the execution of raw bash scripts sent from Theta Directory.
# Useful for GitOps deployments, but allows remote code execution.
arbitrary_bash: false
+9 -4
View File
@@ -7,6 +7,11 @@ DIST_DIR="./dist"
mkdir -p "$DIST_DIR"
LDFLAGS="-s -w"
# Windows GUI binaries (tray, helper) build as GUI-subsystem so no console
# window pops up when the installer starts the tray or the service spawns the
# helper. The agent stays a console app (handy for foreground debugging; as a
# Windows service it never shows a console anyway).
GUI_LDFLAGS="$LDFLAGS -H=windowsgui"
echo "Building Theta Agent binaries..."
@@ -26,10 +31,10 @@ echo " -> windows/arm64..."
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-windows-arm64.exe"
echo " -> windows/amd64 helper..."
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-helper-windows-amd64.exe" ./cmd/theta-agent-helper/
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$GUI_LDFLAGS" -o "$DIST_DIR/theta-agent-helper-windows-amd64.exe" ./cmd/theta-agent-helper/
echo " -> windows/arm64 helper..."
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-helper-windows-arm64.exe" ./cmd/theta-agent-helper/
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$GUI_LDFLAGS" -o "$DIST_DIR/theta-agent-helper-windows-arm64.exe" ./cmd/theta-agent-helper/
echo " -> darwin/amd64 (macOS Intel)..."
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-darwin-amd64"
@@ -45,10 +50,10 @@ echo " -> linux/arm64 tray..."
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-tray-linux-arm64" ./cmd/theta-agent-tray/
echo " -> windows/amd64 tray..."
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-tray-windows-amd64.exe" ./cmd/theta-agent-tray/
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$GUI_LDFLAGS" -o "$DIST_DIR/theta-agent-tray-windows-amd64.exe" ./cmd/theta-agent-tray/
echo " -> windows/arm64 tray..."
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o "$DIST_DIR/theta-agent-tray-windows-arm64.exe" ./cmd/theta-agent-tray/
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$GUI_LDFLAGS" -o "$DIST_DIR/theta-agent-tray-windows-arm64.exe" ./cmd/theta-agent-tray/
echo ""
echo "Build complete! Artifacts in $DIST_DIR:"
+1 -1
View File
@@ -55,7 +55,7 @@ func printUsage() {
fmt.Println(" theta-agent Run agent daemon in foreground")
fmt.Println(" theta-agent get-secret <key> Fetch single secret value from OpenBao")
fmt.Println(" theta-agent get-secrets [flags] Fetch all host/resource secrets (flags: --json, --env)")
fmt.Println(" theta-agent update Self-update binary from SSO Manager")
fmt.Println(" theta-agent update Self-update binary from Theta Directory")
fmt.Println(" theta-agent reinitialize [flags] Reset enrollment credentials & re-register")
fmt.Println(" theta-agent install-service (Windows) register the agent as a service")
fmt.Println(" theta-agent remove-service (Windows) unregister the agent service")
+29 -23
View File
@@ -9,11 +9,11 @@
; theta-agent-2.1.0-windows-amd64-setup.exe /SILENT ^
; /SERVER_URL=https://sso.example.com /JOIN_KEY=tjk_...
;
; Interactively, a wizard page asks for the SSO Manager URL and a join key (with
; a button that opens the SSO's Directory -> Install Agent page to mint one).
; In silent mode, /SERVER_URL, /JOIN_KEY, /AUTH_TOKEN, /PUBLIC_KEY and
; /B64_CONFIG (base64 of a full agent.yml) drive the same result. The values are
; written into agent.yml so the installed service enrolls on first start.
; Interactively, a wizard page asks for the Theta Directory URL and a join key
; (with a button that opens the Theta Directory's Directory -> Install Agent page
; to mint one). In silent mode, /SERVER_URL, /JOIN_KEY, /AUTH_TOKEN, /PUBLIC_KEY
; and /B64_CONFIG (base64 of a full agent.yml) drive the same result. The values
; are written into agent.yml so the installed service enrolls on first start.
#ifndef MyAppVersion
#define MyAppVersion "2.1.0"
@@ -128,8 +128,8 @@ begin
Result := True;
end;
// Opens the SSO's Directory page so the operator can mint a join key right from
// the wizard. Uses the Server URL they just typed.
// Opens the Theta Directory page so the operator can mint a join key right
// from the wizard. Uses the Server URL they just typed.
procedure OnOpenSSOClick(Sender: TObject);
var
Url: String;
@@ -137,7 +137,7 @@ var
begin
Url := Trim(ServerURLEdit.Text);
if Url = '' then begin
MsgBox('Enter the SSO Manager URL first (e.g. https://sso.example.com).',
MsgBox('Enter the Theta Directory URL first (e.g. https://directory.example.com).',
mbInformation, MB_OK);
Exit;
end;
@@ -150,48 +150,54 @@ var
InfoLabel: TNewStaticText;
UrlLabel: TNewStaticText;
KeyLabel: TNewStaticText;
Y: Integer;
begin
AgentConfigPage := CreateCustomPage(wpWelcome,
'SSO Manager connection',
'Tell the agent which SSO to enroll with.');
'Theta Directory connection',
'Tell the agent which Theta Directory to enroll with.');
InfoLabel := TNewStaticText.Create(AgentConfigPage);
InfoLabel.Parent := AgentConfigPage.Surface;
InfoLabel.WordWrap := True;
InfoLabel.Caption := 'Paste the SSO Manager URL for this deployment. Then either paste a join key '
InfoLabel.Caption := 'Paste the Theta Directory URL for this deployment. Then either paste a join key '
+ '(mint one with the button below, under Directory -> Install Agent) or leave it blank to '
+ 'enroll from the tray / CLI later.';
InfoLabel.AutoSize := True;
InfoLabel.Width := AgentConfigPage.Surface.Width;
// WordWrap + AutoSize are mutually exclusive in VCL; give the wrapped label a
// fixed height so the fields below it land on screen.
InfoLabel.Width := AgentConfigPage.SurfaceWidth;
InfoLabel.Height := ScaleY(48);
Y := InfoLabel.Top + InfoLabel.Height + ScaleY(12);
UrlLabel := TNewStaticText.Create(AgentConfigPage);
UrlLabel.Parent := AgentConfigPage.Surface;
UrlLabel.Caption := 'SSO Manager URL:';
UrlLabel.Top := InfoLabel.Top + InfoLabel.Height + 16;
UrlLabel.Caption := 'Theta Directory URL:';
UrlLabel.Top := Y;
ServerURLEdit := TNewEdit.Create(AgentConfigPage);
ServerURLEdit.Parent := AgentConfigPage.Surface;
ServerURLEdit.Top := UrlLabel.Top + UrlLabel.Height + 4;
ServerURLEdit.Width := AgentConfigPage.Surface.Width;
ServerURLEdit.Top := UrlLabel.Top + UrlLabel.Height + ScaleY(4);
ServerURLEdit.Width := AgentConfigPage.SurfaceWidth;
ServerURLEdit.Text := ServerURL;
OpenSSOButton := TNewButton.Create(AgentConfigPage);
OpenSSOButton.Parent := AgentConfigPage.Surface;
OpenSSOButton.Top := ServerURLEdit.Top + ServerURLEdit.Height + 8;
OpenSSOButton.Top := ServerURLEdit.Top + ServerURLEdit.Height + ScaleY(10);
OpenSSOButton.Left := ServerURLEdit.Left;
OpenSSOButton.Caption := 'Open SSO install-agent page...';
OpenSSOButton.Width := 190;
OpenSSOButton.Caption := 'Open Theta Directory install-agent page...';
OpenSSOButton.Width := WizardForm.CalculateButtonWidth([OpenSSOButton.Caption]);
OpenSSOButton.Height := ScaleY(23);
OpenSSOButton.OnClick := @OnOpenSSOClick;
KeyLabel := TNewStaticText.Create(AgentConfigPage);
KeyLabel.Parent := AgentConfigPage.Surface;
KeyLabel.Caption := 'Join key (optional):';
KeyLabel.Top := OpenSSOButton.Top + OpenSSOButton.Height + 12;
KeyLabel.Top := OpenSSOButton.Top + OpenSSOButton.Height + ScaleY(12);
JoinKeyEdit := TNewEdit.Create(AgentConfigPage);
JoinKeyEdit.Parent := AgentConfigPage.Surface;
JoinKeyEdit.Top := KeyLabel.Top + KeyLabel.Height + 4;
JoinKeyEdit.Width := AgentConfigPage.Surface.Width;
JoinKeyEdit.Top := KeyLabel.Top + KeyLabel.Height + ScaleY(4);
JoinKeyEdit.Width := AgentConfigPage.SurfaceWidth;
JoinKeyEdit.Text := JoinKey;
end;
+2 -2
View File
@@ -63,7 +63,7 @@ func runAgent() {
}
cfg := cm.Get()
log.Printf("Connecting to SSO Manager at %s", cfg.ServerURL)
log.Printf("Connecting to Theta Directory at %s", cfg.ServerURL)
log.Printf("Loaded capabilities: Telemetry=%v, LDAP=%v, Reboot=%v, Bash=%v",
cfg.Capabilities.Telemetry,
cfg.Capabilities.ConfigureLDAP,
@@ -83,7 +83,7 @@ func runAgent() {
// Tray IPC server — desktop tray connects here for status updates.
go globalTrayServer.Start()
// WebSocket connection to SSO Manager
// WebSocket connection to Theta Directory
go connectWebSocket(cm, exec)
// Home detection + tray status push (polls public IP every 60s).
+5 -2
View File
@@ -251,13 +251,16 @@ function Invoke-Build {
$dist = Join-Path $RepoRoot 'dist'
New-Item -ItemType Directory -Force -Path $dist | Out-Null
$flags = '-s -w'
# The tray and helper are GUI-subsystem binaries: no console window pops up
# when the installer starts the tray or the service spawns the helper.
$guiFlags = '-s -w -H=windowsgui'
foreach ($arch in @('amd64','arm64')) {
$env:GOOS='windows'; $env:GOARCH=$arch; $env:CGO_ENABLED='0'
go build "-ldflags=$flags" -o (Join-Path $dist "theta-agent-windows-$arch.exe") $RepoRoot
if ($LASTEXITCODE -ne 0) { Write-Fail "build agent windows/$arch failed"; return }
go build "-ldflags=$flags" -o (Join-Path $dist "theta-agent-tray-windows-$arch.exe") (Join-Path $RepoRoot 'cmd\theta-agent-tray')
go build "-ldflags=$guiFlags" -o (Join-Path $dist "theta-agent-tray-windows-$arch.exe") (Join-Path $RepoRoot 'cmd\theta-agent-tray')
if ($LASTEXITCODE -ne 0) { Write-Fail "build tray windows/$arch failed"; return }
go build "-ldflags=$flags" -o (Join-Path $dist "theta-agent-helper-windows-$arch.exe") (Join-Path $RepoRoot 'cmd\theta-agent-helper')
go build "-ldflags=$guiFlags" -o (Join-Path $dist "theta-agent-helper-windows-$arch.exe") (Join-Path $RepoRoot 'cmd\theta-agent-helper')
if ($LASTEXITCODE -ne 0) { Write-Fail "build helper windows/$arch failed"; return }
}
Remove-Item Env:GOOS,Env:GOARCH,Env:CGO_ENABLED -ErrorAction SilentlyContinue
+7 -1
View File
@@ -59,7 +59,13 @@ func installService() {
logFatal("cannot create service: %v", err)
}
defer s.Close()
fmt.Printf("[+] Registered theta-agent service (%s)\n", filepath.Base(exe))
// Start it immediately so the daemon binds the tray IPC socket and the
// credential provider is live without waiting for a reboot.
if err := s.Start(); err != nil {
logFatal("cannot start service: %v", err)
}
fmt.Printf("[+] Registered and started theta-agent service (%s)\n", filepath.Base(exe))
}
func removeService() {
+1 -1
View File
@@ -663,6 +663,6 @@ func pushDiscovery(c MessageWriter, cfg *Config) {
if err := c.WriteMessage(websocket.TextMessage, payload); err != nil {
log.Printf("Failed to send discovery data: %v", err)
} else {
log.Println("Discovery data pushed to SSO Manager.")
log.Println("Discovery data pushed to Theta Directory.")
}
}
+4 -4
View File
@@ -146,7 +146,7 @@ func connectWebSocket(cm *ConfigManager, exec Executor) {
// credential every 5s just floods the SSO and its audit log
// forever, so back off hard and say plainly what is wrong.
if resp != nil && (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden) {
log.Printf("Server rejected our token (HTTP %d). Enroll this agent in the SSO Directory and put the issued token in agent.yml. Retrying in %s.", resp.StatusCode, authRetryInterval)
log.Printf("Server rejected our token (HTTP %d). Enroll this agent in the Theta Directory and put the issued token in agent.yml. Retrying in %s.", resp.StatusCode, authRetryInterval)
time.Sleep(authRetryInterval)
continue
}
@@ -155,7 +155,7 @@ func connectWebSocket(cm *ConfigManager, exec Executor) {
continue
}
log.Println("Successfully connected to SSO Manager.")
log.Println("Successfully connected to Theta Directory.")
wsConnected.Store(true)
stopCh := make(chan struct{})
@@ -213,7 +213,7 @@ func connectWebSocket(cm *ConfigManager, exec Executor) {
// than at Dial.
if websocket.IsCloseError(err, closeUnauthorized, closeRevoked, closeTokenRotated) {
authRejected = true
log.Printf("Server closed the connection: %v. This agent's token is not valid for that SSO — re-enroll it and update agent.yml.", err)
log.Printf("Server closed the connection: %v. This agent's token is not valid for that Theta Directory — re-enroll it and update agent.yml.", err)
} else {
log.Println("WebSocket read error:", err)
}
@@ -345,7 +345,7 @@ func handleCommand(cm *ConfigManager, msg WSMessage, c MessageWriter, exec Execu
log.Printf("Enrolled, but could not persist credentials: %v", err)
log.Printf("This agent will re-enroll on every reconnect until %s is writable.", cm.configPath)
} else {
log.Printf("Enrolled with the SSO. Credentials written to %s; the join key is no longer needed.", cm.configPath)
log.Printf("Enrolled with Theta Directory. Credentials written to %s; the join key is no longer needed.", cm.configPath)
}
sendResponse("ok", "enrollment stored")
return