From 5013148ffe961aa5d5cad0ca5956c7407e197225 Mon Sep 17 00:00:00 2001 From: wmantly Date: Sun, 9 Aug 2026 20:33:06 -0700 Subject: [PATCH] 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. --- agent.yml.example | 24 +++++++-------- build_all.sh | 13 ++++++--- cli.go | 2 +- installer/windows/installer.iss | 52 ++++++++++++++++++--------------- main.go | 4 +-- scripts/setup-build-env.ps1 | 7 +++-- service_cli_windows.go | 8 ++++- telemetry.go | 2 +- websocket.go | 8 ++--- 9 files changed, 70 insertions(+), 50 deletions(-) diff --git a/agent.yml.example b/agent.yml.example index acc4606..198df26 100644 --- a/agent.yml.example +++ b/agent.yml.example @@ -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//#" }}. # 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 diff --git a/build_all.sh b/build_all.sh index e549580..00233d6 100755 --- a/build_all.sh +++ b/build_all.sh @@ -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:" diff --git a/cli.go b/cli.go index e114254..b5f850a 100644 --- a/cli.go +++ b/cli.go @@ -55,7 +55,7 @@ func printUsage() { fmt.Println(" theta-agent Run agent daemon in foreground") fmt.Println(" theta-agent get-secret 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") diff --git a/installer/windows/installer.iss b/installer/windows/installer.iss index 4105876..06a737a 100644 --- a/installer/windows/installer.iss +++ b/installer/windows/installer.iss @@ -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; diff --git a/main.go b/main.go index 50026ce..437796b 100644 --- a/main.go +++ b/main.go @@ -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). diff --git a/scripts/setup-build-env.ps1 b/scripts/setup-build-env.ps1 index 51aeb21..483b305 100644 --- a/scripts/setup-build-env.ps1 +++ b/scripts/setup-build-env.ps1 @@ -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 diff --git a/service_cli_windows.go b/service_cli_windows.go index 7732eb4..7e6d963 100644 --- a/service_cli_windows.go +++ b/service_cli_windows.go @@ -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() { diff --git a/telemetry.go b/telemetry.go index 3917709..edebd86 100644 --- a/telemetry.go +++ b/telemetry.go @@ -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.") } } diff --git a/websocket.go b/websocket.go index e073f72..1f1045f 100644 --- a/websocket.go +++ b/websocket.go @@ -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