Skip to content

fix(common): trust the system certificate store so HTTP works behind an intercepting proxy #76

Description

@AbysmalBiscuit

Every devkit HTTP call fails in an environment that terminates TLS at a proxy. Observed in a Claude cloud session on 2026-09-15:

$ issue setup 75 --dry-run --no-summary
Error: fetching the title for 75
Caused by:
    0: https://api.github.com/graphql: Connection Failed: tls connection init failed: invalid peer certificate: UnknownIssuer
    1: invalid peer certificate: UnknownIssuer

The handshake completes and the proxy's certificate arrives; devkit rejects it. ureq's tls feature pins webpki-roots, a copy of Mozilla's root list compiled into the binary, so devkit never consults the machine's own trust store. The environment had already installed its CA there and set SSL_CERT_FILE. Neither reaches devkit.

This is not cloud-specific. A corporate laptop running Zscaler, Netskope, or any MITM inspection proxy fails the same way, and a self-hosted GitHub Enterprise behind an internal CA fails the same way.

Blast radius

Every network path, because none of them share a client:

Call site Client
devkit-common/src/github.rs pooled ureq::Agent via AgentBuilder
devkit-common/src/tracker/linear.rs bare ureq::post, fresh default agent per call
devkit-common/src/slack.rs bare ureq::post, fresh default agent per call
devkit-docs/src/lookup.rs bare ureq::get against crates.io, npm, PyPI

Only github.rs builds an agent it could configure. The other three construct a default agent per request, so there is no single place to set trust roots today.

The gh fallback does not save it. fetch_graphql in devkit-issue/src/prs.rs falls back to gh api graphql when the HTTP path fails, but the tracker's title fetch has no such fallback, and the cloud image has no gh on PATH at all. devkit auth github reports token from GH_TOKEN but could not resolve the identity for the same reason, which reads like a bad token rather than a TLS failure.

Proposed fix

Enable ureq's native-certs feature and route every call site through one shared agent builder in devkit-common.

native-certs = [dep:rustls-native-certs] is available in the locked ureq 2.12.1. It reads the platform trust store, which is where a proxy CA, a corporate CA, and an internal CA all already live. Also honor SSL_CERT_FILE and SSL_CERT_DIR explicitly, since those are the conventional names a sandbox tells its tools to use and the cloud environment sets them.

Centralizing the agent is a prerequisite, not a separate cleanup: three of the four call sites cannot be configured without it. It also gets connection pooling on the Linear, Slack, and docs paths, which currently redial per request.

On gating this behind a cloud environment variable

I would not. Two reasons.

The condition that matters is "something is intercepting my TLS", not "I am in a cloud VM". A CLOUD/IS_CLOUD check fixes the hosted session and leaves every corporate laptop broken, which is the larger population.

And the behavior being gated is "also trust the certificates this machine trusts". That is the correct default everywhere, not a concession to make in one environment. Making it conditional adds a knob whose only other setting is wrong, and invites someone to reach for a verification bypass when the real answer is a trust store read.

If devkit does want a cloud signal for other behavior, that is worth its own discussion, but this bug should not be the thing that introduces it.

Acceptance criteria

  • One agent builder in devkit-common serves GitHub, Linear, Slack, and docs lookups.
  • The client trusts the platform certificate store in addition to the bundled roots.
  • SSL_CERT_FILE and SSL_CERT_DIR, when set, are honored.
  • TLS verification is never disabled, and no flag or environment variable can disable it.
  • A TLS trust failure reports itself as one, naming the certificate store consulted, rather than surfacing as could not resolve the identity.
  • issue setup <n> --dry-run derives a slug from the issue title in a cloud session.

Notes

Slug derivation is the visible symptom but the smallest one. With this broken, a cloud agent also loses issue state, PR triage, issue status, issue prs, and the docs lookups, and must pass --slug by hand to issue setup.

Secondary, not required here: ureq's proxy-from-env feature is off, so devkit ignores HTTPS_PROXY. It did not matter in the observed environment because interception was transparent, but an environment requiring an explicit CONNECT would fail differently.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions