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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions