fix: retain safe Workdir open authorization and response failure causes
Keep bounded reasons and derived failure stages separate from public classification. Reject provider-authored raw error text on transport decoding. Preserve identity mismatch when cleanup fails and retain its handle for explicit close without opening again. Add signed Runtime HTTP, Registry/API logging, remote mismatch, authorization no-dispatch and deferred fault coverage. Focused tests and Workdir suite pass; full-source validation follows T773 audit contract merge.
This commit is contained in:
@@ -83,3 +83,34 @@ fn provider_cannot_inject_free_text_failure_reason() {
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_authored_operational_messages_never_enter_error_display_or_debug() {
|
||||
for code in [
|
||||
Code::Conflict,
|
||||
Code::OutcomeUnknown,
|
||||
Code::Unsupported,
|
||||
Code::InvalidRequest,
|
||||
Code::Unavailable,
|
||||
Code::Io,
|
||||
Code::Transport,
|
||||
Code::Internal,
|
||||
] {
|
||||
let wire = serde_json::json!({
|
||||
"code": code, "message":"/host/private command-secret body-secret proof-secret key-secret Authorization: Bearer bearer-secret",
|
||||
"failure_reason": "session_open_task_failed"
|
||||
});
|
||||
let decoded: WorkdirTransportError = serde_json::from_value(wire).unwrap();
|
||||
let error = decoded.into_workdir_error();
|
||||
for text in [
|
||||
format!("{error}"),
|
||||
format!("{error:?}"),
|
||||
serde_json::to_string(&WorkdirTransportError::from_workdir_error(&error)).unwrap(),
|
||||
] {
|
||||
assert!(!text.contains("secret"), "{text}");
|
||||
assert!(!text.contains("/host/private"), "{text}");
|
||||
}
|
||||
assert_eq!(error.failure_reason(), Some(Reason::SessionOpenTaskFailed));
|
||||
assert_eq!(WorkdirTransportError::from_workdir_error(&error).code, code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,3 +216,106 @@ async fn remote_checkout_diagnostics_preserve_mutation_outcome_and_read_failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mismatched_http_results_keep_cause_and_mutation_uncertainty() {
|
||||
use workdir::{
|
||||
CheckoutOperation, CheckoutRequest, CommandHandle, CommandOutputRequest,
|
||||
WorkdirFailureReason as Failure,
|
||||
};
|
||||
for case in ["variant", "bytes", "mutation", "identity"] {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
reply(
|
||||
&listener,
|
||||
"200 OK",
|
||||
if case == "identity" {
|
||||
r#"{"session_id":"s","workdir_id":"wrong-secret","capabilities":{"bits":63}}"#
|
||||
} else {
|
||||
r#"{"session_id":"s","workdir_id":"w","capabilities":{"bits":63}}"#
|
||||
},
|
||||
);
|
||||
if case != "identity" {
|
||||
let result = if case == "bytes" {
|
||||
serde_json::json!({"operation":"command_output","result":{
|
||||
"chunks": [], "next_cursor": 123, "terminal": null
|
||||
}})
|
||||
} else {
|
||||
serde_json::to_value(
|
||||
workdir::http::WorkdirSessionOperationResult::CommandCancel,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
reply(&listener, "200 OK", &result.to_string());
|
||||
}
|
||||
});
|
||||
let opened = RemoteWorkdirSession::open(
|
||||
reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap(),
|
||||
format!("http://{address}").parse().unwrap(),
|
||||
"request-secret",
|
||||
Workdir::new("w").id().clone(),
|
||||
OpenWorkdirSessionRequest::default(),
|
||||
)
|
||||
.await;
|
||||
let error = if case == "identity" {
|
||||
opened.unwrap_err()
|
||||
} else {
|
||||
let remote = opened.unwrap();
|
||||
match case {
|
||||
"variant" => remote
|
||||
.stat(StatRequest {
|
||||
path: WorkdirPath::new("file-secret").unwrap(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err(),
|
||||
"bytes" => remote
|
||||
.command_output(CommandOutputRequest {
|
||||
handle: CommandHandle("command-secret".into()),
|
||||
cursor: 0,
|
||||
limit: 16,
|
||||
wait: false,
|
||||
})
|
||||
.await
|
||||
.unwrap_err(),
|
||||
"mutation" => remote
|
||||
.checkout_execute(CheckoutRequest {
|
||||
target: WorkdirPath::new("file-secret").unwrap(),
|
||||
validator: vec![1],
|
||||
operation: CheckoutOperation::Write {
|
||||
content: b"body-secret".to_vec(),
|
||||
expected_hash: [0; 32],
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap_err(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
server.join().unwrap();
|
||||
let expected = if case == "identity" {
|
||||
Failure::SessionIdentityMismatch
|
||||
} else {
|
||||
Failure::OperationResultMismatch
|
||||
};
|
||||
assert_eq!(error.failure_reason(), Some(expected), "{case}: {error:?}");
|
||||
let transport = WorkdirTransportError::from_workdir_error(&error);
|
||||
assert_eq!(
|
||||
transport.code,
|
||||
if case == "mutation" {
|
||||
Code::OutcomeUnknown
|
||||
} else {
|
||||
Code::Unavailable
|
||||
}
|
||||
);
|
||||
let wire = serde_json::to_string(&transport).unwrap();
|
||||
assert!(!wire.contains("secret"), "{wire}");
|
||||
assert!(!format!("{error:?}").contains("secret"));
|
||||
let forwarded = transport.into_workdir_error();
|
||||
assert_eq!(forwarded.failure_reason(), Some(expected));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user