Runner smoke: surface relay-enroll failure cause + curl egress probe
The runner image smoke test only reported an opaque "challenge request" when nucleicd failed to enroll with the relay on boot. RelayAccess.postJSON collapsed transport errors, non-200 statuses, and malformed bodies into one string, hiding whether the fully-static musl binary's URLSession/libcurl leg is failing vs the relay rejecting vs no egress. Surface the specific cause, and add an independent curl probe to the smoke step so one build tells us which layer is at fault. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -68,6 +68,16 @@ jobs:
|
||||
run: |
|
||||
chmod +x containers/nucleic-runner/nucleicd
|
||||
file containers/nucleic-runner/nucleicd
|
||||
# Independent egress probe: can THIS runner reach the relay's enroll endpoint, and what
|
||||
# does it answer? Isolates a runner-network/relay problem from a Swift-binary (static-musl
|
||||
# URLSession/libcurl) problem — the daemon's own enroll POST goes through URLSession.
|
||||
echo "----- relay egress probe (curl) -----"
|
||||
curl -sS -m 15 -o /tmp/relay.out -w 'HTTP %{http_code} in %{time_total}s\n' \
|
||||
-X POST https://relay.nucleic.blakeslee.xyz/v1/relay/enroll/challenge \
|
||||
-H 'content-type: application/json' -d '{"staticKey":"probe"}' \
|
||||
|| echo "curl failed: exit $?"
|
||||
head -c 400 /tmp/relay.out 2>/dev/null; echo
|
||||
echo "-------------------------------------"
|
||||
# Don't let `set -e` abort before we've printed the log: capture the exit code, then
|
||||
# decide. `timeout` returns 124 when it had to kill a still-running daemon (the healthy
|
||||
# case — nucleicd runs forever); a clean SIGTERM shutdown returns 0. Any other code is a
|
||||
|
||||
@@ -99,7 +99,7 @@ public actor RelayAccess {
|
||||
let staticKeyB64 = RelayEnrollment.staticKeyEncoding(identity.staticPublicKey)
|
||||
let challenge = try await postJSON(
|
||||
"/v1/relay/enroll/challenge", body: ["staticKey": staticKeyB64],
|
||||
or: Error.enrollFailed("challenge request"))
|
||||
label: "challenge request")
|
||||
guard let challengeID = challenge["challengeId"] as? String,
|
||||
let ephB64 = challenge["ephemeralPublicKey"] as? String,
|
||||
let ephemeralPublicKey = Data(base64URLNoPad: ephB64)
|
||||
@@ -111,7 +111,7 @@ public actor RelayAccess {
|
||||
let enrolled = try await postJSON(
|
||||
"/v1/relay/enroll",
|
||||
body: ["challengeId": challengeID, "proof": proof.base64URLNoPadEncoded],
|
||||
or: Error.enrollFailed("proof rejected"))
|
||||
label: "proof rejected")
|
||||
guard let hostID = enrolled["hostId"] as? String,
|
||||
let secret = enrolled["hostSecret"] as? String,
|
||||
let roomID = enrolled["roomId"] as? String, roomID == identity.hostID
|
||||
@@ -144,18 +144,36 @@ public actor RelayAccess {
|
||||
roomID: roomID, token: token, url: urlOverride, exp: json["exp"] as? Double)
|
||||
}
|
||||
|
||||
/// POST `body` as JSON and decode the JSON reply. On failure the thrown `enrollFailed`
|
||||
/// carries the *specific* cause — transport error (the URLSession/libcurl leg, which is the
|
||||
/// weak point in a fully-static musl build), a non-200 status (relay reachable but rejecting),
|
||||
/// or a malformed body — prefixed by `label` so a Settings status row / the runner's boot log
|
||||
/// says which step and why, not just an opaque "challenge request".
|
||||
private func postJSON(
|
||||
_ path: String, body: [String: String], or error: Error
|
||||
_ path: String, body: [String: String], label: String
|
||||
) async throws -> [String: Any] {
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(path))
|
||||
request.httpMethod = "POST"
|
||||
request.timeoutInterval = 10
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
||||
guard let (data, response) = try? await session.data(for: request),
|
||||
(response as? HTTPURLResponse)?.statusCode == 200,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
else { throw error }
|
||||
let data: Data
|
||||
let response: URLResponse
|
||||
do {
|
||||
(data, response) = try await session.data(for: request)
|
||||
} catch {
|
||||
throw Error.enrollFailed("\(label): transport error: \(error)")
|
||||
}
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
throw Error.enrollFailed("\(label): non-HTTP response")
|
||||
}
|
||||
guard http.statusCode == 200 else {
|
||||
let snippet = String(data: data, encoding: .utf8)?.prefix(200) ?? ""
|
||||
throw Error.enrollFailed("\(label): HTTP \(http.statusCode) \(snippet)")
|
||||
}
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw Error.enrollFailed("\(label): malformed response body")
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user