Skip to content

Commit 71075cc

Browse files
committed
chore: merge current main into CI cleanup
2 parents 8963c3c + 556e029 commit 71075cc

8 files changed

Lines changed: 65 additions & 18 deletions

bin/fm-no-mistakes-worker

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import argparse
55
import fcntl
66
import hashlib
7+
import ipaddress
78
import json
89
import os
910
from pathlib import Path
@@ -44,6 +45,7 @@ LIFECYCLE_ENV_KEYS = {
4445
"FM_AZURE_BUDGET_START_DATE", "FM_AZURE_DEPLOYMENT_GENERATION",
4546
"FM_AZURE_OWNER_TAG", "FM_AZURE_NAMING_PREFIX", "FM_AZURE_RESOURCE_GROUP",
4647
"FM_AZURE_STORAGE_NAME", "FM_AZURE_LOCATION", "FM_AZURE_WORKER_STATE_DIR",
48+
"FM_AZURE_WORKER_IMAGE_ID", "FM_AZURE_OPERATOR_DATA_PLANE_IP",
4749
"FM_AZURE_WORKER_MAX", "FM_AZURE_WORKER_IDLE_COOLDOWN_SECONDS",
4850
"FM_AZURE_WORKER_IDLE_RELEASE_SECONDS", "FM_AZURE_WORKER_POLICY_PHASE",
4951
"FM_AZURE_WORKER_STEADY_TARGET_USD", "FM_AZURE_WORKER_COMMISSIONING_CEILING_USD",
@@ -115,6 +117,20 @@ def validate_config(value):
115117
not isinstance(item, str) or not item or "\x00" in item for item in env.values()
116118
):
117119
raise WrapperError("lifecycle environment is incomplete or malformed", "config_invalid", False)
120+
operator_ip = env.get("FM_AZURE_OPERATOR_DATA_PLANE_IP")
121+
if operator_ip is not None:
122+
try:
123+
parsed_operator_ip = ipaddress.IPv4Address(operator_ip)
124+
except ipaddress.AddressValueError:
125+
raise WrapperError(
126+
"operator data-plane route must be one canonical IPv4 address",
127+
"config_invalid", False,
128+
)
129+
if str(parsed_operator_ip) != operator_ip:
130+
raise WrapperError(
131+
"operator data-plane route must be one canonical IPv4 address",
132+
"config_invalid", False,
133+
)
118134
for field, low, high in (
119135
("assignment_timeout_seconds", 1, 7200),
120136
("cleanup_timeout_seconds", 1, 7200),

docs/azure-no-mistakes-worker-config.example.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"FM_AZURE_DEPLOYMENT_GENERATION": "replace-with-deployment-generation",
1919
"FM_AZURE_OWNER_TAG": "replace-with-owner-tag",
2020
"FM_AZURE_NAMING_PREFIX": "replace-with-naming-prefix",
21-
"FM_AZURE_STORAGE_NAME": "replace-with-storage-account"
21+
"FM_AZURE_STORAGE_NAME": "replace-with-storage-account",
22+
"FM_AZURE_OPERATOR_DATA_PLANE_IP": "replace-with-one-exact-operator-egress-ipv4",
23+
"FM_AZURE_WORKER_IMAGE_ID": "replace-with-exact-compute-gallery-image-version-resource-id"
2224
},
2325
"assignment_timeout_seconds": 1800,
2426
"cleanup_timeout_seconds": 1800,

docs/azure-workers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ The request and result echo the canonical `step` (`review` or `test`) separately
363363
They also echo the caller's lowercase SHA-256 `runtime_identity`, which binds the exact wrapper bytes, private wrapper-config bytes, and transport protocol into the job's content-addressed input; a changed runtime is a new job identity, never a replay under mutable code.
364364
The caller payload contains exactly `repo.bundle` and `brief.md`; the wrapper verifies both against the request, stages the configured digest-bound credential-free `runtime.tar.gz`, and submits the request's exact argv without a shell.
365365
The owner-private config uses `fm.no-mistakes-worker-wrapper-config/v1` and names the Firstmate home, canonical Pi account pool home, sealed runtime path and digest, lifecycle executable and exact clean Firstmate source commit, bounded assignment/cleanup/wall times, and the non-secret lifecycle environment.
366+
That environment may carry the exact immutable `FM_AZURE_WORKER_IMAGE_ID` and single reviewed `FM_AZURE_OPERATOR_DATA_PLANE_IP`; retaining them across incremental retries prevents an existing claimed VM from being replayed against a different image reference or disabling the controller's default-deny operator route mid-recovery.
366367
The wrapper rechecks that source commit and clean tracked lifecycle closure before every lifecycle call, and re-verifies the staged guest runtime against its configured digest after copying it, so neither an ordinary Firstmate update nor a path replacement can silently change an admitted job.
367368

