Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6945061a0b | ||
|
|
8c57be4b12 | ||
|
|
6dc80f6d26 |
5 changed files with 86 additions and 12 deletions
|
|
@ -16,6 +16,12 @@ repair the store. Consumers own native requests, provider lowering, prompts,
|
||||||
schemas, tool execution, retry between passes, interpretation, decisions, and
|
schemas, tool execution, retry between passes, interpretation, decisions, and
|
||||||
state admission.
|
state admission.
|
||||||
|
|
||||||
|
Keep the feature boundary exact. `--no-default-features` is the contract-only
|
||||||
|
surface used where a Mind seals provider request bytes. The default `client`
|
||||||
|
surface adds only authenticated framing and socket transport. `daemon` adds the
|
||||||
|
service authorities. Contract-only consumers must not compile client crypto or
|
||||||
|
socket physiology by accident.
|
||||||
|
|
||||||
Do not link upstream Codex crates into this package. Idunn freezes the official
|
Do not link upstream Codex crates into this package. Idunn freezes the official
|
||||||
Codex binary as an exact package input. The child receives auth RPC only, never
|
Codex binary as an exact package input. The child receives auth RPC only, never
|
||||||
consumer identity, prompts, tools, provider requests, or model output.
|
consumer identity, prompts, tools, provider requests, or model output.
|
||||||
|
|
|
||||||
14
Cargo.toml
14
Cargo.toml
|
|
@ -17,8 +17,14 @@ path = "src/main.rs"
|
||||||
required-features = ["daemon"]
|
required-features = ["daemon"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["client"]
|
||||||
|
client = [
|
||||||
|
"dep:aes-gcm",
|
||||||
|
"dep:rand_core",
|
||||||
|
"dep:zeroize",
|
||||||
|
]
|
||||||
daemon = [
|
daemon = [
|
||||||
|
"client",
|
||||||
"dep:anyhow",
|
"dep:anyhow",
|
||||||
"dep:chrono",
|
"dep:chrono",
|
||||||
"dep:cultcache-rs",
|
"dep:cultcache-rs",
|
||||||
|
|
@ -30,14 +36,14 @@ daemon = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes-gcm = "0.10"
|
aes-gcm = { version = "0.10", optional = true }
|
||||||
anyhow = { version = "1", optional = true }
|
anyhow = { version = "1", optional = true }
|
||||||
base64 = "0.23"
|
base64 = "0.23"
|
||||||
chrono = { version = "0.4", features = ["serde"], optional = true }
|
chrono = { version = "0.4", features = ["serde"], optional = true }
|
||||||
cultcache-rs = { git = "https://github.com/GameCult/cultcache-rs.git", rev = "fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c", optional = true }
|
cultcache-rs = { git = "https://github.com/GameCult/cultcache-rs.git", rev = "fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c", optional = true }
|
||||||
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "75c180782aeba7cfd22d6412877397708a4ed28f", optional = true }
|
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "75c180782aeba7cfd22d6412877397708a4ed28f", optional = true }
|
||||||
ed25519-dalek = { version = "2.2", optional = true }
|
ed25519-dalek = { version = "2.2", optional = true }
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
rand_core = { version = "0.6", features = ["getrandom"], optional = true }
|
||||||
rmp-serde = "1"
|
rmp-serde = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_bytes = { version = "0.11", optional = true }
|
serde_bytes = { version = "0.11", optional = true }
|
||||||
|
|
@ -46,4 +52,4 @@ sha2 = "0.10"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
ureq = { version = "3.4", default-features = false, features = ["rustls"], optional = true }
|
ureq = { version = "3.4", default-features = false, features = ["rustls"], optional = true }
|
||||||
uuid = { version = "1", features = ["v4"], optional = true }
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
zeroize = "1"
|
zeroize = { version = "1", optional = true }
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -12,11 +12,13 @@ one credential store. The public connector reads the credential only after the
|
||||||
child's private auth RPC completes, then performs the exact Responses request.
|
child's private auth RPC completes, then performs the exact Responses request.
|
||||||
It owns no prompt policy or decision authority.
|
It owns no prompt policy or decision authority.
|
||||||
|
|
||||||
The default library is the shared consumer ABI: v2 typed contracts, exact
|
The library has three explicit compile surfaces. With no features it exposes
|
||||||
provider lowering, encryption, bounded direct-pipe framing, one-request socket
|
only the v2 provider/result contracts and their deterministic validation. The
|
||||||
transport, and exact result validation. It does not compile CultCache, HTTP,
|
default `client` feature adds encryption, bounded direct-pipe framing, and the
|
||||||
TLS, provider auth, replay, or daemon code. Epiphany and Ghostlight consume this
|
one-request socket client. It still compiles no CultCache, HTTP, TLS, provider
|
||||||
surface instead of copying wire law.
|
auth, replay, or daemon code. The `daemon` feature includes the client surface
|
||||||
|
and the service. Epiphany and Ghostlight can therefore type and seal their exact
|
||||||
|
provider requests without importing transport physiology into their Minds.
|
||||||
|
|
||||||
Connection establishment is bounded independently. Response read/write timeout
|
Connection establishment is bounded independently. Response read/write timeout
|
||||||
is optional so a consumer's typed outer pass may remain the sole deadline owner.
|
is optional so a consumer's typed outer pass may remain the sole deadline owner.
|
||||||
|
|
@ -45,9 +47,10 @@ No upstream Codex crate is linked into the package. The official binary is a
|
||||||
pinned deployment input, keeping Codex's application build graph outside both
|
pinned deployment input, keeping Codex's application build graph outside both
|
||||||
consumers and this small daemon.
|
consumers and this small daemon.
|
||||||
|
|
||||||
Focused verification uses the two real build surfaces:
|
Focused verification proves each real build surface independently:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
cargo test --lib --no-default-features
|
||||||
cargo test --lib
|
cargo test --lib
|
||||||
cargo test --lib --features daemon
|
cargo test --lib --features daemon
|
||||||
cargo check --bin codex-connector --features daemon
|
cargo check --bin codex-connector --features daemon
|
||||||
|
|
|
||||||
30
src/lib.rs
30
src/lib.rs
|
|
@ -1,6 +1,9 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use std::net::{SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[cfg(feature = "daemon")]
|
#[cfg(feature = "daemon")]
|
||||||
|
|
@ -8,14 +11,18 @@ use std::collections::HashMap;
|
||||||
#[cfg(feature = "daemon")]
|
#[cfg(feature = "daemon")]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use aes_gcm::aead::{Aead, Payload};
|
use aes_gcm::aead::{Aead, Payload};
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||||
#[cfg(feature = "daemon")]
|
#[cfg(feature = "daemon")]
|
||||||
use cultcache_rs::{CultCache, DatabaseEntry, OwnedRedbMessagePackBackingStore};
|
use cultcache_rs::{CultCache, DatabaseEntry, OwnedRedbMessagePackBackingStore};
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::{OsRng, RngCore};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
#[cfg(feature = "client")]
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
#[cfg(feature = "daemon")]
|
#[cfg(feature = "daemon")]
|
||||||
|
|
@ -59,9 +66,11 @@ pub const RESULT_SCHEMA_ID: &str = "gamecult.codex.transport_result.v2";
|
||||||
pub const RECEIPT_SCHEMA_ID: &str = "gamecult.codex.transport_receipt.v2";
|
pub const RECEIPT_SCHEMA_ID: &str = "gamecult.codex.transport_receipt.v2";
|
||||||
pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.codex.transport_envelope.v2";
|
pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.codex.transport_envelope.v2";
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct CodexTransportKey([u8; 32]);
|
pub struct CodexTransportKey([u8; 32]);
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
impl CodexTransportKey {
|
impl CodexTransportKey {
|
||||||
pub fn from_connection_secret(secret: &str) -> Result<Self, ServiceError> {
|
pub fn from_connection_secret(secret: &str) -> Result<Self, ServiceError> {
|
||||||
if secret.trim().is_empty() || secret.trim() != secret {
|
if secret.trim().is_empty() || secret.trim() != secret {
|
||||||
|
|
@ -71,6 +80,7 @@ impl CodexTransportKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
impl Drop for CodexTransportKey {
|
impl Drop for CodexTransportKey {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.0.zeroize();
|
self.0.zeroize();
|
||||||
|
|
@ -634,12 +644,14 @@ pub fn canonical_invocation_bytes(
|
||||||
rmp_serde::to_vec(invocation).map_err(|_| ContractError::Encoding)
|
rmp_serde::to_vec(invocation).map_err(|_| ContractError::Encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum CodexEnvelopeKind {
|
pub enum CodexEnvelopeKind {
|
||||||
Invocation,
|
Invocation,
|
||||||
Result,
|
Result,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct CodexTransportEnvelope {
|
pub struct CodexTransportEnvelope {
|
||||||
pub schema_id: String,
|
pub schema_id: String,
|
||||||
|
|
@ -650,6 +662,7 @@ pub struct CodexTransportEnvelope {
|
||||||
pub ciphertext: Vec<u8>,
|
pub ciphertext: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
pub fn encrypt_invocation(
|
pub fn encrypt_invocation(
|
||||||
invocation: &CodexTransportInvocation,
|
invocation: &CodexTransportInvocation,
|
||||||
security: &CodexTransportKey,
|
security: &CodexTransportKey,
|
||||||
|
|
@ -664,6 +677,7 @@ pub fn encrypt_invocation(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
pub fn decrypt_invocation(
|
pub fn decrypt_invocation(
|
||||||
envelope: &CodexTransportEnvelope,
|
envelope: &CodexTransportEnvelope,
|
||||||
security: &CodexTransportKey,
|
security: &CodexTransportKey,
|
||||||
|
|
@ -680,6 +694,7 @@ pub fn decrypt_invocation(
|
||||||
Ok(invocation)
|
Ok(invocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
pub fn encrypt_result(
|
pub fn encrypt_result(
|
||||||
result: &CodexTransportResult,
|
result: &CodexTransportResult,
|
||||||
security: &CodexTransportKey,
|
security: &CodexTransportKey,
|
||||||
|
|
@ -694,6 +709,7 @@ pub fn encrypt_result(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
pub fn decrypt_result(
|
pub fn decrypt_result(
|
||||||
envelope: &CodexTransportEnvelope,
|
envelope: &CodexTransportEnvelope,
|
||||||
security: &CodexTransportKey,
|
security: &CodexTransportKey,
|
||||||
|
|
@ -712,6 +728,7 @@ pub fn decrypt_result(
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CodexConnectorClient {
|
pub struct CodexConnectorClient {
|
||||||
endpoint: SocketAddr,
|
endpoint: SocketAddr,
|
||||||
|
|
@ -720,6 +737,7 @@ pub struct CodexConnectorClient {
|
||||||
response_timeout: Option<Duration>,
|
response_timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
impl CodexConnectorClient {
|
impl CodexConnectorClient {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
endpoint: SocketAddr,
|
endpoint: SocketAddr,
|
||||||
|
|
@ -770,6 +788,7 @@ impl CodexConnectorClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CodexConnectorClientError {
|
pub enum CodexConnectorClientError {
|
||||||
#[error("invalid connector client configuration")]
|
#[error("invalid connector client configuration")]
|
||||||
|
|
@ -784,12 +803,14 @@ pub enum CodexConnectorClientError {
|
||||||
Transport(#[from] ServiceError),
|
Transport(#[from] ServiceError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum TransportFrameError {
|
enum TransportFrameError {
|
||||||
Connection(std::io::Error),
|
Connection(std::io::Error),
|
||||||
Size,
|
Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn client_frame_error(error: TransportFrameError) -> CodexConnectorClientError {
|
fn client_frame_error(error: TransportFrameError) -> CodexConnectorClientError {
|
||||||
match error {
|
match error {
|
||||||
TransportFrameError::Connection(error) => CodexConnectorClientError::Connection(error),
|
TransportFrameError::Connection(error) => CodexConnectorClientError::Connection(error),
|
||||||
|
|
@ -797,6 +818,7 @@ fn client_frame_error(error: TransportFrameError) -> CodexConnectorClientError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn read_transport_frame(
|
fn read_transport_frame(
|
||||||
reader: &mut impl Read,
|
reader: &mut impl Read,
|
||||||
max_frame_bytes: usize,
|
max_frame_bytes: usize,
|
||||||
|
|
@ -816,6 +838,7 @@ fn read_transport_frame(
|
||||||
Ok(payload)
|
Ok(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn write_transport_frame(
|
fn write_transport_frame(
|
||||||
writer: &mut impl Write,
|
writer: &mut impl Write,
|
||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
|
|
@ -831,6 +854,7 @@ fn write_transport_frame(
|
||||||
.map_err(TransportFrameError::Connection)
|
.map_err(TransportFrameError::Connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn encrypt_message(
|
fn encrypt_message(
|
||||||
caller_runtime_id: &str,
|
caller_runtime_id: &str,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
|
|
@ -868,6 +892,7 @@ fn encrypt_message(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn decrypt_message(
|
fn decrypt_message(
|
||||||
envelope: &CodexTransportEnvelope,
|
envelope: &CodexTransportEnvelope,
|
||||||
security: &CodexTransportKey,
|
security: &CodexTransportKey,
|
||||||
|
|
@ -890,6 +915,7 @@ fn decrypt_message(
|
||||||
.map_err(|_| ServiceError::Encryption)
|
.map_err(|_| ServiceError::Encryption)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn envelope_aad(
|
fn envelope_aad(
|
||||||
schema_id: &str,
|
schema_id: &str,
|
||||||
caller_runtime_id: &str,
|
caller_runtime_id: &str,
|
||||||
|
|
@ -900,6 +926,7 @@ fn envelope_aad(
|
||||||
.map_err(|_| ServiceError::Encoding)
|
.map_err(|_| ServiceError::Encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn validate_envelope(
|
fn validate_envelope(
|
||||||
envelope: &CodexTransportEnvelope,
|
envelope: &CodexTransportEnvelope,
|
||||||
expected_kind: CodexEnvelopeKind,
|
expected_kind: CodexEnvelopeKind,
|
||||||
|
|
@ -1643,6 +1670,7 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
fn security(key: &str) -> CodexTransportKey {
|
fn security(key: &str) -> CodexTransportKey {
|
||||||
CodexTransportKey::from_connection_secret(key).unwrap()
|
CodexTransportKey::from_connection_secret(key).unwrap()
|
||||||
}
|
}
|
||||||
|
|
@ -1654,6 +1682,7 @@ mod tests {
|
||||||
CodexTransportInvocation::new(caller_runtime_id, 2_000, [7; 32], request).unwrap()
|
CodexTransportInvocation::new(caller_runtime_id, 2_000, [7; 32], request).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[test]
|
#[test]
|
||||||
fn encrypted_invocation_hides_content_and_binds_outer_identity() {
|
fn encrypted_invocation_hides_content_and_binds_outer_identity() {
|
||||||
let invocation = invocation();
|
let invocation = invocation();
|
||||||
|
|
@ -1682,6 +1711,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "client")]
|
||||||
#[test]
|
#[test]
|
||||||
fn shared_client_transports_one_exact_encrypted_request_and_result() {
|
fn shared_client_transports_one_exact_encrypted_request_and_result() {
|
||||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -500,7 +500,7 @@ fn parse_responses_sse(reader: impl Read) -> Result<ParsedProviderStream, &'stat
|
||||||
.by_ref()
|
.by_ref()
|
||||||
.take((MAX_SSE_FRAME_BYTES + 1) as u64)
|
.take((MAX_SSE_FRAME_BYTES + 1) as u64)
|
||||||
.read_until(b'\n', &mut line)
|
.read_until(b'\n', &mut line)
|
||||||
.map_err(|error| provider_sse_read_error(error.kind()))?;
|
.map_err(|error| provider_sse_read_error(&error))?;
|
||||||
if read == 0 {
|
if read == 0 {
|
||||||
if !data.is_empty() {
|
if !data.is_empty() {
|
||||||
state.accept_frame(&data)?;
|
state.accept_frame(&data)?;
|
||||||
|
|
@ -544,7 +544,24 @@ fn parse_responses_sse(reader: impl Read) -> Result<ParsedProviderStream, &'stat
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn provider_sse_read_error(kind: io::ErrorKind) -> &'static str {
|
fn provider_sse_read_error(error: &io::Error) -> &'static str {
|
||||||
|
if let Some(ureq_error) = error
|
||||||
|
.get_ref()
|
||||||
|
.and_then(|source| source.downcast_ref::<ureq::Error>())
|
||||||
|
{
|
||||||
|
return match ureq_error {
|
||||||
|
ureq::Error::Timeout(_) => "provider SSE read timed out",
|
||||||
|
ureq::Error::Protocol(_) => "provider SSE protocol failed",
|
||||||
|
ureq::Error::Tls(_) | ureq::Error::Rustls(_) => "provider SSE secure transport failed",
|
||||||
|
ureq::Error::BodyStalled => "provider SSE body stalled",
|
||||||
|
ureq::Error::Io(inner) => provider_sse_io_error(inner.kind()),
|
||||||
|
_ => "provider SSE read failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
provider_sse_io_error(error.kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_sse_io_error(kind: io::ErrorKind) -> &'static str {
|
||||||
match kind {
|
match kind {
|
||||||
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => "provider SSE read timed out",
|
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => "provider SSE read timed out",
|
||||||
io::ErrorKind::ConnectionReset
|
io::ErrorKind::ConnectionReset
|
||||||
|
|
@ -1057,6 +1074,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WrappedFailingReader(Option<ureq::Error>);
|
||||||
|
|
||||||
|
impl Read for WrappedFailingReader {
|
||||||
|
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
Err(io::Error::other(self.0.take().unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn jwt(payload: Value) -> String {
|
fn jwt(payload: Value) -> String {
|
||||||
let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap());
|
let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap());
|
||||||
format!("e30.{payload}.signature")
|
format!("e30.{payload}.signature")
|
||||||
|
|
@ -1257,6 +1282,10 @@ mod tests {
|
||||||
parse_responses_sse(FailingReader(io::ErrorKind::Other)).err(),
|
parse_responses_sse(FailingReader(io::ErrorKind::Other)).err(),
|
||||||
Some("provider SSE read failed")
|
Some("provider SSE read failed")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_responses_sse(WrappedFailingReader(Some(ureq::Error::BodyStalled))).err(),
|
||||||
|
Some("provider SSE body stalled")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue