Stop Noise relay recovery on rejected executor credentials (#59750)

## Why

Registry `401` responses during harness key validation previously produced generic relay resets, causing recovery to retry rejected executor credentials instead of surfacing the authentication failure.

## What changed

- Send an authenticated Noise handshake rejection for registry `401` responses without opening an application transport.
- Propagate the rejection as `EnvironmentRegistryAuth` and preserve it in the terminal connection state so recovery stops with a specific authentication error.
- Preserve registry HTTP status codes so `403` responses for stale harness grants and transient service failures can continue through recovery.

## Testing

Add tests that reject tampered authentication rejections and exercise session recovery: `401` stops after one rejected recovery attempt, while `403`, `502`, and `503` retry with fresh harness grants and reconnect successfully.

GitOrigin-RevId: 9a55412e583146c094948618c7c9aeb4fb4b953e
This commit is contained in:
richardopenai
2026-11-05 22:03:55 +00:00
committed by copyberry
parent 219ab4043e
commit c48785a9b7
10 changed files with 289 additions and 16 deletions
+17 -4
View File
@@ -495,7 +495,7 @@ where
///
/// Implementations must consult an authority independent of rendezvous. The
/// exec-server invokes this after parsing the first IK message and before
/// completing the responder handshake.
/// opening an application transport.
pub(crate) trait HarnessKeyValidator: Send + Sync {
fn validate_harness_key(
&self,
@@ -645,7 +645,20 @@ where
reason,
registry_status,
);
send_reset(&physical_outgoing_tx, validation_result.stream_id);
// Authenticate the rejection so the harness can stop retrying.
if registry_status == Some(401)
&& let Ok(response) = pending.handshake.reject_registry_authentication()
{
let frame = RelayMessageFrame::handshake(
validation_result.stream_id,
response,
);
let _ = physical_outgoing_tx.try_send(
encode_relay_message_frame(&frame),
);
} else {
send_reset(&physical_outgoing_tx, validation_result.stream_id);
}
record_failed_handshake(
&mut failed_handshakes,
&mut handshake_cooldown_until,
@@ -864,8 +877,8 @@ where
);
let validator = validator.clone();
// Failed validation leaves no transport state and sends only a
// generic reset.
// Failed validation leaves no transport state. Preserve the
// trusted registry status for request-scoped rejection handling.
validation_tasks.spawn(async move {
let result = match timeout(
HARNESS_KEY_VALIDATION_TIMEOUT,