368369
`docs/azure-no-mistakes-worker-config.example.json` is the copy-and-fill wrapper config template.

tests/behavior-test-durations.tsv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@
7676
1686 tests/fm-macos-permissions.test.sh
7777
3428 tests/fm-nm-step-liveness.test.sh
7878
1560 tests/fm-no-mistakes-reattach.test.sh
79-
3500 tests/fm-no-mistakes-runtime.test.sh
80-
8500 tests/fm-no-mistakes-worker.test.sh
79+
2770 tests/fm-no-mistakes-runtime.test.sh
80+
7260 tests/fm-no-mistakes-worker.test.sh
8181
2000 tests/fm-pi-account-home.test.sh
8282
10 tests/fm-pi-primary-live-e2e.test.sh
8383
80 tests/fm-pi-primary-types.test.sh

tests/fm-azure-pilot.test.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ run_bounded_mutation_deadline_checks() {
593593
state_dir=$(mktemp -d)
594594
write_sourceable_script "$sourceable"
595595
set +e
596+
# shellcheck disable=SC2030,SC2031
596597
output=$(
597598
(
598599
set -- help "$state_dir"
@@ -614,7 +615,7 @@ run_bounded_mutation_deadline_checks() {
614615
status=$?
615616
set -e
616617
[ "$status" -ne 0 ] || fail "bounded Azure mutation accepted a hung CLI"
617-
# shellcheck disable=SC2031
618+
# shellcheck disable=SC2030,SC2031
618619
[ "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["phase"])' "$state_dir/apply.json")" = retained ] || fail "timed-out mutation did not retain exact operation state"
619620
python3 - "$SCRIPT" <<'PY' || fail "foundation mutating Azure calls bypass bounded state owner"
620621
from pathlib import Path
@@ -750,6 +751,7 @@ run_worker_create_replay_quota_checks() {
750751
sourceable=$(mktemp)
751752
write_sourceable_script "$sourceable"
752753
set +e
754+
# These exports intentionally reset names assigned in earlier isolated runtime-test subshells.
753755
# shellcheck disable=SC2030,SC2031
754756
output=$(
755757
(

tests/fm-credential-expiry.test.sh

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,9 @@ home = work / "home"
280280
281281
# The preflight runs before the FIFO lane wait, before runtime_config, and
282282
# again once the lane is held because the lane wait can outlast the
283-
# credential. A third check immediately before staging still precedes every
284-
# uploaded object and shared-host run. The snapshot wrapper deliberately
285-
# delegates these contracts to the two functions that own lane admission and
286-
# review execution.
283+
# credential. A third check inside the held lane still precedes every remote
284+
# staged object and reviewer-host use. The snapshot wrapper delegates these
285+
# contracts to the two functions that own lane admission and paid compute.
287286
lane_source = inspect.getsource(adapter._run_azure_review_after_snapshot)
288287
entry_source = inspect.getsource(adapter.run_azure_review)
289288
assert "_run_azure_review_after_snapshot" in entry_source
@@ -292,7 +291,7 @@ assert lane_source.index("preflight_reviewer_credential") < lane_source.index("r
292291
assert lane_source.rindex("preflight_reviewer_credential") > lane_source.index("acquire_review_lane")
293292
compute_source = inspect.getsource(adapter._run_azure_review_in_lane)
294293
assert compute_source.index("preflight_reviewer_credential") < compute_source.index("upload_blob")
295-
assert compute_source.index("preflight_reviewer_credential") < compute_source.index("submit_model_run")
294+
assert compute_source.index("preflight_reviewer_credential") < compute_source.index("ensure_model_host")
296295
297296
298297
def review(profile, harness):

tests/fm-no-mistakes-worker.test.sh

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ import subprocess
3737
import sys
3838
3939
root = Path(__file__).parent
40+
expected_image = "/subscriptions/11111111-1111-4111-8111-111111111111/resourceGroups/fixture/providers/Microsoft.Compute/galleries/fixture/images/worker/versions/1.0.0"
41+
if os.environ.get("FM_AZURE_WORKER_IMAGE_ID") != expected_image:
42+
raise SystemExit("exact worker image identity was not forwarded to lifecycle")
43+
if os.environ.get("FM_AZURE_OPERATOR_DATA_PLANE_IP") != "203.0.113.10":
44+
raise SystemExit("exact operator data-plane IP was not forwarded to lifecycle")
4045
state_path = root / "fake-state.json"
4146
complete = root / "complete"
4247
log = root / "calls.log"
@@ -286,7 +291,7 @@ pass "guest supervisor re-verifies the sealed runtime inventory and executable"
286291

287292
RUNTIME_SHA=$(shasum -a 256 "$TMP_ROOT/runtime.tar.gz" | awk '{print $1}')
288293
cat > "$TMP_ROOT/config.json" <<JSON
289-
{"schema":"fm.no-mistakes-worker-wrapper-config/v1","fm_home":"$HOME_DIR","account_pool_home":"$HOME_DIR/accounts","runtime_bundle":"$TMP_ROOT/runtime.tar.gz","runtime_bundle_sha256":"$RUNTIME_SHA","lifecycle_path":"$FAKE","lifecycle_source_commit":"$LIFECYCLE_COMMIT","lifecycle_env":{"FM_AZURE_TENANT_ID":"22222222-2222-4222-8222-222222222222","FM_AZURE_SUBSCRIPTION_ID":"11111111-1111-4111-8111-111111111111","FM_AZURE_ADMIN_EMAIL":"fixture@example.invalid","FM_AZURE_ADMIN_USERNAME":"fixtureadmin","FM_AZURE_ADMIN_SSH_PUBLIC_KEY":"ssh-ed25519 AAAATEST fixture","FM_AZURE_RUNNER_OPERATOR_OBJECT_ID":"33333333-3333-4333-8333-333333333333","FM_AZURE_KEY_VAULT_NAME":"fixture-vault","FM_AZURE_BUDGET_START_DATE":"2026-08-01","FM_AZURE_DEPLOYMENT_GENERATION":"dep-fixture","FM_AZURE_OWNER_TAG":"owner","FM_AZURE_NAMING_PREFIX":"fixture","FM_AZURE_STORAGE_NAME":"fixturestorage"},"assignment_timeout_seconds":30,"cleanup_timeout_seconds":30,"poll_seconds":1,"wall_seconds":60}
294+
{"schema":"fm.no-mistakes-worker-wrapper-config/v1","fm_home":"$HOME_DIR","account_pool_home":"$HOME_DIR/accounts","runtime_bundle":"$TMP_ROOT/runtime.tar.gz","runtime_bundle_sha256":"$RUNTIME_SHA","lifecycle_path":"$FAKE","lifecycle_source_commit":"$LIFECYCLE_COMMIT","lifecycle_env":{"FM_AZURE_TENANT_ID":"22222222-2222-4222-8222-222222222222","FM_AZURE_SUBSCRIPTION_ID":"11111111-1111-4111-8111-111111111111","FM_AZURE_ADMIN_EMAIL":"fixture@example.invalid","FM_AZURE_ADMIN_USERNAME":"fixtureadmin","FM_AZURE_ADMIN_SSH_PUBLIC_KEY":"ssh-ed25519 AAAATEST fixture","FM_AZURE_RUNNER_OPERATOR_OBJECT_ID":"33333333-3333-4333-8333-333333333333","FM_AZURE_KEY_VAULT_NAME":"fixture-vault","FM_AZURE_BUDGET_START_DATE":"2026-08-01","FM_AZURE_DEPLOYMENT_GENERATION":"dep-fixture","FM_AZURE_OWNER_TAG":"owner","FM_AZURE_NAMING_PREFIX":"fixture","FM_AZURE_STORAGE_NAME":"fixturestorage","FM_AZURE_OPERATOR_DATA_PLANE_IP":"203.0.113.10","FM_AZURE_WORKER_IMAGE_ID":"/subscriptions/11111111-1111-4111-8111-111111111111/resourceGroups/fixture/providers/Microsoft.Compute/galleries/fixture/images/worker/versions/1.0.0"},"assignment_timeout_seconds":30,"cleanup_timeout_seconds":30,"poll_seconds":1,"wall_seconds":60}
290295
JSON
291296
chmod 600 "$TMP_ROOT/config.json"
292297

@@ -343,7 +348,31 @@ PY
343348
[ ! -e "$TMP_ROOT/calls.log" ] \
344349
|| fail "missing Azure foundation field reached lifecycle before config validation"
345350
done
346-
pass "wrapper requires the complete Azure foundation identity before lifecycle dispatch"
351+
352+
python3 - "$TMP_ROOT/config.json" "$TMP_ROOT/config-broad-route.json" <<'PY'
353+
import json, pathlib, sys
354+
source, target = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
355+
value = json.loads(source.read_text())
356+
value["lifecycle_env"]["FM_AZURE_OPERATOR_DATA_PLANE_IP"] = "0.0.0.0/0"
357+
target.write_text(json.dumps(value, separators=(",", ":")))
358+
PY
359+
chmod 600 "$TMP_ROOT/config-broad-route.json"
360+
write_request "$TMP_ROOT/request-broad-route.json" job-broad-route
361+
"$WRAPPER" --config "$TMP_ROOT/config-broad-route.json" execute \
362+
--request "$TMP_ROOT/request-broad-route.json" --payload "$PAYLOAD" \
363+
--result "$TMP_ROOT/result-broad-route.json" --outcome "$TMP_ROOT/outcome-broad-route.bundle" \
364+
--step-outcome "$TMP_ROOT/step-outcome-broad-route.json"
365+
python3 - "$TMP_ROOT/result-broad-route.json" <<'PY' \
366+
|| fail "broad operator data-plane route did not return a closed config failure"
367+
import json, pathlib, sys
368+
result = json.loads(pathlib.Path(sys.argv[1]).read_text())
369+
assert result["outcome"] == "failed"
370+
assert result["error_category"] == "config_invalid"
371+
assert result["retryable"] is False
372+
PY
373+
[ ! -e "$TMP_ROOT/calls.log" ] \
374+
|| fail "broad operator data-plane route reached lifecycle before config validation"
375+
pass "wrapper requires exact Azure foundation identity and a single IPv4 operator route before dispatch"
347376

348377
printf 'ok\n' > "$TMP_ROOT/mode"
349378
write_request "$TMP_ROOT/request.json" job-success

tests/fm-watch-triage.test.sh

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,13 +1887,11 @@ SH
18871887
. "$1"
18881888
run_bounded 1 "$2"
18891889
' _ "$WATCH" "$stubborn" || rc=$?
1890-
# GNU timeout reports 137 when its KILL fallback fires; the Perl fallback
1891-
# reports the logical timeout status 124. Both prove the same bounded
1892-
# termination, and the process check below proves cleanup.
1893-
case "$rc" in
1894-
124|137) ;;
1895-
*) fail "watcher timeout wrapper returned unexpected status $rc" ;;
1896-
esac
1890+
# GNU timeout may return 137 after its --kill-after escalation sends KILL;
1891+
# both values prove the bounded timeout path, and process absence below is
1892+
# the independent cleanup proof.
1893+
[ "$rc" -eq 124 ] || [ "$rc" -eq 137 ] \
1894+
|| fail "watcher timeout wrapper returned $rc instead of 124 or 137"
18971895
[ -s "$pid_file" ] || fail "watcher timeout fixture did not start"
18981896
! kill -0 "$(cat "$pid_file")" 2>/dev/null \
18991897
|| fail "watcher timeout wrapper left the TERM-resistant process alive"

0 commit comments

Comments
 (0)