Separate agent remediation runtime

This commit is contained in:
Pulse Test
2026-08-29 23:47:00 +01:00
parent 1720fd635b
commit d607d5cf46
80 changed files with 6418 additions and 82 deletions
+8
View File
@@ -144,6 +144,10 @@ for target in "${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}"; do
task_components+=(agent-helper)
task_targets+=("${target}")
done
for target in "${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}"; do
task_components+=(agent-runner)
task_targets+=("${target}")
done
if [[ "${PROFILE}" == "full" ]]; then
for target in "${PULSE_RELEASE_SERVER_TARGETS[@]}"; do
task_components+=(server)
@@ -169,6 +173,10 @@ build_one() {
;;
agent-helper)
package=./cmd/pulse-agent-helper
ldflags="${agent_ldflags}"
;;
agent-runner)
package=./cmd/pulse-agent-runner
ldflags=""
;;
mcp)
+31
View File
@@ -127,6 +127,7 @@ agent_ldflags="$(./scripts/release_ldflags.sh agent --version "v${VERSION}" "${u
echo "Building unified agents for all platforms..."
agent_build_order=("${PULSE_RELEASE_AGENT_TARGETS[@]}")
agent_helper_build_order=("${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}")
agent_runner_build_order=("${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}")
if [[ -n "${compiled_payload_dir:-}" ]]; then
test -d "${compiled_payload_dir}/binaries" || {
@@ -150,10 +151,21 @@ else
build_env="$(pulse_release_target_env "${target}")"
output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent-helper "${target}")"
env ${build_env} go build \
-ldflags="${agent_ldflags}" \
"${release_go_build_args[@]}" \
-o "${output_path}" \
./cmd/pulse-agent-helper
done
echo "Building action runners for Linux..."
for target in "${agent_runner_build_order[@]}"; do
build_env="$(pulse_release_target_env "${target}")"
output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent-runner "${target}")"
env ${build_env} go build \
"${release_go_build_args[@]}" \
-o "${output_path}" \
./cmd/pulse-agent-runner
done
fi
# Platform-native signing jobs may supply replacement desktop binaries. They
@@ -248,6 +260,12 @@ for target in "${agent_helper_build_order[@]}"; do
exit 1
}
done
for target in "${agent_runner_build_order[@]}"; do
test -f "${BUILD_DIR}/$(pulse_release_binary_filename agent-runner "${target}")" || {
echo "Error: release payload is missing agent runner binary for ${target}." >&2
exit 1
}
done
for target in "${build_order[@]}"; do
test -f "${BUILD_DIR}/$(pulse_release_binary_filename server "${target}")" || {
echo "Error: release payload is missing server binary for ${target}." >&2
@@ -318,6 +336,9 @@ done
for target in "${agent_helper_build_order[@]}"; do
cp "$BUILD_DIR/pulse-agent-helper-${target}" "$universal_dir/bin/pulse-agent-helper-${target}"
done
for target in "${agent_runner_build_order[@]}"; do
cp "$BUILD_DIR/pulse-agent-runner-${target}" "$universal_dir/bin/pulse-agent-runner-${target}"
done
cp "scripts/install-container-agent.sh" "$universal_dir/scripts/install-container-agent.sh"
cp "scripts/install-docker.sh" "$universal_dir/scripts/install-docker.sh"
@@ -404,6 +425,11 @@ for target in "${agent_helper_build_order[@]}"; do
tar -czf "$RELEASE_DIR/pulse-agent-helper-v${VERSION}-${target}.tar.gz" -C "$BUILD_DIR" "pulse-agent-helper-${target}"
done
# Package the separately enabled action runner (Linux only).
for target in "${agent_runner_build_order[@]}"; do
tar -czf "$RELEASE_DIR/pulse-agent-runner-v${VERSION}-${target}.tar.gz" -C "$BUILD_DIR" "pulse-agent-runner-${target}"
done
# Package standalone pulse-mcp binaries (all platforms). Mirrors
# the pulse-agent packaging shape exactly so the release-asset
# upload step does not need per-binary special cases.
@@ -443,6 +469,11 @@ for target in "${agent_helper_build_order[@]}"; do
cp "$BUILD_DIR/pulse-agent-helper-${target}" "$RELEASE_DIR/"
done
# Copy bare action runner binaries for the signed installer download endpoint.
for target in "${agent_runner_build_order[@]}"; do
cp "$BUILD_DIR/pulse-agent-runner-${target}" "$RELEASE_DIR/"
done
# Copy bare pulse-mcp binaries for /releases/latest/download/ redirect
# compatibility. The install-mcp.sh installer fetches these directly from
# the GitHub Releases endpoint without needing a versioned URL.
+874 -11
View File
File diff suppressed because it is too large Load Diff
@@ -196,6 +196,9 @@ done
for target in "${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}"; do
printf 'helper:%s:%s\n' "${target}" "$(pulse_release_binary_filename agent-helper "${target}")"
done
for target in "${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}"; do
printf 'runner:%s:%s\n' "${target}" "$(pulse_release_binary_filename agent-runner "${target}")"
done
`, "pulse-agent-helper-target-test", targetScriptPath)
targetOutput, err := targetCmd.CombinedOutput()
if err != nil {
@@ -204,6 +207,7 @@ done
var linuxAgentTargets []string
var helperTargets []string
var runnerTargets []string
for _, line := range strings.Split(strings.TrimSpace(string(targetOutput)), "\n") {
switch {
case strings.HasPrefix(line, "agent:"):
@@ -218,11 +222,24 @@ done
if parts[2] != wantFilename {
t.Fatalf("helper target %s filename = %s, want %s", parts[1], parts[2], wantFilename)
}
case strings.HasPrefix(line, "runner:"):
parts := strings.Split(line, ":")
if len(parts) != 3 {
t.Fatalf("unexpected runner target output %q", line)
}
runnerTargets = append(runnerTargets, parts[1])
wantFilename := "pulse-agent-runner-" + parts[1]
if parts[2] != wantFilename {
t.Fatalf("runner target %s filename = %s, want %s", parts[1], parts[2], wantFilename)
}
}
}
if got, want := strings.Join(helperTargets, ","), strings.Join(linuxAgentTargets, ","); got != want {
t.Fatalf("helper target matrix = %s, want Linux Unified Agent matrix %s", got, want)
}
if got, want := strings.Join(runnerTargets, ","), strings.Join(linuxAgentTargets, ","); got != want {
t.Fatalf("runner target matrix = %s, want Linux Unified Agent matrix %s", got, want)
}
buildBytes, err := os.ReadFile(repoFile("scripts", "build-release.sh"))
if err != nil {
@@ -249,6 +266,7 @@ done
`agent_helper_build_order=("${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}")`,
`output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent-helper "${target}")"`,
`./cmd/pulse-agent-helper`,
`-ldflags="${agent_ldflags}"`,
`cp "$BUILD_DIR/pulse-agent-helper-${target}" "$universal_dir/bin/pulse-agent-helper-${target}"`,
`tar -czf "$RELEASE_DIR/pulse-agent-helper-v${VERSION}-${target}.tar.gz" -C "$BUILD_DIR" "pulse-agent-helper-${target}"`,
`cp "$BUILD_DIR/pulse-agent-helper-${target}" "$RELEASE_DIR/"`,
@@ -257,6 +275,18 @@ done
t.Fatalf("build-release.sh missing agent helper release wiring: %s", needle)
}
}
for _, needle := range []string{
`agent_runner_build_order=("${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}")`,
`output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent-runner "${target}")"`,
`./cmd/pulse-agent-runner`,
`cp "$BUILD_DIR/pulse-agent-runner-${target}" "$universal_dir/bin/pulse-agent-runner-${target}"`,
`tar -czf "$RELEASE_DIR/pulse-agent-runner-v${VERSION}-${target}.tar.gz" -C "$BUILD_DIR" "pulse-agent-runner-${target}"`,
`cp "$BUILD_DIR/pulse-agent-runner-${target}" "$RELEASE_DIR/"`,
} {
if !strings.Contains(buildScript, needle) {
t.Fatalf("build-release.sh missing agent runner release wiring: %s", needle)
}
}
for _, needle := range []string{
`for target in "${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}"; do`,
`task_components+=(agent-helper)`,
@@ -266,6 +296,15 @@ done
t.Fatalf("build-release-binaries.sh missing agent helper compilation wiring: %s", needle)
}
}
for _, needle := range []string{
`for target in "${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}"; do`,
`task_components+=(agent-runner)`,
`package=./cmd/pulse-agent-runner`,
} {
if !strings.Contains(compileScript, needle) {
t.Fatalf("build-release-binaries.sh missing action runner compilation wiring: %s", needle)
}
}
for _, needle := range []string{
`if [[ ${#PULSE_RELEASE_AGENT_HELPER_TARGETS[@]} -eq 0 ]]; then`,
`src="${agent_binary_dir}/pulse-agent-helper-${target}"`,
@@ -276,6 +315,15 @@ done
t.Fatalf("release_asset_common.sh missing agent helper packaging wiring: %s", needle)
}
}
for _, needle := range []string{
`if [[ ${#PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]} -eq 0 ]]; then`,
`src="${agent_binary_dir}/pulse-agent-runner-${target}"`,
`dest="${staging_dir}/bin/pulse-agent-runner-${target}"`,
} {
if !strings.Contains(commonScript, needle) {
t.Fatalf("release_asset_common.sh missing action runner packaging wiring: %s", needle)
}
}
helperStage := strings.Index(commonScript, `src="${agent_binary_dir}/pulse-agent-helper-${target}"`)
binSigning := strings.Index(commonScript, `pulse_release_sign_directory_assets "${staging_dir}/bin"`)
if helperStage < 0 || binSigning < 0 || helperStage > binSigning {
@@ -301,6 +349,12 @@ done
t.Fatalf("create-release.yml missing bare helper upload: %s", asset)
}
}
for _, target := range runnerTargets {
asset := "release/pulse-agent-runner-" + target
if !strings.Contains(workflow, asset) {
t.Fatalf("create-release.yml missing bare action runner upload: %s", asset)
}
}
for _, signatureGlob := range []string{
`release_upload_with_retry "${TAG}" release/*.sig --clobber`,
`release_upload_with_retry "${TAG}" release/*.sshsig --clobber`,
@@ -406,6 +460,12 @@ func TestReleaseContainerContextTreatsServerSignaturesAsArchitectureBound(t *tes
files[name+".sig"] = "shared-helper-signature-" + helperTarget
files[name+".sshsig"] = "shared-helper-ssh-signature-" + helperTarget
}
for _, runnerTarget := range []string{"linux-amd64", "linux-arm64", "linux-armv7", "linux-armv6", "linux-386"} {
name := "bin/pulse-agent-runner-" + runnerTarget
files[name] = "shared-runner-" + runnerTarget
files[name+".sig"] = "shared-runner-signature-" + runnerTarget
files[name+".sshsig"] = "shared-runner-ssh-signature-" + runnerTarget
}
if driftUniversalPayload {
files["scripts/install.sh"] = "drifted-agent-installer"
}
@@ -735,6 +795,11 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
`release/pulse-agent-helper-linux-armv7`,
`release/pulse-agent-helper-linux-armv6`,
`release/pulse-agent-helper-linux-386`,
`release/pulse-agent-runner-linux-amd64`,
`release/pulse-agent-runner-linux-arm64`,
`release/pulse-agent-runner-linux-armv7`,
`release/pulse-agent-runner-linux-armv6`,
`release/pulse-agent-runner-linux-386`,
`release/pulse-agent-freebsd-amd64`,
`release/pulse-agent-freebsd-arm64`,
`release/pulse-agent-windows-amd64.exe`,
+113 -1
View File
@@ -5640,6 +5640,8 @@ func TestInstallSHTypedPrivilegedHelperUnits(t *testing.T) {
set -euo pipefail
PRIVILEGED_HELPER_NAME="pulse-agent-helper"
PRIVILEGED_HELPER_SOCKET_PATH="/run/pulse-agent/helper.sock"
PRIVILEGED_HELPER_UPDATE_QUARANTINE_DIR="/var/lib/pulse-agent/update-quarantine"
PRIVILEGED_HELPER_STATE_DIR="/var/lib/pulse-agent-helper"
LEAST_PRIVILEGE_USER="pulse-agent"
` + extractInstallShellFunction(t, "render_privileged_helper_socket_unit") + `
` + extractInstallShellFunction(t, "render_privileged_helper_service_unit") + `
@@ -5682,6 +5684,8 @@ func TestInstallSHTypedPrivilegedHelperUnits(t *testing.T) {
"RestrictAddressFamilies=AF_UNIX",
"ProtectSystem=strict",
"ProtectHome=true",
"ReadOnlyPaths=/var/lib/pulse-agent/update-quarantine",
"ReadWritePaths=/var/lib/pulse-agent-helper /usr/local/bin",
} {
if !strings.Contains(service, required) {
t.Fatalf("typed helper service unit missing %q:\n%s", required, service)
@@ -5708,7 +5712,6 @@ func TestInstallSHTypedPrivilegedHelperProfileIsOptInAndFailClosed(t *testing.T)
`--enable-privileged-helper is supported only on standard Linux systemd hosts; no broader-privilege fallback was applied`,
`Preserving existing typed privileged-helper profile`,
`PULSE_AGENT_HELPER_SOCKET`,
`EXEC_ARG_ITEMS+=(--disable-auto-update)`,
`chown root:root "${INSTALL_DIR}/${BINARY_NAME}"`,
`chown root:root "$PRIVILEGED_HELPER_BINARY_PATH"`,
`chown -R "${LEAST_PRIVILEGE_USER}:${LEAST_PRIVILEGE_USER}" "$STATE_DIR"`,
@@ -5729,6 +5732,115 @@ func TestInstallSHTypedPrivilegedHelperProfileIsOptInAndFailClosed(t *testing.T)
t.Fatalf("install.sh missing typed-helper invariant: %s", required)
}
}
if strings.Contains(script, `EXEC_ARG_ITEMS+=(--disable-auto-update)`) {
t.Fatal("typed helper profile must keep updater enabled for signed helper-backed activation")
}
}
func TestInstallSHTypedPrivilegeHelperUpdateFilesystemBoundary(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
for _, required := range []string{
`PRIVILEGED_HELPER_STATE_DIR="/var/lib/pulse-agent-helper"`,
`PRIVILEGED_HELPER_UPDATE_STAGING_DIR="${PRIVILEGED_HELPER_STATE_DIR}/update-staging"`,
`PRIVILEGED_HELPER_UPDATE_QUARANTINE_DIR="/var/lib/pulse-agent/update-quarantine"`,
`ReadOnlyPaths=${PRIVILEGED_HELPER_UPDATE_QUARANTINE_DIR}`,
`ReadWritePaths=${PRIVILEGED_HELPER_STATE_DIR} /usr/local/bin`,
`install -d -o "$LEAST_PRIVILEGE_USER" -g "$LEAST_PRIVILEGE_USER" -m 0700`,
`install -d -o root -g root -m 0700`,
`Typed privileged-helper updates require the fixed /usr/local/bin/pulse-agent target`,
} {
if !strings.Contains(script, required) {
t.Fatalf("install.sh missing typed helper update boundary %q", required)
}
}
}
func TestInstallSHActionRunnerIsSeparateOptInLifecycle(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
for _, required := range []string{
`ACTION_RUNNER_ENABLED="false"`,
`ACTION_RUNNER_NAME="pulse-agent-runner"`,
`ACTION_RUNNER_TOKEN_FILE="${ACTION_RUNNER_CONFIG_DIR}/token"`,
`--enable-action-runner) ACTION_RUNNER_ENABLED="true"; ACTION_RUNNER_EXPLICIT="true"; shift ;;`,
`--disable-action-runner) ACTION_RUNNER_ENABLED="false"; ACTION_RUNNER_EXPLICIT="true"; shift ;;`,
`--uninstall-action-runner) UNINSTALL_ACTION_RUNNER="true"; shift ;;`,
`--enable-action-runner requires the safe --least-privilege --enable-privileged-helper collector profile`,
`--enable-action-runner cannot be combined with legacy collector --enable-commands`,
`--action-token-file requires --enable-action-runner (or an existing preserved runner profile)`,
`The action runner must use a separate credential from the collector token`,
`Preserving existing separately enabled action-runner profile`,
`write_action_runner_env_value "PULSE_AGENT_RUNNER_TOKEN_FILE" "$ACTION_RUNNER_TOKEN_FILE"`,
`write_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID_FILE" "${STATE_DIR%/}/agent-id"`,
`write_action_runner_env_value "PULSE_AGENT_RUNNER_HEALTH_FILE" "$ACTION_RUNNER_HEALTH_FILE"`,
`/download/${ACTION_RUNNER_BINARY_NAME}?${DOWNLOAD_QUERY}`,
`verify_download_signature "$TMP_ACTION_RUNNER_BIN" "$runner_signature"`,
`install -o root -g root -m 0755 "$TMP_ACTION_RUNNER_BIN"`,
`chmod 0600 "$ACTION_RUNNER_TOKEN_FILE"`,
`ACTION_TOKEN=""`,
`health_mtime=$(stat -c '%Y' "$ACTION_RUNNER_HEALTH_FILE"`,
`"registered"[[:space:]]*:[[:space:]]*true`,
`"host_id"[[:space:]]*:[[:space:]]*`,
`[[ "$health_agent_id" == "$expected_agent_id" ]]`,
`rolling back runner-only files while leaving monitoring active`,
`Pulse action runner removed. Collector monitoring was left installed and running.`,
} {
if !strings.Contains(script, required) {
t.Fatalf("install.sh missing action-runner invariant: %s", required)
}
}
teardown := extractInstallShellFunction(t, "teardown_action_runner_service")
if strings.Contains(teardown, "teardown_systemd_agent_service") || strings.Contains(teardown, `rm -f "${INSTALL_DIR}/${BINARY_NAME}"`) {
t.Fatalf("runner teardown must not remove the monitoring collector:\n%s", teardown)
}
}
func TestInstallSHRendersHardenedActionRunnerUnit(t *testing.T) {
root := t.TempDir()
unitPath := filepath.Join(root, "pulse-agent-runner.service")
script := `
set -euo pipefail
ACTION_RUNNER_ENV_FILE="/etc/pulse-agent-runner/runner.env"
ACTION_RUNNER_STATE_DIR="/var/lib/pulse-agent-runner"
` + extractInstallShellFunction(t, "render_action_runner_service_unit") + `
render_action_runner_service_unit "` + unitPath + `" "/usr/local/lib/pulse-agent/pulse-agent-runner"
`
if out, err := exec.Command("bash", "-c", script).CombinedOutput(); err != nil {
t.Fatalf("render action runner unit: %v\n%s", err, out)
}
content, err := os.ReadFile(unitPath)
if err != nil {
t.Fatal(err)
}
unit := string(content)
for _, required := range []string{
"ExecStart=/usr/local/lib/pulse-agent/pulse-agent-runner",
"EnvironmentFile=/etc/pulse-agent-runner/runner.env",
"User=root",
"Group=root",
"NoNewPrivileges=true",
"ProtectHome=true",
"ProtectSystem=strict",
"ProtectKernelTunables=true",
"ProtectKernelModules=true",
"ProtectControlGroups=true",
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
"ReadWritePaths=/var/lib/pulse-agent-runner",
} {
if !strings.Contains(unit, required) {
t.Fatalf("action runner unit missing %q:\n%s", required, unit)
}
}
if strings.Contains(unit, "PrivateNetwork=true") {
t.Fatalf("networked action runner cannot use the helper's private-network sandbox:\n%s", unit)
}
}
func TestInstallSHTypedPrivilegedHelperProtectsCredentialsAfterStateChown(t *testing.T) {
@@ -0,0 +1,289 @@
package installtests
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func safeProfileInspectFunctions(t *testing.T) string {
t.Helper()
return extractInstallShellFunction(t, "safe_profile_platform_supported") + "\n" +
extractInstallShellFunction(t, "safe_profile_detect_current_profile") + "\n" +
extractInstallShellFunction(t, "safe_profile_unit_property") + "\n" +
extractInstallShellFunction(t, "safe_profile_inspect")
}
func safeProfileTransactionFunctions(t *testing.T) string {
t.Helper()
return extractInstallShellFunction(t, "safe_profile_detect_current_profile") + "\n" +
extractInstallShellFunction(t, "safe_profile_snapshot_entry") + "\n" +
extractInstallShellFunction(t, "safe_profile_manifest_value") + "\n" +
extractInstallShellFunction(t, "safe_profile_begin_transaction") + "\n" +
extractInstallShellFunction(t, "safe_profile_restore_entry") + "\n" +
extractInstallShellFunction(t, "safe_profile_restore_transaction") + "\n" +
extractInstallShellFunction(t, "safe_profile_commit_transaction")
}
func TestSafeProfileInspectIsReadOnlyAndReportsDifferences(t *testing.T) {
root := t.TempDir()
binDir := filepath.Join(root, "bin")
unitDir := filepath.Join(root, "systemd")
mustMkdirAll(t, binDir, unitDir)
binary := filepath.Join(binDir, "pulse-agent")
unit := filepath.Join(unitDir, "pulse-agent.service")
runner := filepath.Join(unitDir, "pulse-agent-runner.service")
unitBody := "[Service]\nUser=root\nAmbientCapabilities=CAP_SETUID CAP_SETGID\nExecStart=" + binary + " --enable-host --enable-docker --enable-proxmox --enable-commands\n"
mustWrite(t, binary, "collector-before\n")
mustWrite(t, unit, unitBody)
mustWrite(t, runner, "runner-independent\n")
harness := `
set -euo pipefail
AGENT_NAME=pulse-agent
BINARY_NAME=pulse-agent
INSTALL_DIR="` + binDir + `"
LEAST_PRIVILEGE_USER=pulse-agent
SAFE_PROFILE_COLLECTOR_UNIT="` + unit + `"
ACTION_RUNNER_SERVICE_UNIT="` + runner + `"
log_error() { printf 'ERROR:%s\n' "$*" >&2; }
uname() { printf 'Linux\n'; }
systemctl() {
if [[ "$1" == show ]]; then
case "$4" in User) printf 'root\n' ;; AmbientCapabilities) printf 'CAP_SETUID CAP_SETGID\n' ;; esac
fi
}
id() { if [[ "${1:-}" == -nG ]]; then printf 'root docker\n'; fi; return 0; }
` + safeProfileInspectFunctions(t) + `
safe_profile_inspect
`
out, err := exec.Command("bash", "-c", harness).CombinedOutput()
if err != nil {
t.Fatalf("inspect: %v\n%s", err, out)
}
for _, want := range []string{
"platform_supported=true", "current_profile=legacy-root-command-capable",
"unit_user=root", "unit_groups=root docker", "ambient_capabilities=CAP_SETUID CAP_SETGID",
"provider_docker=true", "provider_proxmox=true", "collector_commands=true",
"action_runner_independent=true", "target_profile=typed-helper-monitoring-only",
"target_groups=no-rootful-docker-group", "degraded_docker=rootful daemon access is removed",
"degraded_actions=collector command authority is removed",
} {
if !strings.Contains(string(out), want) {
t.Fatalf("inspect output missing %q:\n%s", want, out)
}
}
assertFileBody(t, binary, "collector-before\n")
assertFileBody(t, unit, unitBody)
assertFileBody(t, runner, "runner-independent\n")
}
func TestSafeProfileTransactionCommitAndFailureRollback(t *testing.T) {
t.Run("commit", func(t *testing.T) {
root := t.TempDir()
harness := safeProfileHarness(t, root, false) + `
safe_profile_begin_transaction
safe_profile_commit_transaction
grep -q '^PRIOR_PROFILE=legacy-root-command-capable$' "$SAFE_PROFILE_CURRENT_FILE"
grep -q '^CURRENT_PROFILE=typed-helper-monitoring-only$' "$SAFE_PROFILE_CURRENT_FILE"
grep -q "^TRANSACTION_DIR=${SAFE_PROFILE_TRANSACTION_DIR}$" "$SAFE_PROFILE_CURRENT_FILE"
test -f "${SAFE_PROFILE_TRANSACTION_DIR}/collector-binary"
test "$SAFE_PROFILE_TRANSACTION_ACTIVE" = false
test "$SAFE_PROFILE_TRANSACTION_COMMITTED" = true
`
if out, err := exec.Command("bash", "-c", harness).CombinedOutput(); err != nil {
t.Fatalf("commit rehearsal: %v\n%s", err, out)
}
})
t.Run("failure rollback", func(t *testing.T) {
root := t.TempDir()
harness := safeProfileHarness(t, root, true) + `
safe_profile_begin_transaction
transaction="$SAFE_PROFILE_TRANSACTION_DIR"
printf 'new-binary\n' > "$INSTALL_DIR/$BINARY_NAME"
printf '[Service]\nUser=pulse-agent\nEnvironment=PULSE_AGENT_HELPER_SOCKET=/run/pulse-agent/helper.sock\n' > "$SAFE_PROFILE_COLLECTOR_UNIT"
printf 'typed-helper\n' > "$PRIVILEGED_HELPER_BINARY_PATH"
printf 'helper-unit\n' > "$PRIVILEGED_HELPER_SERVICE_UNIT"
printf 'helper-socket\n' > "$PRIVILEGED_HELPER_SOCKET_UNIT"
rm -f "$STATE_DIR/token" "$STATE_DIR/runtime.token"
printf 'changed-agent-id\n' > "$STATE_DIR/agent-id"
printf 'changed-connection\n' > "$STATE_DIR/connection.env"
mkdir -p "$PRIVILEGED_HELPER_CREDENTIAL_DIR"
printf 'moved-monitoring-token\n' > "$PRIVILEGED_HELPER_CREDENTIAL_DIR/token"
printf 'runner-still-independent\n' > "$ACTION_RUNNER_SENTINEL"
safe_profile_restore_transaction "$transaction" automatic-failure
cmp "$INSTALL_DIR/$BINARY_NAME" "$EXPECTED_DIR/collector-binary"
cmp "$SAFE_PROFILE_COLLECTOR_UNIT" "$EXPECTED_DIR/collector-unit"
cmp "$STATE_DIR/token" "$EXPECTED_DIR/state-token"
cmp "$STATE_DIR/runtime.token" "$EXPECTED_DIR/runtime-token"
cmp "$STATE_DIR/agent-id" "$EXPECTED_DIR/agent-id"
cmp "$STATE_DIR/connection.env" "$EXPECTED_DIR/connection-env"
test ! -e "$PRIVILEGED_HELPER_BINARY_PATH"
test ! -e "$PRIVILEGED_HELPER_SERVICE_UNIT"
test ! -e "$PRIVILEGED_HELPER_SOCKET_UNIT"
test ! -e "$PRIVILEGED_HELPER_CREDENTIAL_DIR/token"
grep -q '^legacy sudo grant$' "$PRIVILEGE_SUDOERS_FILE"
grep -q '^legacy smart wrapper$' "$PRIVILEGE_HELPER_DIR/smartctl"
grep -q '^legacy pct wrapper$' "$PRIVILEGE_HELPER_DIR/pct"
grep -q '^runner-still-independent$' "$ACTION_RUNNER_SENTINEL"
grep -q '^CURRENT_PROFILE=legacy-root-command-capable$' "$SAFE_PROFILE_CURRENT_FILE"
grep -q '^gpasswd -a pulse-agent docker$' "$CALL_LOG"
`
if out, err := exec.Command("bash", "-c", harness).CombinedOutput(); err != nil {
t.Fatalf("failure rollback rehearsal: %v\n%s", err, out)
}
})
}
func TestSafeProfileMigrationIsExplicitAndRunnerIndependent(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatal(err)
}
script := string(content)
for _, want := range []string{
`--safe-profile-inspect) SAFE_PROFILE_ACTION="inspect"`,
`--safe-profile-apply) SAFE_PROFILE_ACTION="apply"`,
`--safe-profile-rollback) SAFE_PROFILE_ACTION="rollback"`,
`# Explicit safe-profile migration lifecycle. Ordinary --update deliberately`,
`safe_profile_verify_declared_health`, `safe_profile_commit_transaction`,
`"$SAFE_PROFILE_ACTION" != "apply"`, `target_action_runner=unchanged`,
} {
if !strings.Contains(script, want) {
t.Fatalf("installer missing migration invariant %q", want)
}
}
rollback := extractInstallShellFunction(t, "safe_profile_restore_transaction")
for _, forbidden := range []string{"ACTION_RUNNER_BINARY_PATH", "ACTION_RUNNER_SERVICE_UNIT", "teardown_action_runner_service", "provision_action_runner"} {
if strings.Contains(rollback, forbidden) {
t.Fatalf("collector rollback touched independent runner through %q", forbidden)
}
}
}
func TestSafeProfileApplyRequiresReadinessHelperAndRegistration(t *testing.T) {
gate := extractInstallShellFunction(t, "safe_profile_verify_declared_health")
script := `
set -euo pipefail
AGENT_NAME=pulse-agent
PRIVILEGED_HELPER_NAME=pulse-agent-helper
resolve_agent_health_url() { printf 'http://127.0.0.1:9191/readyz\n'; }
curl() { return 0; }
systemctl() { return 0; }
verify_agent_server_registration_with_retry() { return 0; }
` + gate + `
safe_profile_verify_declared_health
verify_agent_server_registration_with_retry() { return 1; }
if safe_profile_verify_declared_health; then
echo 'registration failure was accepted' >&2
exit 1
fi
`
if out, err := exec.Command("bash", "-c", script).CombinedOutput(); err != nil {
t.Fatalf("health gate rehearsal: %v\n%s", err, out)
}
}
func safeProfileHarness(t *testing.T, root string, dockerMember bool) string {
t.Helper()
binDir := filepath.Join(root, "bin")
unitDir := filepath.Join(root, "systemd")
helperDir := filepath.Join(root, "helper")
stateDir := filepath.Join(root, "state")
credentialDir := filepath.Join(root, "credential")
expectedDir := filepath.Join(root, "expected")
mustMkdirAll(t, binDir, unitDir, helperDir, stateDir, expectedDir)
files := map[string]string{
filepath.Join(binDir, "pulse-agent"): "old-binary\n",
filepath.Join(unitDir, "pulse-agent.service"): "[Service]\nUser=root\nAmbientCapabilities=CAP_SETUID CAP_SETGID\nExecStart=/bin/pulse-agent --enable-commands\n",
filepath.Join(root, "sudoers"): "legacy sudo grant\n",
filepath.Join(helperDir, "smartctl"): "legacy smart wrapper\n",
filepath.Join(helperDir, "pct"): "legacy pct wrapper\n",
filepath.Join(stateDir, "token"): "monitoring-token\n",
filepath.Join(stateDir, "runtime.token"): "runtime-monitoring-token\n",
filepath.Join(stateDir, "agent-id"): "stable-agent-id\n",
filepath.Join(stateDir, "connection.env"): "PULSE_URL='https://pulse.example'\n",
}
for path, body := range files {
mustWrite(t, path, body)
}
for source, name := range map[string]string{
filepath.Join(binDir, "pulse-agent"): "collector-binary",
filepath.Join(unitDir, "pulse-agent.service"): "collector-unit",
filepath.Join(stateDir, "token"): "state-token",
filepath.Join(stateDir, "runtime.token"): "runtime-token",
filepath.Join(stateDir, "agent-id"): "agent-id",
filepath.Join(stateDir, "connection.env"): "connection-env",
} {
body, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
mustWrite(t, filepath.Join(expectedDir, name), string(body))
}
membership := "pulse-agent"
if dockerMember {
membership = "pulse-agent docker"
}
return `
set -euo pipefail
AGENT_NAME=pulse-agent
BINARY_NAME=pulse-agent
INSTALL_DIR="` + binDir + `"
LEAST_PRIVILEGE_USER=pulse-agent
PRIVILEGE_HELPER_DIR="` + helperDir + `"
PRIVILEGE_SUDOERS_FILE="` + filepath.Join(root, "sudoers") + `"
PRIVILEGED_HELPER_BINARY_PATH="` + filepath.Join(helperDir, "pulse-agent-helper") + `"
PRIVILEGED_HELPER_SERVICE_UNIT="` + filepath.Join(unitDir, "pulse-agent-helper.service") + `"
PRIVILEGED_HELPER_SOCKET_UNIT="` + filepath.Join(unitDir, "pulse-agent-helper.socket") + `"
PRIVILEGED_HELPER_SOCKET_PATH="` + filepath.Join(root, "run", "helper.sock") + `"
PRIVILEGED_HELPER_NAME=pulse-agent-helper
PRIVILEGED_HELPER_CREDENTIAL_DIR="` + credentialDir + `"
SAFE_PROFILE_COLLECTOR_UNIT="` + filepath.Join(unitDir, "pulse-agent.service") + `"
SAFE_PROFILE_STATE_DIR="` + filepath.Join(root, "profile") + `"
SAFE_PROFILE_CURRENT_FILE="${SAFE_PROFILE_STATE_DIR}/current.env"
SAFE_PROFILE_TRANSACTION_DIR=""
SAFE_PROFILE_TRANSACTION_ACTIVE=false
SAFE_PROFILE_TRANSACTION_COMMITTED=false
STATE_DIR="` + stateDir + `"
ACTION_RUNNER_SENTINEL="` + filepath.Join(root, "runner-sentinel") + `"
EXPECTED_DIR="` + expectedDir + `"
CALL_LOG="` + filepath.Join(root, "calls.log") + `"
EXIT_GENERAL=1
EXIT_MISSING_ARGS=2
log_info() { :; }
log_error() { printf 'ERROR:%s\n' "$*" >&2; }
fail() { printf 'FAIL:%s\n' "$1" >&2; return "${2:-1}"; }
systemctl() { case "${1:-}" in is-active|is-enabled) return 0 ;; *) return 0 ;; esac; }
getent() { [[ "${1:-}" == group && "${2:-}" == docker ]]; }
id() { if [[ "${1:-}" == -nG ]]; then printf '` + membership + `\n'; fi; return 0; }
gpasswd() { printf 'gpasswd %s\n' "$*" >> "$CALL_LOG"; }
` + safeProfileTransactionFunctions(t) + "\n"
}
func mustMkdirAll(t *testing.T, dirs ...string) {
t.Helper()
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
}
}
func mustWrite(t *testing.T, path, body string) {
t.Helper()
if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
t.Fatal(err)
}
}
func assertFileBody(t *testing.T, path, want string) {
t.Helper()
body, err := os.ReadFile(path)
if err != nil || string(body) != want {
t.Fatalf("%s body=%q want=%q err=%v", path, body, want, err)
}
}
@@ -126,6 +126,15 @@ for arch in amd64 arm64; do
fi
done
done
for runner_target in linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386; do
for suffix in "" .sig .sshsig; do
required="bin/pulse-agent-runner-${runner_target}${suffix}"
if [[ ! -f "${output_dir}/${arch}/${required}" ]]; then
echo "Error: ${archive} is missing candidate container input ${required}." >&2
exit 1
fi
done
done
done
if ! diff -qr \
+9
View File
@@ -143,6 +143,10 @@ pulse_release_stage_server_archive() {
echo "Error: PULSE_RELEASE_AGENT_HELPER_TARGETS is empty." >&2
return 1
fi
if [[ ${#PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]} -eq 0 ]]; then
echo "Error: PULSE_RELEASE_AGENT_RUNNER_TARGETS is empty." >&2
return 1
fi
rm -rf "${staging_dir}"
mkdir -p "${staging_dir}/bin" "${staging_dir}/scripts"
@@ -162,6 +166,11 @@ pulse_release_stage_server_archive() {
dest="${staging_dir}/bin/pulse-agent-helper-${target}"
install -m 0755 "${src}" "${dest}"
done
for target in "${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}"; do
src="${agent_binary_dir}/pulse-agent-runner-${target}"
dest="${staging_dir}/bin/pulse-agent-runner-${target}"
install -m 0755 "${src}" "${dest}"
done
(
cd "${staging_dir}/bin"
ln -sf pulse-agent-windows-amd64.exe pulse-agent-windows-amd64
+9
View File
@@ -26,6 +26,14 @@ PULSE_RELEASE_AGENT_HELPER_TARGETS=(
linux-386
)
PULSE_RELEASE_AGENT_RUNNER_TARGETS=(
linux-amd64
linux-arm64
linux-armv7
linux-armv6
linux-386
)
PULSE_RELEASE_SERVER_TARGETS=(
linux-amd64
linux-arm64
@@ -68,6 +76,7 @@ pulse_release_binary_filename() {
case "${component}" in
agent) filename="pulse-agent-${target}" ;;
agent-helper) filename="pulse-agent-helper-${target}" ;;
agent-runner) filename="pulse-agent-runner-${target}" ;;
mcp) filename="pulse-mcp-${target}" ;;
server) filename="pulse-${target}" ;;
control-plane) filename="pulse-control-plane-${target}" ;;
@@ -585,6 +585,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"exact_files": [
"scripts/installtests/agent_state_dir_lifecycle_test.go",
"scripts/installtests/install_sh_test.go",
"scripts/installtests/safe_profile_migration_test.go",
],
}
],
@@ -610,6 +611,39 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"exact_files": [
"scripts/installtests/agent_state_dir_lifecycle_test.go",
"scripts/installtests/install_sh_test.go",
"scripts/installtests/safe_profile_migration_test.go",
],
}
],
)
def test_action_runner_runtime_uses_separate_typed_runner_policy(self):
required = infer_impacted_subsystems(
[
"cmd/pulse-agent-runner/main.go",
"internal/actionrunner/runner.go",
"internal/dockeragent/action_runtime.go",
]
)
self.assertEqual(set(required), {"agent-lifecycle"})
lifecycle = required["agent-lifecycle"]
self.assertEqual(
lifecycle["verification_requirements"],
[
{
"id": "action-runner-runtime",
"label": "separate typed action runner and durable receipt proof",
"touched_runtime_files": [
"cmd/pulse-agent-runner/main.go",
"internal/actionrunner/runner.go",
"internal/dockeragent/action_runtime.go",
],
"allow_same_subsystem_tests": True,
"test_prefixes": ["internal/actionrunner/"],
"exact_files": [
"cmd/pulse-agent-runner/main_test.go",
"internal/agentexec/server_websocket_test.go",
"internal/hostagent/action_runner_client_test.go",
],
}
],
@@ -1278,6 +1312,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"test_prefixes": ["frontend-modern/src/api/__tests__/"],
"exact_files": [
"frontend-modern/src/types/api.ts",
"internal/api/action_runner_credentials_test.go",
"internal/api/ai_handlers_more_test.go",
"internal/api/ai_handlers_patrol_actions_additional_test.go",
"internal/api/alerting/external_probe_notifications_test.go",
@@ -1441,6 +1441,7 @@ class SubsystemLookupTest(unittest.TestCase):
[
"scripts/installtests/agent_state_dir_lifecycle_test.go",
"scripts/installtests/install_sh_test.go",
"scripts/installtests/safe_profile_migration_test.go",
],
)
@@ -1459,6 +1460,7 @@ class SubsystemLookupTest(unittest.TestCase):
[
"scripts/installtests/agent_state_dir_lifecycle_test.go",
"scripts/installtests/install_sh_test.go",
"scripts/installtests/safe_profile_migration_test.go",
],
)
@@ -3511,6 +3513,37 @@ class SubsystemLookupTest(unittest.TestCase):
],
)
def test_lookup_paths_assigns_separate_action_runner_to_agent_lifecycle(self) -> None:
result = lookup_paths(
[
"cmd/pulse-agent-runner/main.go",
"internal/actionrunner/runner.go",
"internal/dockeragent/action_runtime.go",
]
)
self.assertEqual(result["unowned_runtime_files"], [])
self.assertEqual(
{item["subsystem"] for item in result["impacted_subsystems"]},
{"agent-lifecycle"},
)
for file_entry in result["files"]:
self.assertEqual(file_entry["classification"], "runtime")
self.assertEqual(len(file_entry["matches"]), 1)
match = file_entry["matches"][0]
self.assertEqual(match["subsystem"], "agent-lifecycle")
self.assertEqual(
match["verification_requirement"]["id"],
"action-runner-runtime",
)
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"cmd/pulse-agent-runner/main_test.go",
"internal/agentexec/server_websocket_test.go",
"internal/hostagent/action_runner_client_test.go",
],
)
def test_lookup_paths_reports_windows_installer_as_shared_boundary(self) -> None:
result = lookup_paths(["scripts/install.ps1"])
self.assertEqual(result["unowned_runtime_files"], [])
+11 -2
View File
@@ -254,7 +254,7 @@ if [ "$SKIP_DOCKER" = false ]; then
# Validate all required binaries exist and are non-empty
info "Checking downloadable binaries in /opt/pulse/bin/..."
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/bin; required="pulse pulse-agent-linux-amd64 pulse-agent-linux-arm64 pulse-agent-linux-armv7 pulse-agent-linux-armv6 pulse-agent-linux-386 pulse-agent-helper-linux-amd64 pulse-agent-helper-linux-arm64 pulse-agent-helper-linux-armv7 pulse-agent-helper-linux-armv6 pulse-agent-helper-linux-386 pulse-agent-darwin-amd64 pulse-agent-darwin-arm64 pulse-agent-windows-amd64.exe pulse-agent-windows-amd64 pulse-agent-windows-arm64.exe pulse-agent-windows-arm64 pulse-agent-windows-386.exe pulse-agent-windows-386 pulse-agent-freebsd-amd64 pulse-agent-freebsd-arm64"; for f in $required; do [ -e "$f" ] || { echo "missing binary $f" >&2; exit 1; }; [ -s "$f" ] || { echo "empty binary $f" >&2; exit 1; }; done; [ "$(readlink pulse-agent-windows-amd64)" = "pulse-agent-windows-amd64.exe" ] || { echo "unified agent windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-arm64)" = "pulse-agent-windows-arm64.exe" ] || { echo "unified agent windows arm64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-386)" = "pulse-agent-windows-386.exe" ] || { echo "unified agent windows 386 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/bin; required="pulse pulse-agent-linux-amd64 pulse-agent-linux-arm64 pulse-agent-linux-armv7 pulse-agent-linux-armv6 pulse-agent-linux-386 pulse-agent-helper-linux-amd64 pulse-agent-helper-linux-arm64 pulse-agent-helper-linux-armv7 pulse-agent-helper-linux-armv6 pulse-agent-helper-linux-386 pulse-agent-runner-linux-amd64 pulse-agent-runner-linux-arm64 pulse-agent-runner-linux-armv7 pulse-agent-runner-linux-armv6 pulse-agent-runner-linux-386 pulse-agent-darwin-amd64 pulse-agent-darwin-arm64 pulse-agent-windows-amd64.exe pulse-agent-windows-amd64 pulse-agent-windows-arm64.exe pulse-agent-windows-arm64 pulse-agent-windows-386.exe pulse-agent-windows-386 pulse-agent-freebsd-amd64 pulse-agent-freebsd-arm64"; for f in $required; do [ -e "$f" ] || { echo "missing binary $f" >&2; exit 1; }; [ -s "$f" ] || { echo "empty binary $f" >&2; exit 1; }; done; [ "$(readlink pulse-agent-windows-amd64)" = "pulse-agent-windows-amd64.exe" ] || { echo "unified agent windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-arm64)" = "pulse-agent-windows-arm64.exe" ] || { echo "unified agent windows arm64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-386)" = "pulse-agent-windows-386.exe" ] || { echo "unified agent windows 386 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
success "All downloadable binaries present"
# Validate the arch-resolved /usr/local/bin/pulse-agent symlink. The helm
@@ -543,10 +543,18 @@ privileged_helper_entries=(
./bin/pulse-agent-helper-linux-armv6
./bin/pulse-agent-helper-linux-386
)
action_runner_entries=(
./bin/pulse-agent-runner-linux-amd64
./bin/pulse-agent-runner-linux-arm64
./bin/pulse-agent-runner-linux-armv7
./bin/pulse-agent-runner-linux-armv6
./bin/pulse-agent-runner-linux-386
)
platform_tar_entries=(
./bin/pulse
"${unified_agent_entries[@]}"
"${privileged_helper_entries[@]}"
"${action_runner_entries[@]}"
./scripts/install-container-agent.sh
./scripts/install-docker.sh
./scripts/install.sh
@@ -562,7 +570,8 @@ validate_universal_tarball() {
"pulse-v${PULSE_VERSION}.tar.gz" \
./VERSION \
"${unified_agent_entries[@]}" \
"${privileged_helper_entries[@]}"
"${privileged_helper_entries[@]}" \
"${action_runner_entries[@]}"
}
# Each archive previously underwent up to four complete gzip scans in series.