Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/actions/agent/assert-invalid-descriptor/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Assert rejection of invalid Provider discovery data
description: Publish a malformed descriptor and prove Agent recovery.

inputs:
container-name:
description: Fedora container running the installed Agent.
required: true
evidence-directory:
description: Host directory receiving rejection evidence.
required: true
timeout-seconds:
description: Maximum time to wait for rejection.
required: false
default: "60"

runs:
using: composite
steps:
- name: Reject malformed discovery data
shell: bash
env:
CONTAINER_NAME: ${{ inputs.container-name }}
EVIDENCE_DIRECTORY: ${{ inputs.evidence-directory }}
TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }}
run: |
set -euo pipefail
mkdir -p "${EVIDENCE_DIRECTORY}"
provider_id="$(docker exec "${CONTAINER_NAME}" \
python3 -c 'import uuid; print(uuid.uuid4())')"
descriptor="/run/stateforge-agent/providers/${provider_id}.sfp.json"
docker exec "${CONTAINER_NAME}" bash -c \
'umask 0027; printf "{" > "$1"' -- "${descriptor}"

deadline=$((SECONDS + TIMEOUT_SECONDS))
while (( SECONDS < deadline )); do
journal="$(docker exec "${CONTAINER_NAME}" \
journalctl -u stateforge-agent.service --no-pager -o cat)"
if grep -F 'Ignoring invalid Provider endpoint descriptor:' \
<<< "${journal}" | grep -Fq "path=${descriptor}"; then
printf 'Rejected malformed descriptor %s.\n' "${descriptor}" \
> "${EVIDENCE_DIRECTORY}/invalid-descriptor.txt"
docker exec "${CONTAINER_NAME}" \
systemctl is-active --quiet stateforge-agent.service
exit 0
fi
sleep 1
done

echo "Agent did not reject malformed Provider discovery data" >&2
exit 1
61 changes: 61 additions & 0 deletions .github/actions/agent/assert-session/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Assert Agent acceptance of a Provider session
description: Wait until the installed Agent accepts the expected generation.

inputs:
container-name:
description: Fedora container running the Agent and Provider.
required: true
provider-instance-id:
description: Stable Provider installation identity.
required: true
endpoint-generation:
description: Provider endpoint generation that must be accepted.
required: true
evidence-directory:
description: Host directory receiving the accepted session record.
required: true
timeout-seconds:
description: Maximum time to wait for Agent acceptance.
required: false
default: "60"

runs:
using: composite
steps:
- name: Assert current Bootstrap and Manifest session
shell: bash
env:
CONTAINER_NAME: ${{ inputs.container-name }}
ENDPOINT_GENERATION: ${{ inputs.endpoint-generation }}
EVIDENCE_DIRECTORY: ${{ inputs.evidence-directory }}
PROVIDER_INSTANCE_ID: ${{ inputs.provider-instance-id }}
TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }}
run: |
set -euo pipefail
mkdir -p "${EVIDENCE_DIRECTORY}"
deadline=$((SECONDS + TIMEOUT_SECONDS))
while (( SECONDS < deadline )); do
journal="$(docker exec "${CONTAINER_NAME}" \
journalctl -u stateforge-agent.service --no-pager -o cat)"
if grep -F 'Rejecting Provider manifest:' <<< "${journal}" \
| grep -F "provider_instance_id=${PROVIDER_INSTANCE_ID}" \
| grep -Fq "endpoint_generation=${ENDPOINT_GENERATION}"; then
echo "Agent rejected the expected Provider manifest" >&2
exit 1
fi
session="$(grep -F 'SFP session established:' <<< "${journal}" \
| grep -F "provider_instance_id=${PROVIDER_INSTANCE_ID}" \
| grep -F "endpoint_generation=${ENDPOINT_GENERATION}" \
| tail -n 1 || true)"
if [[ -n "${session}" ]]; then
printf '%s\n' "${session}" \
> "${EVIDENCE_DIRECTORY}/accepted-session.txt"
exit 0
fi
sleep 1
done

docker exec "${CONTAINER_NAME}" \
journalctl -u stateforge-agent.service --no-pager || true
echo "Agent did not accept the expected Provider generation" >&2
exit 1
178 changes: 178 additions & 0 deletions .github/actions/agent/install/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: Install the exact Agent and Provider runtime
description: Install the tested RPM and wheels in an isolated Fedora system.

inputs:
agent-artifact-directory:
description: Directory containing the exact Agent RPM build.
required: true
agent-test-image-directory:
description: Bundled exact Agent Fedora test-image definition.
required: true
provider-artifact-directory:
description: Directory containing the exact Provider wheel.
required: true
protocol-artifact-directory:
description: Directory containing the exact Protocol wheel.
required: true
evidence-directory:
description: Host directory receiving installation evidence.
required: true
container-name:
description: Name of the isolated Fedora system.
required: false
default: stateforge-provider-system-test
container-image:
description: Local tag for the Fedora system-test image.
required: false
default: stateforge-provider-system-test:fedora42

outputs:
container-name:
description: Fedora container containing the installed runtime.
value: ${{ steps.install.outputs.container-name }}

runs:
using: composite
steps:
- name: Install immutable runtime artifacts
id: install
shell: bash
env:
AGENT_ARTIFACT_DIRECTORY: ${{ inputs.agent-artifact-directory }}
AGENT_TEST_IMAGE_DIRECTORY: ${{ inputs.agent-test-image-directory }}
CONTAINER_IMAGE: ${{ inputs.container-image }}
CONTAINER_NAME: ${{ inputs.container-name }}
EVIDENCE_DIRECTORY: ${{ inputs.evidence-directory }}
PROTOCOL_ARTIFACT_DIRECTORY: ${{ inputs.protocol-artifact-directory }}
PROVIDER_ARTIFACT_DIRECTORY: ${{ inputs.provider-artifact-directory }}
run: |
set -euo pipefail
mkdir -p "${EVIDENCE_DIRECTORY}"

