mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Separate agent remediation runtime
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
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`,
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"], [])
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user