fix(agent99): bind check receipts to same-run identity
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m40s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 11s
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m40s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 11s
This commit is contained in:
@@ -47,7 +47,7 @@ usage() {
|
||||
"Usage: $0 [--check|--apply] [options]" \
|
||||
"" \
|
||||
"Default is a remote no-write check/plan against fixed target ${TARGET}." \
|
||||
"Apply requires --apply plus --trace-id, --run-id, and --work-item-id." \
|
||||
"Check and apply require --trace-id, --run-id, and --work-item-id." \
|
||||
"" \
|
||||
"Options:" \
|
||||
" --check No-write transport and envelope check (default)." \
|
||||
@@ -139,12 +139,13 @@ done
|
||||
[[ "${CONNECT_TIMEOUT_SECONDS}" =~ ^[0-9]+$ ]] || fail "invalid_connect_timeout"
|
||||
(( CONNECT_TIMEOUT_SECONDS >= 1 && CONNECT_TIMEOUT_SECONDS <= 30 )) || fail "invalid_connect_timeout"
|
||||
|
||||
readonly SAFE_ID_PATTERN='^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$'
|
||||
[[ "${TRACE_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "${MODE}_trace_id_missing_or_invalid"
|
||||
[[ "${RUN_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "${MODE}_run_id_missing_or_invalid"
|
||||
[[ "${WORK_ITEM_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "${MODE}_work_item_id_missing_or_invalid"
|
||||
|
||||
if [[ "${MODE}" == "apply" ]]; then
|
||||
REMOTE_EXECUTION_TIMEOUT_SECONDS="2400"
|
||||
readonly SAFE_ID_PATTERN='^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$'
|
||||
[[ "${TRACE_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "apply_trace_id_missing_or_invalid"
|
||||
[[ "${RUN_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "apply_run_id_missing_or_invalid"
|
||||
[[ "${WORK_ITEM_ID}" =~ ${SAFE_ID_PATTERN} ]] || fail "apply_work_item_id_missing_or_invalid"
|
||||
fi
|
||||
|
||||
for dependency in git python3 ssh scp mktemp hostname timeout; do
|
||||
@@ -208,6 +209,7 @@ trap cleanup EXIT HUP INT TERM
|
||||
ENVELOPE_PATH="${WORK_DIR}/envelope.json"
|
||||
BOOTSTRAP_PATH="${WORK_DIR}/remote-bootstrap.ps1"
|
||||
BOOTSTRAP_PAYLOAD_PATH="${WORK_DIR}/remote-bootstrap.ps1.gz.b64"
|
||||
CHECK_REMOTE_RECEIPT_PATH="${WORK_DIR}/check-remote-receipt.json"
|
||||
|
||||
python3 - \
|
||||
"${SOURCE_ROOT}" \
|
||||
@@ -561,12 +563,65 @@ cleanup_remote_payload() {
|
||||
>/dev/null 2>&1
|
||||
}
|
||||
|
||||
project_check_receipt_identity() {
|
||||
python3 - \
|
||||
"${CHECK_REMOTE_RECEIPT_PATH}" \
|
||||
"${TRACE_ID}" \
|
||||
"${RUN_ID}" \
|
||||
"${WORK_ITEM_ID}" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
receipt_path = Path(sys.argv[1])
|
||||
trace_id, run_id, work_item_id = sys.argv[2:5]
|
||||
try:
|
||||
receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
raise SystemExit(65)
|
||||
|
||||
if not isinstance(receipt, dict):
|
||||
raise SystemExit(65)
|
||||
if receipt.get("schemaVersion") != "agent99_remote_atomic_deploy_receipt_v1":
|
||||
raise SystemExit(65)
|
||||
if receipt.get("mode") != "check":
|
||||
raise SystemExit(65)
|
||||
if receipt.get("status") not in {"check_ready", "check_failed_no_apply"}:
|
||||
raise SystemExit(65)
|
||||
|
||||
identity = {
|
||||
"traceId": trace_id,
|
||||
"runId": run_id,
|
||||
"workItemId": work_item_id,
|
||||
}
|
||||
for field, expected in identity.items():
|
||||
if field in receipt and receipt[field] != expected:
|
||||
raise SystemExit(65)
|
||||
receipt[field] = expected
|
||||
|
||||
sys.stdout.write(json.dumps(receipt, separators=(",", ":"), ensure_ascii=True) + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
if [[ "${MODE}" == "check" ]]; then
|
||||
set +e
|
||||
timeout --signal=TERM --kill-after=10 "${REMOTE_EXECUTION_TIMEOUT_SECONDS}s" \
|
||||
ssh "${SSH_OPTIONS[@]}" "${TARGET}" "${REMOTE_CHECK_COMMAND}" </dev/null
|
||||
ssh "${SSH_OPTIONS[@]}" "${TARGET}" "${REMOTE_CHECK_COMMAND}" </dev/null \
|
||||
>"${CHECK_REMOTE_RECEIPT_PATH}"
|
||||
status=$?
|
||||
set -e
|
||||
set +e
|
||||
project_check_receipt_identity
|
||||
projection_status=$?
|
||||
set -e
|
||||
if [[ "${projection_status}" -ne 0 ]]; then
|
||||
printf 'agent99_transport_error=check_receipt_identity_projection_failed\n' >&2
|
||||
if [[ "${status}" -eq 0 ]]; then
|
||||
exit 65
|
||||
fi
|
||||
fi
|
||||
exit "${status}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
@@ -55,9 +56,9 @@ def test_sender_is_valid_shell_with_check_as_the_default() -> None:
|
||||
assert 'MODE="check"' in source
|
||||
assert "--apply" in source
|
||||
assert 'MODE="apply"' in source
|
||||
assert "apply_trace_id_missing_or_invalid" in source
|
||||
assert "apply_run_id_missing_or_invalid" in source
|
||||
assert "apply_work_item_id_missing_or_invalid" in source
|
||||
assert 'fail "${MODE}_trace_id_missing_or_invalid"' in source
|
||||
assert 'fail "${MODE}_run_id_missing_or_invalid"' in source
|
||||
assert 'fail "${MODE}_work_item_id_missing_or_invalid"' in source
|
||||
assert 'MODE_SELECTION="default"' in source
|
||||
assert 'REMOTE_EXECUTION_TIMEOUT_SECONDS="180"' in source
|
||||
assert 'REMOTE_EXECUTION_TIMEOUT_SECONDS="2400"' in source
|
||||
@@ -291,6 +292,34 @@ def test_receiver_has_single_flight_idempotency_and_no_secret_receipt_boundary()
|
||||
assert "[bool]$prior.relayReload.newGenerationVerified" in source
|
||||
|
||||
|
||||
def test_check_and_apply_receipts_project_the_same_run_identity() -> None:
|
||||
sender = SENDER.read_text(encoding="utf-8")
|
||||
projector = sender[
|
||||
sender.index("project_check_receipt_identity()") : sender.index(
|
||||
'if [[ "${MODE}" == "check" ]]'
|
||||
)
|
||||
]
|
||||
assert '"traceId": trace_id' in projector
|
||||
assert '"runId": run_id' in projector
|
||||
assert '"workItemId": work_item_id' in projector
|
||||
assert "receipt[field] != expected" in projector
|
||||
assert "raise SystemExit(65)" in projector
|
||||
|
||||
receiver = RECEIVER.read_text(encoding="utf-8")
|
||||
success_receipt = receiver[
|
||||
receiver.index("$successReceipt = [pscustomobject]@{") : receiver.index(
|
||||
"Write-AgentRemoteDeployReceipt $receiptPath $successReceipt"
|
||||
)
|
||||
]
|
||||
assert "traceId = $traceId" in success_receipt
|
||||
assert "runId = $runId" in success_receipt
|
||||
assert "workItemId = $workItemId" in success_receipt
|
||||
assert "secretValueRead = $false" in success_receipt
|
||||
assert "privateKeyValueRead = $false" in success_receipt
|
||||
assert "tokenValueRead = $false" in success_receipt
|
||||
assert "environmentSecretRead = $false" in success_receipt
|
||||
|
||||
|
||||
def test_receiver_creates_canonical_success_receipt_once_with_atomic_no_replace() -> None:
|
||||
source = RECEIVER.read_text(encoding="utf-8")
|
||||
writer = source[
|
||||
@@ -586,6 +615,39 @@ def test_apply_without_all_three_identities_fails_before_ssh() -> None:
|
||||
assert "known_hosts_file_missing" not in result.stderr
|
||||
|
||||
|
||||
def test_check_requires_complete_safe_same_run_identity_before_ssh() -> None:
|
||||
cases = (
|
||||
((), "check_trace_id_missing_or_invalid"),
|
||||
(("--trace-id", "trace-fixture"), "check_run_id_missing_or_invalid"),
|
||||
(
|
||||
("--trace-id", "trace-fixture", "--run-id", "run-fixture"),
|
||||
"check_work_item_id_missing_or_invalid",
|
||||
),
|
||||
(
|
||||
(
|
||||
"--trace-id",
|
||||
"trace fixture",
|
||||
"--run-id",
|
||||
"run-fixture",
|
||||
"--work-item-id",
|
||||
"AIA-SRE-P0-20260715",
|
||||
),
|
||||
"check_trace_id_missing_or_invalid",
|
||||
),
|
||||
)
|
||||
|
||||
for identity_args, expected_error in cases:
|
||||
result = subprocess.run(
|
||||
["bash", str(SENDER), "--check", *identity_args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 64
|
||||
assert expected_error in result.stderr
|
||||
assert "known_hosts_file_missing" not in result.stderr
|
||||
|
||||
|
||||
def test_check_mode_uses_bounded_control_command_without_ssh_stdin(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -610,7 +672,13 @@ def test_check_mode_uses_bounded_control_command_without_ssh_stdin(
|
||||
"#!/usr/bin/env bash\n"
|
||||
f"printf '%s\\n' \"$@\" >{shlex.quote(str(args_path))}\n"
|
||||
f"wc -c | tr -d ' ' >{shlex.quote(str(bytes_path))}\n"
|
||||
"printf '%s\\n' '{\"status\":\"check_ready\"}'\n",
|
||||
'if [[ -n "${FAKE_CHECK_RECEIPT:-}" ]]; then\n'
|
||||
" printf '%s\\n' \"$FAKE_CHECK_RECEIPT\"\n"
|
||||
"else\n"
|
||||
" printf '%s\\n' "
|
||||
'\'{"schemaVersion":"agent99_remote_atomic_deploy_receipt_v1",'
|
||||
'"status":"check_ready","mode":"check"}\'\n'
|
||||
"fi\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
fake_timeout = fake_bin / "timeout"
|
||||
@@ -647,6 +715,12 @@ def test_check_mode_uses_bounded_control_command_without_ssh_stdin(
|
||||
"bash",
|
||||
str(SENDER),
|
||||
"--check",
|
||||
"--trace-id",
|
||||
"trace-fixture",
|
||||
"--run-id",
|
||||
"run-fixture",
|
||||
"--work-item-id",
|
||||
"AIA-SRE-P0-20260715",
|
||||
"--known-hosts-file",
|
||||
str(known_hosts),
|
||||
"--identity-file",
|
||||
@@ -659,7 +733,11 @@ def test_check_mode_uses_bounded_control_command_without_ssh_stdin(
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert '"status":"check_ready"' in result.stdout
|
||||
receipt = json.loads(result.stdout)
|
||||
assert receipt["status"] == "check_ready"
|
||||
assert receipt["traceId"] == "trace-fixture"
|
||||
assert receipt["runId"] == "run-fixture"
|
||||
assert receipt["workItemId"] == "AIA-SRE-P0-20260715"
|
||||
args = args_path.read_text(encoding="utf-8")
|
||||
remote_command = args.splitlines()[-1]
|
||||
assert "BatchMode=yes" in args
|
||||
@@ -673,6 +751,9 @@ def test_check_mode_uses_bounded_control_command_without_ssh_stdin(
|
||||
assert "agent99-contract-check.ps1" in check_script
|
||||
assert "Get-FileHash" in check_script
|
||||
assert "runtimeContract" in check_script
|
||||
assert "trace-fixture" not in check_script
|
||||
assert "run-fixture" not in check_script
|
||||
assert "AIA-SRE-P0-20260715" not in check_script
|
||||
assert "$ready=[bool]($c.executed-and$c.ok-and$c.exitCode-eq 0)" in check_script
|
||||
assert "'check_failed_no_apply'" in check_script
|
||||
assert "if(-not$ready){exit 65}" in check_script
|
||||
@@ -692,6 +773,30 @@ def test_check_mode_uses_bounded_control_command_without_ssh_stdin(
|
||||
stdin_bytes = int(bytes_path.read_text(encoding="utf-8"))
|
||||
assert stdin_bytes == 0
|
||||
|
||||
conflicting_environment = environment.copy()
|
||||
conflicting_environment["FAKE_CHECK_RECEIPT"] = json.dumps(
|
||||
{
|
||||
"schemaVersion": "agent99_remote_atomic_deploy_receipt_v1",
|
||||
"status": "check_ready",
|
||||
"mode": "check",
|
||||
"traceId": "different-trace",
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
conflicting_result = subprocess.run(
|
||||
result.args,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=conflicting_environment,
|
||||
)
|
||||
assert conflicting_result.returncode == 65
|
||||
assert conflicting_result.stdout == ""
|
||||
assert (
|
||||
"agent99_transport_error=check_receipt_identity_projection_failed"
|
||||
in conflicting_result.stderr
|
||||
)
|
||||
|
||||
|
||||
def test_apply_uses_ephemeral_file_transport_and_cleans_up_without_ssh_stdin(
|
||||
tmp_path: Path,
|
||||
|
||||
Reference in New Issue
Block a user