Skip to content

keygen: add generate_signing_keypair() programmatic API alongside adcp-keygen CLI #217

@bokelley

Description

@bokelley

Observation

Round-8 webhooks-9421 DX agent (PR #205) reported:

"`adcp-keygen` shipped to `.venv/bin/` isn't on PATH when invoking `.venv/bin/python` directly (typical CI / subprocess context). First `subprocess.run(["adcp-keygen", ...])` raised `FileNotFoundError`. Worked around with `Path(sys.executable).parent / "adcp-keygen"`. Suggest either documenting this idiom in the keygen module or shipping `generate_signing_keypair()` as a programmatic API returning `(pem_bytes, public_jwk)` so callers don't need to shell out."

Proposal

Add a programmatic entry point to src/adcp/signing/keygen.py:

def generate_signing_keypair(
    *,
    alg: Literal["ed25519", "es256"] = "ed25519",
    kid: str | None = None,
    purpose: Literal["request-signing", "webhook-signing"] = "request-signing",
    passphrase: bytes | None = None,
) -> tuple[bytes, dict[str, Any]]:
    """Generate a signing keypair. Returns (pem_bytes, public_jwk).

    Programmatic companion to the ``adcp-keygen`` CLI — call this from
    tests, provisioning scripts, or any non-shell context where spawning
    a subprocess is wrong.

    Returns:
        (pem_bytes, public_jwk) tuple. ``pem_bytes`` is the PKCS#8
        private key (optionally encrypted if ``passphrase`` is set).
        ``public_jwk`` is the public half, ready to publish at your
        agent's ``jwks_uri``.
    """

Re-export from adcp.signing + top-level adcp. Rewrite the CLI's main() to call this helper + handle file-writing + stdout printing separately. Zero duplication between CLI and programmatic paths.

Acceptance

  • Unit test: generate_signing_keypair() returns a PEM that loads via load_pem_private_key and a JWK that WebhookSender.from_jwk accepts.
  • Unit test: purpose="webhook-signing" produces a JWK with adcp_use: "webhook-signing".
  • CLI tests still pass — CLI main() now just wraps the helper.
  • Doc example in src/adcp/signing/__init__.py showing programmatic + CLI equivalence.

Priority

4.1 DX polish. Current CLI works; this eliminates subprocess-path ergonomic tax for test harnesses and provisioning code.

Related: #205 (round-8 validation)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions