7 Commits

Author SHA1 Message Date
wmantly 9fdbb8aab0 feat(mdns): Linux local-discovery -- skip the WAN relay when on-site
Implements the Linux half of AGENT_LOCAL_DISCOVERY_SPEC.md: when a local
theta-gateway/theta-proxy announces itself via mDNS as fronting this
agent's ServerURL host, skip the relay/WAN path and talk to it directly.
Off by default (config.PreferLocalDirectory / prefer_local_directory)
since it changes host name resolution.

- local_discovery.go: polls for _theta-suite._tcp every 30s via
  hashicorp/mdns, matches the TXT "hosts" field against the agent's own
  target host, applies/clears a hosts-file override on change. Presence/
  absence of the mDNS announcement IS the "on this LAN or not" signal --
  no separate network detection needed, since multicast doesn't cross
  routers/VLANs.
- hosts_override.go: writes a single marked, idempotent block into
  /etc/hosts (never touches anything else in the file); clearing removes
  the block entirely rather than leaving empty markers.
- HARD RULE preserved: this only ever changes DNS resolution, never TLS
  trust -- nothing here touches certificate validation, so a spoofed
  rogue mDNS announcement produces a TLS failure against the real
  hostname's cert, not a silent MITM.

Verified end-to-end with real containers (Node mDNS announcer + this
actual Go binary, not mocked), which caught two real bugs neither showed
up in code review:

1. mdns.Lookup()'s DefaultParams() requests both IPv4 and IPv6. The
   underlying client sends the v4 query (which got a real, valid
   response per a packet capture), then sends the v6 query, and if THAT
   send fails (no IPv6 route -- common on plain v4 hosts/containers) the
   whole Query() returns that error synchronously, before ever entering
   the response-listening loop. The v4 response was silently discarded.
   Fixed by building QueryParam manually with DisableIPv6: true instead
   of using the Lookup() convenience wrapper.
2. The original hosts-file writer used write-tmp-then-rename for
   atomicity. /etc/hosts is frequently a bind mount (every container
   runtime does this) -- rename() onto a bind-mounted file fails with
   EBUSY, since you can't atomically replace a mountpoint. Switched to
   truncate-and-rewrite in place; the process-local mutex already
   serializes writers, so the lost atomicity is a small, acceptable
   tradeoff against a confirmed hard failure.

Full cycle verified: announcer starts -> agent discovers it -> hosts
override applied -> announcer stops -> override cleanly reverts, no
stale entry, no discovery trace left.

Windows/macOS remain unbuilt -- need platform-native testing this
environment can't do (see AGENT_LOCAL_DISCOVERY_SPEC.md §3's open
question: hosts-file edits vs. a local stub resolver, per-OS elevation
and DNS-cache behavior).
2026-08-10 17:54:39 -04:00
wmantly 4a619f7adc feat(windows): platform ops, service wrapper, helper, and air-gap paths
First Windows parity milestone (DESIGN-WINDOWS.md §13 build order item 1).

- Add a PlatformOps abstraction so command dispatch is OS-neutral:
  - linuxPlatformOps keeps today's systemctl/journalctl/bash behavior (deliberately
    untagged so shared dispatch tests run on Windows CI)
  - windowsPlatformOps maps reboot/shutdown to shutdown.exe, service control to
    sc.exe (stop+start for restart), fetch_logs to Get-WinEvent, arbitrary_bash to
    powershell -EncodedCommand (byte-exact under arbitrary quoting), and declines
    configure_ldap (Windows logon goes through OpenCredential)
- Run theta-agent as a Windows service (x/sys/windows/svc): SYSTEM auto-start,
    SCM stop/shutdown handling; CLI install-service/remove-service via svc/mgr
- Add theta-agent-helper (session-0 companion): lock/display_off/logout via
    user32/wtsapi32, and staged self-update (wait for service stop, swap the
    locked exe, sc start)
- Self-update becomes platform-aware: Linux renames over the running binary;
    Windows stages .new and hands the swap to the helper (running exe is locked)
- Platform paths: agent.yml and tray.sock under %ProgramData%\Theta42 (the
    service runs as SYSTEM while the tray runs as the user, so the per-user temp
    dir no longer works for tray IPC); LDAP byte-pump falls back to TCP loopback
- config: service_name, desktop_helper, public_ip_detect (air-gap: skips
    external public-IP lookups in telemetry + home monitor), wireguard block
- cli: platform-aware config path + self-update artifact name + service restart
- tests: dispatch tests pin linuxPlatformOps; 0600 mode assertions gated to
    POSIX so the suite is green on Windows

Rebuilds all tracked dist binaries (v2.1.0).
2026-08-09 17:10:07 -07:00
wmantly c676c658ed feat(tray): desktop tray icon companion with Theta logo, color-coded status, and home LAN detection (#10)
- tray_icons.go / cmd/theta-agent-tray: Renders iconic Theta 42 logo in status colors:
    - 🔴 Red: Not connected to directory
    - 🟡 Yellow: Connected to directory, but not on home LAN
    - 🟢 Green: Connected to directory on home LAN
    - 🔵 Blue: Connected to directory with active WireGuard tunnel
- home_detect.go: Compares agent public IP with home site public IP
- tray_server.go / tray_ipc.go: Unix socket IPC daemon server (/run/theta/tray.sock or /tmp/theta-tray.sock)
- cmd/theta-agent-tray: Desktop GUI binary with system tray menu (Auto-connect toggle, Connect/Disconnect VPN)
- Auto-detects DISPLAY / WAYLAND_DISPLAY environment variables
2026-08-08 23:16:20 -04:00
wmantly 8f0158eb9f Add LDAP byte-pump tunnel, secrets rendering, and IAM engine
See CHANGELOG.md for the full breakdown. Summary:

- ldap_tunnel.go: serves a local unix socket for SSSD/PAM and relays raw
  bytes to the SSO over the existing WSS channel (ldap_tunnel messages);
  the agent never parses LDAP (DESIGN.md §4). Adds safeWriter to
  serialize WebSocket writes now that telemetry, heartbeat, the LDAP
  tunnel, and command responses all share one connection.
- secrets.go: renders local templates ({{ bao "path#key" }} placeholders)
  by fetching node-scoped values from the SSO and writing the target
  atomically at 0600, on a signed render_secrets command (DESIGN.md §5).
  demo/ has minimal bash + Node consumers of the rendered file.
- iam.go: applies signed node IAM pushes -- sudoers.d rules (visudo -c
  validated), SSH AuthorizedKeysCommand keys, /etc/security/access.conf,
  and revocation via sss_cache -E + pkill -u (DESIGN.md §6).
- Capability reporting: the agent's enabled capabilities ride along in
  its discovery frame so the SSO can show them in the Directory.
- DESIGN.md: the v2 protocol design this implements.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 17:12:58 -04:00
wmantly 0493076575 Enterprise release: add self-updates, command signing, and telemetry enhancements 2026-08-03 01:59:27 -04:00
wmantly 8a97bc7fbd feat: websocket client and command router 2026-08-02 01:39:48 -04:00
wmantly 7150a6dcd9 feat: initial theta-agent repository structure and security model 2026-08-02 01:33:38 -04:00