fix(runner): initialize Rustls crypto provider (#13023)

## Thinking Path

> - Paperclip runs AI agents through local and remote execution
adapters.
> - The native runner daemon uses Rustls for secure transport.
> - The production dependency graph enables the `ring` and `aws-lc-rs`
crypto backends.
> - Rustls cannot select a default backend when both backends are
active.
> - The runner daemon did not select a backend before it built a TLS
client configuration.
> - This pull request installs the workspace-selected `ring` provider
during process startup.
> - The benefit is that the runner can start reliably with the
production feature graph.

## Linked Issues or Issue Description

**What happened?**

`paperclip-runnerd` exited with code 101 before it opened a provider
session. Rustls reported that it could not select a process-level
`CryptoProvider` because the binary included two crypto backends.

**Expected behavior**

The runner daemon must select its configured crypto provider before it
creates a TLS client configuration. The daemon must start and open the
provider session.

**Steps to reproduce**

1. Build `paperclip-runnerd` with the locked production dependency
graph.
2. Start the daemon through the local loopback transport.
3. Observe the Rustls provider-selection panic before this change.

**Paperclip version or commit**

`d8b958053` on `master`.

**Deployment mode**

Self-hosted server.

**Installation method**

Built from source.

**Agent adapter(s) involved**

Codex through `paperclip_runner`.

**Operating system**

Linux 7.0.0-1010-aws on aarch64.

## What Changed

- Install the Rustls `ring` provider before runner setup reaches TLS
initialization.
- Accept an existing process-level provider as an initialized state.
- Add a focused regression test for TLS builder creation and repeated
initialization.

## Verification

- `cargo test --manifest-path
packages/paperclip-runner/runner/Cargo.toml --locked -p
paperclip-runner-core --bin paperclip-runnerd
startup_installs_a_crypto_provider_before_tls_initialization`
- `cargo test --manifest-path
packages/paperclip-runner/runner/Cargo.toml --locked -p
paperclip-runner-core --test local_runner
runnerd_startup_reports_build_metadata_without_panicking`
- `cargo test --manifest-path
packages/paperclip-runner/runner/Cargo.toml --locked -p
paperclip-runner-core --test local_runner
happy_path_emits_one_result_and_one_terminal`
- `cargo fmt --manifest-path packages/paperclip-runner/runner/Cargo.toml
--all -- --check`
- Built the debug daemon and ran `paperclip-runnerd --build-metadata`
successfully.
- `pnpm -r typecheck`
- `pnpm build`

## Risks

- Low risk. The change selects the crypto provider that the workspace
already declares.
- A host process can install a provider first. The runner accepts that
initialized state.
- No database, API, UI, telemetry, observability, or run-log contract
changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex with model `gpt-5`. The context-window size is not
exposed. The model used reasoning, repository tools, code execution, and
test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-07 21:58:01 -05:00 committed by GitHub
parent d8b9580531
commit ff24578765
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 19 deletions

View File

@ -104,6 +104,14 @@ fn install_diagnostic_panic_hook(directory: Option<PathBuf>) {
}));
}
fn install_crypto_provider() {
// The production dependency graph enables both rustls crypto backends.
// Select the backend declared by this workspace before any TLS builder
// asks rustls for the process-level default. An embedding process may have
// already selected a provider, which is also a valid initialized state.
let _ = rustls::crypto::ring::default_provider().install_default();
}
fn build_metadata() -> serde_json::Value {
json!({
"schema": RUNNERD_BUILD_METADATA_SCHEMA,
@ -359,6 +367,28 @@ fn run(args: &[String]) -> Result<(), LocalRunnerError> {
})
}
fn run_main(args: Vec<String>) -> ExitCode {
let diagnostics_directory = diagnostic_directory(&args);
install_diagnostic_panic_hook(diagnostics_directory.clone());
install_crypto_provider();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
let message = format!("paperclip-runnerd: {error}");
if let Some(directory) = diagnostics_directory {
if let Err(persist_error) = persist_runner_diagnostic(&directory, &message) {
eprintln!(
"paperclip-runnerd: failed to persist bounded diagnostic: {persist_error}"
);
}
} else {
eprintln!("{message}");
}
ExitCode::FAILURE
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -374,6 +404,17 @@ mod tests {
);
}
#[test]
fn startup_installs_a_crypto_provider_before_tls_initialization() {
let _ = run_main(vec!["--build-metadata".to_owned()]);
assert!(rustls::crypto::CryptoProvider::get_default().is_some());
// Startup is process-global. A repeated startup call must remain
// safe when a provider was selected earlier in the process lifetime.
let _ = run_main(vec!["--build-metadata".to_owned()]);
let _ = rustls::ClientConfig::builder();
}
#[test]
fn persistent_diagnostic_is_private_bounded_and_redacted() {
let unique = format!(
@ -422,23 +463,5 @@ mod tests {
}
fn main() -> ExitCode {
let args = std::env::args().skip(1).collect::<Vec<_>>();
let diagnostics_directory = diagnostic_directory(&args);
install_diagnostic_panic_hook(diagnostics_directory.clone());
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
let message = format!("paperclip-runnerd: {error}");
if let Some(directory) = diagnostics_directory {
if let Err(persist_error) = persist_runner_diagnostic(&directory, &message) {
eprintln!(
"paperclip-runnerd: failed to persist bounded diagnostic: {persist_error}"
);
}
} else {
eprintln!("{message}");
}
ExitCode::FAILURE
}
}
run_main(std::env::args().skip(1).collect())
}

View File

@ -68,6 +68,19 @@ fn event_type(message: &Value) -> Option<&str> {
.flatten()
}
#[test]
fn runnerd_startup_reports_build_metadata_without_panicking() {
let output = Command::new(env!("CARGO_BIN_EXE_paperclip-runnerd"))
.arg("--build-metadata")
.output()
.expect("runner daemon should start");
assert!(output.status.success());
let metadata: Value =
serde_json::from_slice(&output.stdout).expect("build metadata should be valid JSON");
assert_eq!(metadata["binaryName"], "paperclip-runnerd");
}
#[test]
fn happy_path_emits_one_result_and_one_terminal() {
let commands = [