Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d1cfd08a5 | |||
| 52379c2434 | |||
| 48d17e0e9f | |||
| 6500fadafb | |||
| 6348c4c060 |
@@ -5,6 +5,11 @@ All notable changes to the `theta-agent` daemon will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v1.3.0] - 2026-08-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Silently ignore `heartbeat_ack`** — the server replies to the agent's own periodic heartbeat with `heartbeat_ack`. The agent had no case for it, so it fell through to the unknown-command handler, logged `Unknown command type: heartbeat_ack` every minute, and answered with a spurious error response. Heartbeat acks are fire-and-forget; the agent now ignores them silently.
|
||||||
|
|
||||||
## [v1.2.0] - 2026-08-03
|
## [v1.2.0] - 2026-08-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+35
-1
@@ -23,10 +23,35 @@ if [ "$EUID" -ne 0 ]; then
|
|||||||
error "This script must be run as root."
|
error "This script must be run as root."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install SSSD and PAM integration packages if missing
|
||||||
|
install_sssd_deps() {
|
||||||
|
if ! command -v sssd >/dev/null 2>&1; then
|
||||||
|
log "Installing SSSD and PAM integration dependencies..."
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get update -qq || true
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sssd sssd-ldap libnss-sss libpam-sss libsss-sudo pam-auth-update || true
|
||||||
|
if command -v pam-auth-update >/dev/null 2>&1; then
|
||||||
|
pam-auth-update --enable mkhomedir || true
|
||||||
|
fi
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y sssd sssd-ldap sssd-tools || true
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y sssd sssd-ldap sssd-tools || true
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
|
pacman -S --noconfirm sssd || true
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then
|
||||||
|
zypper in -y sssd || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "SSSD is already installed."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# 2. Argument Parsing
|
# 2. Argument Parsing
|
||||||
URL=""
|
URL=""
|
||||||
TOKEN=""
|
TOKEN=""
|
||||||
B64_CONFIG=""
|
B64_CONFIG=""
|
||||||
|
INSTALL_SSSD=0
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
@@ -38,6 +63,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
TOKEN="$2"
|
TOKEN="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--install-sssd|--ldap)
|
||||||
|
INSTALL_SSSD=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
B64_CONFIG="$1"
|
B64_CONFIG="$1"
|
||||||
shift
|
shift
|
||||||
@@ -50,7 +79,7 @@ if [ -z "$B64_CONFIG" ] && [ -z "$URL" ] || [ -z "$B64_CONFIG" ] && [ -z "$TOKEN
|
|||||||
error "Missing required configuration. Either provide a base64 encoded config, or both --url and --token."
|
error "Missing required configuration. Either provide a base64 encoded config, or both --url and --token."
|
||||||
echo "Usage examples:"
|
echo "Usage examples:"
|
||||||
echo " sh install.sh \"BASE64_CONFIG\""
|
echo " sh install.sh \"BASE64_CONFIG\""
|
||||||
echo " sh install.sh --url \"https://sso.local\" --token \"secret-token\""
|
echo " sh install.sh --url \"https://sso.local\" --token \"secret-token\" --install-sssd"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -86,6 +115,11 @@ EOF
|
|||||||
fi
|
fi
|
||||||
chmod 600 "$CONFIG_FILE"
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# 4b. Ensure SSSD dependencies are installed if configure_ldap is enabled
|
||||||
|
if [ "$INSTALL_SSSD" -eq 1 ] || grep -q -i "configure_ldap:\s*true" "$CONFIG_FILE" 2>/dev/null; then
|
||||||
|
install_sssd_deps
|
||||||
|
fi
|
||||||
|
|
||||||
# 5. Setup systemd service
|
# 5. Setup systemd service
|
||||||
log "Creating systemd service unit..."
|
log "Creating systemd service unit..."
|
||||||
cat <<EOF > "$SERVICE_FILE"
|
cat <<EOF > "$SERVICE_FILE"
|
||||||
|
|||||||
Binary file not shown.
+12
-1
@@ -139,7 +139,11 @@ func connectWebSocket(cm *ConfigManager, exec Executor) {
|
|||||||
|
|
||||||
func handleCommand(cm *ConfigManager, msg WSMessage, c MessageWriter, exec Executor) {
|
func handleCommand(cm *ConfigManager, msg WSMessage, c MessageWriter, exec Executor) {
|
||||||
cfg := cm.Get()
|
cfg := cm.Get()
|
||||||
log.Printf("Received command: %s", msg.Type)
|
// Don't log the server's fire-and-forget heartbeat ack — it arrives every
|
||||||
|
// 60s and is not a command to act on; logging it is pure per-minute noise.
|
||||||
|
if msg.Type != "heartbeat_ack" {
|
||||||
|
log.Printf("Received command: %s", msg.Type)
|
||||||
|
}
|
||||||
|
|
||||||
sendResponse := func(status string, message string) {
|
sendResponse := func(status string, message string) {
|
||||||
resp, _ := json.Marshal(map[string]string{"status": status, "message": message})
|
resp, _ := json.Marshal(map[string]string{"status": status, "message": message})
|
||||||
@@ -311,6 +315,13 @@ func handleCommand(cm *ConfigManager, msg WSMessage, c MessageWriter, exec Execu
|
|||||||
respPayload, _ := json.Marshal(resp)
|
respPayload, _ := json.Marshal(resp)
|
||||||
c.WriteMessage(websocket.TextMessage, respPayload)
|
c.WriteMessage(websocket.TextMessage, respPayload)
|
||||||
return
|
return
|
||||||
|
// heartbeat_ack is the server's acknowledgement of the agent's own periodic
|
||||||
|
// heartbeat (the agent sends `heartbeat`, the server answers `heartbeat_ack`).
|
||||||
|
// There is nothing to do with it -- it is not a command to run, and answering
|
||||||
|
// an ack with an error response would inject spurious errors into the
|
||||||
|
// command-response channel every minute. Silently ignore.
|
||||||
|
case "heartbeat_ack":
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
log.Printf("Unknown command type: %s", msg.Type)
|
log.Printf("Unknown command type: %s", msg.Type)
|
||||||
sendResponse("error", "unknown command type")
|
sendResponse("error", "unknown command type")
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ func TestHandleCommand(t *testing.T) {
|
|||||||
expectedCmd []string
|
expectedCmd []string
|
||||||
expectedFile string
|
expectedFile string
|
||||||
expectedFileCont string
|
expectedFileCont string
|
||||||
|
// heartbeat_ack (and any fire-and-forget ack) must be silently ignored —
|
||||||
|
// no response message, no command, no log noise.
|
||||||
|
expectedNoResponse bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "config command success",
|
name: "config command success",
|
||||||
@@ -175,6 +178,16 @@ func TestHandleCommand(t *testing.T) {
|
|||||||
expectedStatus: "error",
|
expectedStatus: "error",
|
||||||
expectedCmd: nil,
|
expectedCmd: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "heartbeat_ack is silently ignored",
|
||||||
|
cfg: &Config{
|
||||||
|
Capabilities: Capabilities{},
|
||||||
|
},
|
||||||
|
msg: WSMessage{
|
||||||
|
Type: "heartbeat_ack",
|
||||||
|
},
|
||||||
|
expectedNoResponse: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unknown command",
|
name: "unknown command",
|
||||||
cfg: &Config{
|
cfg: &Config{
|
||||||
@@ -194,6 +207,16 @@ func TestHandleCommand(t *testing.T) {
|
|||||||
cm := &ConfigManager{current: tc.cfg}
|
cm := &ConfigManager{current: tc.cfg}
|
||||||
handleCommand(cm, tc.msg, mockConn, mockExec)
|
handleCommand(cm, tc.msg, mockConn, mockExec)
|
||||||
|
|
||||||
|
if tc.expectedNoResponse {
|
||||||
|
if len(mockConn.Messages) != 0 {
|
||||||
|
t.Fatalf("expected no response message, got %d: %v", len(mockConn.Messages), mockConn.Messages)
|
||||||
|
}
|
||||||
|
if len(mockExec.ExecutedCommands) > 0 {
|
||||||
|
t.Errorf("expected no commands to be executed, but got %v", mockExec.ExecutedCommands)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(mockConn.Messages) != 1 {
|
if len(mockConn.Messages) != 1 {
|
||||||
t.Fatalf("expected 1 response message, got %d", len(mockConn.Messages))
|
t.Fatalf("expected 1 response message, got %d", len(mockConn.Messages))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user