mapfile -t agent_rpms < <(
find "${AGENT_ARTIFACT_DIRECTORY}" -maxdepth 1 -type f \
-name 'stateforge-agent-*.rpm' ! -name '*.src.rpm' -print
)
mapfile -t provider_wheels < <(
find "${PROVIDER_ARTIFACT_DIRECTORY}" -maxdepth 1 -type f \
-name 'stateforge_provider-*.whl' -print
)
mapfile -t protocol_wheels < <(
find "${PROTOCOL_ARTIFACT_DIRECTORY}" -maxdepth 1 -type f \
-name 'stateforge_protocol-*.whl' -print
)
if (( ${#agent_rpms[@]} != 1 || \
${#provider_wheels[@]} != 1 || \
${#protocol_wheels[@]} != 1 )); then
echo "Expected one Agent RPM, Provider wheel, and Protocol wheel" >&2
exit 1
fi

(
cd "${AGENT_ARTIFACT_DIRECTORY}"
sha256sum --check SHA256SUMS
) > "${EVIDENCE_DIRECTORY}/agent-checksums.txt"
(
cd "${AGENT_TEST_IMAGE_DIRECTORY}"
sha256sum --check SHA256SUMS
) > "${EVIDENCE_DIRECTORY}/agent-test-image-checksums.txt"
(
cd "${PROVIDER_ARTIFACT_DIRECTORY}"
sha256sum --check SHA256SUMS
) > "${EVIDENCE_DIRECTORY}/provider-checksums.txt"
(
cd "${PROTOCOL_ARTIFACT_DIRECTORY}"
sha256sum --check SHA256SUMS
) > "${EVIDENCE_DIRECTORY}/protocol-checksums.txt"

agent_artifacts="$(cd "${AGENT_ARTIFACT_DIRECTORY}" && pwd -P)"
agent_test_image="$(
cd "${AGENT_TEST_IMAGE_DIRECTORY}" && pwd -P
)"
evidence="$(cd "${EVIDENCE_DIRECTORY}" && pwd -P)"
provider_artifacts="$(cd "${PROVIDER_ARTIFACT_DIRECTORY}" && pwd -P)"
protocol_artifacts="$(cd "${PROTOCOL_ARTIFACT_DIRECTORY}" && pwd -P)"
agent_rpm="$(basename "${agent_rpms[0]}")"

docker build \
--file "${agent_test_image}/packaging/rpm/Dockerfile.test" \
--tag "${CONTAINER_IMAGE}" \
"${agent_test_image}/packaging/rpm"
docker rm --force "${CONTAINER_NAME}" >/dev/null 2>&1 || true
docker run --detach \
--name "${CONTAINER_NAME}" \
--privileged \
--cgroupns=host \
--tmpfs /run \
--tmpfs /run/lock \
--volume /sys/fs/cgroup:/sys/fs/cgroup:rw \
--mount \
"type=bind,source=${agent_artifacts},target=/artifacts/agent,readonly" \
--mount \
"type=bind,source=${provider_artifacts},target=/artifacts/provider,readonly" \
--mount \
"type=bind,source=${protocol_artifacts},target=/artifacts/protocol,readonly" \
--mount "type=bind,source=${evidence},target=/evidence" \
"${CONTAINER_IMAGE}" >/dev/null

for _ in {1..60}; do
state="$(docker exec "${CONTAINER_NAME}" \
systemctl is-system-running 2>/dev/null || true)"
if [[ "${state}" == "running" || "${state}" == "degraded" ]]; then
break
fi
sleep 1
done
if [[ "${state:-}" != "running" && "${state:-}" != "degraded" ]]; then
echo "systemd did not become ready in ${CONTAINER_NAME}" >&2
exit 1
fi

docker exec "${CONTAINER_NAME}" \
rpm -Uvh --nodeps "/artifacts/agent/${agent_rpm}" \
> "${EVIDENCE_DIRECTORY}/agent-rpm-install.txt"
docker exec "${CONTAINER_NAME}" rpm -V --nodeps stateforge-agent \
> "${EVIDENCE_DIRECTORY}/agent-rpm-verify.txt"
docker exec "${CONTAINER_NAME}" python3 -c '
import re
from importlib.metadata import requires

dependencies = []
for requirement in requires("stateforge-agent") or ():
name = re.split(r"[ ;<>=!~\[(]", requirement, maxsplit=1)[0]
if name.strip().lower().replace("_", "-") != "stateforge-protocol":
dependencies.append(requirement)
print("\n".join(dependencies))
' > "${EVIDENCE_DIRECTORY}/agent-requirements.txt"
docker exec "${CONTAINER_NAME}" bash -lc \
'python3 -m pip install --no-cache-dir --break-system-packages --prefix /usr --requirement /evidence/agent-requirements.txt /artifacts/protocol/stateforge_protocol-*.whl /artifacts/provider/stateforge_provider-*.whl' \
> "${EVIDENCE_DIRECTORY}/runtime-install.txt"

docker exec "${CONTAINER_NAME}" \
sed -i \
's|url = ""|url = "https://control.stateforge.test"|' \
/etc/stateforge-agent/config.toml
docker exec "${CONTAINER_NAME}" python3 -c '
import json
from importlib.metadata import distribution
from pathlib import Path

import stateforge_agent
import stateforge_provider
import stateforge_protocol

packages = {}
for name, module in (
("stateforge-agent", stateforge_agent),
("stateforge-provider", stateforge_provider),
("stateforge-protocol", stateforge_protocol),
):
package = distribution(name)
packages[name] = {
"module_file": str(Path(module.__file__).resolve()),
"version": package.version,
}
print(json.dumps(packages, indent=2, sort_keys=True))
' > "${EVIDENCE_DIRECTORY}/installed-runtime.json"

echo "container-name=${CONTAINER_NAME}" >> "${GITHUB_OUTPUT}"
46 changes: 46 additions & 0 deletions .github/actions/agent/start/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Start the installed StateForge Agent
description: Start the packaged systemd service and wait for readiness.

inputs:
container-name:
description: Fedora container containing the installed Agent.
required: true
timeout-seconds:
description: Maximum time to wait for service readiness.
required: false
default: "60"

runs:
using: composite
steps:
- name: Start Agent service
shell: bash
env:
CONTAINER_NAME: ${{ inputs.container-name }}
TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }}
run: |
set -euo pipefail
docker exec "${CONTAINER_NAME}" systemctl daemon-reload
docker exec "${CONTAINER_NAME}" systemctl start stateforge-agent.service

deadline=$((SECONDS + TIMEOUT_SECONDS))
while (( SECONDS < deadline )); do
state="$(docker exec "${CONTAINER_NAME}" \
systemctl is-active stateforge-agent.service 2>/dev/null || true)"
if [[ "${state}" == "active" ]] && docker exec \
"${CONTAINER_NAME}" journalctl -u stateforge-agent.service \
--no-pager -o cat | grep -Fq 'StateForge Agent started'; then
exit 0
fi
if [[ "${state}" == "failed" || "${state}" == "inactive" ]]; then
break
fi
sleep 1
done

docker exec "${CONTAINER_NAME}" \
systemctl status stateforge-agent.service --no-pager || true
docker exec "${CONTAINER_NAME}" \
journalctl -u stateforge-agent.service --no-pager || true
echo "Agent service did not become ready" >&2
exit 1
Loading
Loading