Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# =============================================================================
# Release & publish workflow (conclave v1.0 — distribution + release-eng)
#
# !!! INERT UNTIL CONFIGURED — SAFE TO MERGE !!!
#
# This workflow does NOTHING until BOTH of the following are true:
# (a) a GitHub *Release* is published (a pushed git tag ALONE does not trigger
# it — the trigger is `release: published`, the explicit human gesture), AND
# (b) the `conclave-cli` PyPI project + its OIDC Trusted Publisher are configured
# by the owner (see RELEASING.md "One-time PyPI setup").
#
# Until (a) AND (b) hold, merging this file changes nothing at runtime: no tag is
# cut here, no version is bumped here, nothing is published here. Cutting v1.0.0 is
# then a single tag + GitHub Release away (the full runbook is in RELEASING.md).
#
# What it does WHEN a Release is published:
# build — builds sdist + wheel with `python -m build`, uploads them as
# workflow artifacts so publish + sign consume the exact same bytes.
# pypi-publish — publishes those artifacts to PyPI via OIDC Trusted Publishing
# (NO API token, NO stored secret), with PEP 740 attestations.
# Fails CLOSED if the Trusted Publisher is not yet configured —
# it never falls back to anything insecure.
# sign — signs the sdist + wheel with Sigstore keyless (ambient OIDC ->
# Fulcio) and attaches the `.sigstore` bundles to the GitHub
# Release assets.
#
# Distribution-name note: the PyPI distribution name is `conclave-cli` (the name
# `conclave` is taken by an unrelated project). The command + import package stay
# `conclave`. The Trusted Publisher must therefore name PROJECT `conclave-cli`.
#
# Pin discipline: every `uses:` is pinned to a full 40-char commit SHA with a
# `# vX.Y.Z` comment. conclave's other workflows (test.yml, gitleaks.yml) pin to
# mutable major tags; this publish workflow holds elevated OIDC permissions, so it
# is hardened one level further with full commit-SHA pins.
# =============================================================================
name: Release

on:
# Primary, safe trigger: only fires once a GitHub Release is *published*.
# A pushed tag by itself does NOT publish a Release and therefore does NOT
# trigger this workflow.
release:
types: [published]

# Least-privilege at the top level; each job widens scope locally only as needed.
permissions:
contents: read

jobs:
# --------------------------------------------------------------------------
# Job 1: build sdist + wheel and publish them as workflow artifacts so the
# publish and sign jobs consume the exact same bytes (no rebuild drift).
# --------------------------------------------------------------------------
build:
name: Build sdist + wheel
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Python 3.12
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
cache: pip

- name: Install build frontend
run: python -m pip install --upgrade "build==1.2.2.post1"

- name: Build sdist + wheel
run: python -m build --sdist --wheel --outdir dist/

- name: Show built artifacts
run: ls -l dist/

- name: Upload dist artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
if-no-files-found: error

# --------------------------------------------------------------------------
# Job 2: publish to PyPI via OIDC Trusted Publishing.
#
# No API token, no stored secret: pypa/gh-action-pypi-publish mints a short-lived
# OIDC token from this job's `id-token: write` and exchanges it for a PyPI upload
# token. PyPI must have a Trusted Publisher (pending or active) for `conclave-cli`
# pointing at owner=ernestprovo23, repo=conclave, workflow=release.yml — see
# RELEASING.md. Until that exists, this job fails CLOSED (publish denied); it
# never falls back to anything insecure.
#
# PEP 740 attestations are generated + attached by default (the action's
# `attestations: true` default) since OIDC Trusted Publishing is in use.
# --------------------------------------------------------------------------
pypi-publish:
name: Publish to PyPI (OIDC Trusted Publishing)
needs: build
runs-on: ubuntu-latest
# id-token: write is scoped to THIS job only (never granted at workflow top level).
permissions:
id-token: write # REQUIRED for OIDC Trusted Publishing — mints the token
contents: read

steps:
- name: Download dist artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/

# Production PyPI via Trusted Publishing (OIDC). No `password:` — the action
# uses ambient OIDC. PEP 740 attestations are on by default.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: dist/
# No `password:` — OIDC Trusted Publishing is used (no stored secret).
# `attestations` defaults to true under Trusted Publishing -> PEP 740.

# --------------------------------------------------------------------------
# Job 3: sign the release artifacts with Sigstore keyless.
#
# The job's ambient OIDC mints a short-lived Fulcio cert (issuer
# https://token.actions.githubusercontent.com), signs each dist file, and
# attaches the resulting `.sigstore` bundle(s) to the GitHub Release assets
# (release-signing-artifacts, the action's default on a release event).
#
# We self-verify (`verify: true`) against THIS workflow's own identity so a
# broken run can never publish a bad bundle.
# --------------------------------------------------------------------------
sign:
name: Sign release artifacts (Sigstore keyless)
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write # REQUIRED for ambient OIDC -> Fulcio
contents: write # REQUIRED to attach .sigstore bundles to the Release assets

steps:
- name: Download dist artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/

- name: Sign sdist + wheel and attach bundles to the Release
uses: sigstore/gh-action-sigstore-python@5b79a39c381910c090341a2c9b0bf022c8b387e1 # v3.4.0
with:
inputs: ./dist/*.tar.gz ./dist/*.whl
# Self-check: verify what we just signed against THIS workflow's own
# identity so a broken run never publishes a bad bundle.
verify: true
verify-cert-identity: "https://github.com/ernestprovo23/conclave/.github/workflows/release.yml@${{ github.ref }}"
verify-oidc-issuer: "https://token.actions.githubusercontent.com"
# release-signing-artifacts defaults to true: on a release event the
# .sigstore bundles are attached to the Release assets automatically.
release-signing-artifacts: true
upload-signing-artifacts: true
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,39 @@ jobs:

- name: ruff format --check
run: ruff format --check .

audit:
name: "pip-audit"
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-audit-${{ hashFiles('pyproject.toml') }}

# Install the project so its resolved runtime + dev deps are present in the
# environment, then audit that environment. conclave is security-positioned,
# so this gate is FAIL-CLOSED: a known vulnerability in any resolved
# dependency fails CI. The dep surface is tiny (httpx + a few well-maintained
# libs), so false-positive churn is low. If a transitive CVE with no fix
# blocks an unrelated PR, suppress it narrowly with
# `--ignore-vuln <GHSA/PYSEC id>` and a tracking note (see RELEASING.md).
- name: Install package with dev extras
run: pip install -e ".[dev]"

- name: Install pip-audit
run: pip install pip-audit

# `--skip-editable` drops the local editable `conclave` install (it is not on
# PyPI and cannot be audited); every real dependency is still audited.
- name: Audit resolved dependencies for known vulnerabilities
run: pip-audit --skip-editable
30 changes: 30 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# gitleaks configuration for conclave.
#
# conclave is a bring-your-own-keys tool whose security tests deliberately plant
# OBVIOUSLY-FAKE, key-SHAPED strings (e.g. "sk-FAKE...", "AIza-FAKE...") to prove
# that redact() / the cache / streaming never let a real key escape. Those fake
# fixtures are not secrets, but a key-shaped literal can trip gitleaks' generic
# rules. This allowlist scopes that exception to the test tree ONLY, so a real
# secret committed anywhere in the source or docs is still caught.
#
# We extend gitleaks' bundled default ruleset rather than replacing it, so every
# upstream detector stays active outside the allowlisted paths.

[extend]
useDefault = true

[allowlist]
description = "Fake, key-shaped fixtures used by the key-leak regression tests are not secrets."
# Restrict the allowance to the tests directory: production code, docs, and
# config are still fully scanned.
paths = [
'''tests/.*\.py''',
]
# Defense in depth: also allow the explicit fake-key marker tokens anywhere they
# appear, so a fixture moved/quoted in a doc example is not flagged. These are
# intentionally synthetic sentinels, never real credentials.
regexes = [
'''FAKE-?[A-Za-z0-9_\-]*''',
'''sk-(test|FAKE|streamleak|CONCLAVE)[A-Za-z0-9_\-]*''',
'''AIza-?(dummy|test|FAKE)[A-Za-z0-9_\-]*''',
]
81 changes: 81 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Changelog

All notable changes to conclave are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-06-14

First stable release. conclave is feature-complete for its 1.0 scope: a
bring-your-own-keys multi-model council that fans a prompt to N foundation
models concurrently and merges their answers. This release integrates three
release-readiness workstreams — distribution/release engineering, key-leak
hardening + threat model, and synthesizer behavior documentation/versioning —
on top of v0.3.0.

### Added

- **Distribution name.** The package is now published to PyPI as `conclave-cli`
(`pip install conclave-cli`); the import package, CLI command, and repo all
stay `conclave`. The bare PyPI name `conclave` is an unrelated project.
- **Release engineering.** OIDC Trusted-Publisher release workflow
(`.github/workflows/release.yml`) with Sigstore keyless signing and PEP 740
attestations, inert until a GitHub Release fires and the publisher is
configured; a hash-pinned dev + runtime lockfile (`requirements-dev.lock`)
for reproducible installs/CI; and a `RELEASING.md` operator runbook.
- **Supply-chain CI.** A fail-closed `pip-audit` job added to the CI workflow.
- **Threat model.** `SECURITY.md` now carries a BYO-keys threat model and the
key-handling guarantees consumers can rely on; `.gitleaks.toml` plus a
dedicated `tests/test_keyleak_audit.py` regression suite guard against
secret leakage.
- **Versioned synthesis prompt.** The synthesis prompt set is versioned via
`conclave.prompts.SYNTHESIS_PROMPT_VERSION` and stamped onto every
`CouncilResult.prompt_version`, so a downstream eval can detect a prompt
change rather than silently absorb it.

### Changed

- **Key-leak: cause-chain fix.** The originating `httpx` exception is no longer
attached to `TransportError.__cause__`, closing a path where a verbose
traceback could surface a key-bearing transport exception.
- **Key-leak: transport-logging guard default-on.** `Council.__init__` now
installs `conclave.transport.guard_transport_logging()` by default, dropping
the httpx/httpcore `DEBUG` records that emit the auth header. Callers who
genuinely need that DEBUG band opt out with
`Council(..., allow_transport_debug_logging=True)`.
- **Synthesizer: observable degradation.** Synthesizer/judge degradation is
confirmed (never silent) across synthesize, debate, and the adversarial-judge
paths: an unkeyed or failed synthesizer surfaces on
`CouncilResult.synthesis_error` (and `AdversarialResult.verdict_error`,
mirrored to `synthesis_error`), with no path where synthesis is both absent
and unexplained.
- **Synthesizer behavior documented.** README gains a "Synthesizer behavior"
section covering selection precedence (`synthesizer=` arg → config →
default), observable degradation, and the versioned prompt.

### Scope

- Feature-complete for 1.0: 4 council modes (synthesize / raw / debate /
adversarial), 9 providers, streaming for synthesize/raw, an optional result
cache, and debate convergence early-stop.

### Roadmap (post-1.0)

- `vote` mode (council issue #3) — a ranked/tallied decision mode — is
documented as planned, not shipped.
- A stdio MCP server (council issue #8) is documented as planned; the earlier
HTTP local-server-mode spike was evaluated and shelved.

## [0.3.0] - 2026-06-08

- Provider-highway refactor: LiteLLM removed in favor of an owned `httpx`
transport + adapter registry across the (then) 5 providers.
- CI foundation: GitHub Actions matrix, ruff lint/format, coverage floor,
gitleaks, and branch protection.
- Key-leak fix in `redact()` for custom OpenAI-compatible endpoints; CLI
exit-code contract and httpx client lifecycle hardening; transport/CLI/logging
test backfill; first public release with community files.

[1.0.0]: https://github.com/ernestprovo23/conclave/compare/v0.3.0...v1.0.0
[0.3.0]: https://github.com/ernestprovo23/conclave/releases/tag/v0.3.0
Loading
Loading