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
71 changes: 71 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
build-test:
name: Build & test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: uv sync
- name: Smoke-check the adapter manifest
run: uv run python main.py --emit-manifest

# WS49 — vulnerability gate (osv-scanner). BLOCKING: scans uv.lock; tree clean.
# uv.lock is not auto-detected by the directory walk, so target it explicitly.
# Pinned google/osv-scanner-action by SHA (v2.3.8).
osv-scan:
name: Vulnerability scan (osv-scanner)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run osv-scanner
uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8
with:
scan-args: |-
scan
source
--config=osv-scanner.toml
--lockfile=uv.lock

# WS50 — non-blocking dependency-freshness report.
deps-report:
name: Dependency freshness (report-only)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Report outdated dependencies
run: |
uv sync || true
{
echo '### Outdated dependencies (`uv pip list --outdated`)'
echo '```'
uv pip list --outdated || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

all-checks:
name: All checks passed
runs-on: ubuntu-latest
needs: [build-test, osv-scan]
if: always()
steps:
- name: Verify required jobs succeeded
run: |
if [ "${{ needs.build-test.result }}" != "success" ]; then
echo "build-test did not succeed"; exit 1
fi
if [ "${{ needs.osv-scan.result }}" != "success" ]; then
echo "osv-scan did not succeed"; exit 1
fi
67 changes: 50 additions & 17 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: Publish Adapter

# On every tag push (v*), build the single-binary adapter and publish it as a
# signed OCI artifact via the reusable brokenbots/publish-adapter action. The
# action signs keyless by default (needs id-token: write, granted below). To
# also ship a runnable container image, build + push it and pass its reference
# via the action's `image:` input (see the commented Dockerfile).
# Nuitka compiles a native single-file binary and CANNOT cross-compile, so each
# platform is built on its own native runner (build matrix). A final job collects
# the per-platform binaries into bin/<os>/<arch>/<name> and publishes them as a
# single multi-platform, signed OCI artifact via brokenbots/publish-adapter.

on:
push:
Expand All @@ -22,20 +21,55 @@ permissions:
packages: write
id-token: write # cosign keyless signing

env:
# Published binary name; defaults to the repo name. Rename when you fork.
BIN: ${{ github.event.repository.name }}

jobs:
publish:
runs-on: ubuntu-latest
build:
name: Build ${{ matrix.os }}/${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- { os: linux, arch: amd64, runner: ubuntu-latest }
- { os: linux, arch: arm64, runner: ubuntu-24.04-arm }
- { os: darwin, arch: arm64, runner: macos-14 }
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Build adapter binary
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: uv sync
- name: Build native binary (Nuitka)
run: make build
- name: Stage binary into bin/<os>/<arch>/
run: |
uv sync
make build
mkdir -p "artifact/bin/${{ matrix.os }}/${{ matrix.arch }}"
cp out/adapter "artifact/bin/${{ matrix.os }}/${{ matrix.arch }}/${BIN}"
- uses: actions/upload-artifact@v4
with:
name: bin-${{ matrix.os }}-${{ matrix.arch }}
path: artifact/bin/${{ matrix.os }}/${{ matrix.arch }}/${{ env.BIN }}
if-no-files-found: error

publish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download per-platform binaries
uses: actions/download-artifact@v4
with:
path: _artifacts
- name: Assemble multi-platform bin/ tree
run: |
set -euo pipefail
for triple in linux-amd64 linux-arm64 darwin-arm64; do
os="${triple%-*}"; arch="${triple#*-}"
mkdir -p "artifact/bin/${os}/${arch}"
cp "_artifacts/bin-${triple}/${BIN}" "artifact/bin/${os}/${arch}/${BIN}"
chmod +x "artifact/bin/${os}/${arch}/${BIN}"
done
- name: Resolve version
id: ver
run: |
Expand All @@ -44,12 +78,11 @@ jobs:
else
echo "tag=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
fi

- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

- name: Publish
uses: brokenbots/publish-adapter@v0
with:
binary: ${{ github.workspace }}/out/adapter
# Directory containing the multi-platform bin/<os>/<arch>/ tree.
binary: ${{ github.workspace }}/artifact
registry: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.ver.outputs.tag }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ out/
*.dist/
*.log
.DS_Store
artifact/
*.build
*.dist
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ SIGN_KEY ?=
publish: build
$(CRITERIA) adapter publish out/adapter --registry "$(REGISTRY)" \
$(if $(SIGN_KEY),--sign-key "$(SIGN_KEY)",)

# --- Security (WS49) ----------------------------------------------------------
.PHONY: vuln-scan
vuln-scan: ## Scan for known vulnerabilities (osv-scanner; reads uv.lock). Requires osv-scanner on PATH.
osv-scanner scan source --config=osv-scanner.toml --lockfile=uv.lock
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,20 @@ for the full `serve_remote` API.
## License

MIT

## Security & dependencies

This starter ships the supply-chain baseline every Criteria adapter is expected
to carry — see [SECURITY.md](SECURITY.md) and
[docs/dependency-policy.md](docs/dependency-policy.md). CI (`ci.yml`) runs a
**blocking** osv-scanner gate (over `uv.lock`) plus a non-blocking freshness
report; `publish.yml` builds each platform on its own native runner (Nuitka can't
cross-compile) and publishes one multi-platform signed OCI artifact.

```bash
make vuln-scan # osv-scanner — known-vulnerability gate (WS49)
uv pip list --outdated # freshness report (WS50)
```

> The SDK is installed from its GitHub repo via `[tool.uv.sources]` (it is not on
> PyPI yet). Switch to a PyPI constraint once the SDK is published.
27 changes: 27 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Security

## Reporting a vulnerability

Please report security issues privately via GitHub's **"Report a vulnerability"**
flow (Security → Advisories) on this repository, or email security@brokenbots.net.
Do not open a public issue for an undisclosed vulnerability.

## Supply-chain controls

Adapters built from this starter ship as a **signed, multi-platform OCI artifact**
(`linux/amd64`, `linux/arm64`, `darwin/arm64`), keyless-signed via Sigstore and
published by [`brokenbots/publish-adapter`](https://github.com/brokenbots/publish-adapter).
Because Nuitka cannot cross-compile, each platform is built on its own native
runner (see `.github/workflows/publish.yml`).

Dependency hygiene is enforced in CI and documented in
[docs/dependency-policy.md](docs/dependency-policy.md):

- **`osv-scan`** — osv-scanner (pinned) runs on every PR/push as a **blocking**
gate (scans `uv.lock`); no shipping known vulnerabilities. Exceptions are
documented + dated in [`osv-scanner.toml`](osv-scanner.toml).
- **`deps-report`** — non-blocking freshness report (`uv pip list --outdated`).
- **7-day cooldown** on new releases (security fixes exempt); no automated update
bot (small dependency surface — see the policy).

Reproduce the gate locally with `make vuln-scan` (requires osv-scanner on PATH).
56 changes: 56 additions & 0 deletions docs/dependency-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Dependency-freshness & supply-chain policy

This adapter follows the same two locked mandates as the
[Criteria monorepo](https://github.com/brokenbots/criteria/blob/main/docs/dependency-policy.md).
Each repo owns its own copy; this file is the local authority. It applies to the
Python dependencies (`pyproject.toml` / `uv.lock`) and the GitHub Actions used in
CI.

## 1. Stay current — latest major.minor

Be on the **latest major and minor** of every dependency. Patch versions roll up
freely *within* the cooldown rule below. The only reason to pin **below** latest
is a concrete one: a newer version has a **known security vulnerability** that
affects us, or a **bug we are actually hit by**. Any such pin is a documented,
dated exception (see below).

## 2. Defend against supply-chain attacks — 7-day cooldown

Do **not** adopt any release **newer than 7 days** unless it fixes a known
security issue or a specific bug we're hit by. **Security updates bypass the
cooldown.**

## How freshness & vulnerabilities are tracked — no update bot

This repo runs **no automated dependency-update bot**. The dependency surface is
small, so freshness is managed by review against the tooling below:

| Command | Tool | Answers |
| --- | --- | --- |
| `make vuln-scan` | [`osv-scanner`](https://github.com/google/osv-scanner) | Which deps carry a known advisory (reads `uv.lock`). **CI gate (WS49).** |
| `uv pip list --outdated` | `uv` | Which deps are behind their latest version. |

- **`osv-scan`** runs in CI on every PR/push (pinned `google/osv-scanner-action`)
and is a **required, blocking** check.
- **`deps-report`** runs `uv pip list --outdated` non-blocking and posts the
result to the job summary.

Applying upgrades (honor the 7-day cooldown unless it's a security/bug fix):

```bash
uv lock --upgrade-package <pkg> # bump one dependency
uv lock --upgrade # refresh all within constraints
```

After any upgrade: `uv sync`, `make test`, `make vuln-scan`.

## Holding a dependency below latest

Record any pin below latest as a dated exception (mirrors the `osv-scanner.toml`
"documented + dated" convention) and constrain it in `pyproject.toml`.

| Dependency | Held at | Reason (advisory / bug) | Review by |
| --- | --- | --- | --- |
| _none_ | | | |

On the review date the exception must be cleared or re-justified.
11 changes: 7 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
examples/remote/ and the SDK README.
"""

import asyncio
import sys

from criteria_adapter_sdk import serve


async def main() -> None:
await serve(
def main() -> int:
# serve() is synchronous: it runs the go-plugin handshake + RPC loop (driving
# the async handlers internally) and returns a process exit code. It also
# handles --emit-manifest for the publish pipeline.
return serve(
{
"name": "my-adapter",
"version": "0.1.0",
Expand All @@ -33,4 +36,4 @@ async def _run(req, sender) -> None:


if __name__ == "__main__":
asyncio.run(main())
sys.exit(main())
22 changes: 22 additions & 0 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# osv-scanner configuration (WS49 — vulnerability gate)
#
# Mandate: no shipping code with known security vulnerabilities. The CI `osv-scan`
# job (.github/workflows/ci.yml) and `bun run vuln-scan` run osv-scanner over
# this package's bun.lock using this config.
#
# Default posture: NO ignores. A clean tree carries an empty file.
#
# An ignore is an explicit, auditable decision — never a silent skip. Each entry
# MUST carry:
# - id the OSV / GHSA / advisory id (e.g. "GHSA-xxxx-xxxx-xxxx")
# - reason why it is safe to defer (unreachable code path, false positive,
# upstream fix unavailable, etc.)
# - ignoreUntil a future review-expiry date (RFC3339). On expiry the finding
# re-surfaces and must be re-justified or cleared.
#
# Example (keep commented unless an exception is actually in force):
#
# [[IgnoredVulns]]
# id = "GHSA-xxxx-xxxx-xxxx"
# reason = "Vulnerable function not reachable from our code paths."
# ignoreUntil = "2026-09-01T00:00:00Z"
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ version = "0.1.0"
description = "A starter Criteria adapter in Python"
requires-python = ">=3.12"
dependencies = [
"criteria-adapter-sdk>=0.1.0",
"criteria-python-adapter-sdk>=0.5.0",
]

# The SDK is not published to PyPI; install it from its GitHub repo. Replace with
# a PyPI version constraint once the SDK is published.
[tool.uv.sources]
criteria-python-adapter-sdk = { git = "https://github.com/brokenbots/criteria-python-adapter-sdk.git" }

[dependency-groups]
dev = [
"nuitka>=4.0.8",
Expand Down
Loading
Loading