Skip to main content

procurement-decision-api

CI License: MIT Python Framework: FastAPI

A local-first drafting aid for buyer-side AI procurement records. A buyer must review, authorize, and sign any decision before publication.

A FastAPI service that takes a buyer's own rubric judgments and fetches selected vendor Kinetic Gain Protocol Suite declarations, then returns a draft AI Procurement Decision Card. Package v0.2.0 implements the v0.1 card model; the upstream specification also has v0.2 and v0.3 fields that this service does not accept. The NIST AI RMF crosswalk is informational, not a compliance determination or publication requirement.

The cross-ecosystem bridge

This service connects vendor Suite declarations to a buyer-authored Decision Card:

Vendor publishes:                Buyer drafts (this service produces):
─────────────────────────        ────────────────────────────────────────
AEO Protocol Card           ┐
Tool Disclosure             │
Clinical AI Card            ├──> AI Procurement Decision Card
Student AI Disclosure       │       (status / rubric / conditions /
Agent Card                  │        fetched documents / rationale)
…the other six specs…       ┘

Quick start

From this checkout, use the v0.2.0 code. The published v0.1.1 package has the earlier API behavior.

python -m pip install -e .
export API_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
export FETCH_ALLOWED_HOSTS="acmetutor.example"  # replace with an exact trusted vendor DNS name
procurement-decision-api  # listens on http://127.0.0.1:8088

PowerShell setup from this checkout:

py -3.11 -m pip install -e .
$env:API_TOKEN = [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
$env:FETCH_ALLOWED_HOSTS = "acmetutor.example"
py -3.11 -m procurement_decision_api

Or via Docker:

docker build -t procurement-decision-api:local .
docker run --rm -p 127.0.0.1:8088:8088 -e API_TOKEN -e FETCH_ALLOWED_HOSTS procurement-decision-api:local

The example below uses fictional people, organizations, and URLs. Replace the fetch host with a trusted real vendor host before use. A fetched declaration is only a reference, not evidence that its claims are true. Draft and validation endpoints require Authorization: Bearer <API_TOKEN>.

Then draft a record:

curl -s http://localhost:8088/decisions/draft \
  -H "Authorization: Bearer $API_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "decision_id": "SPRINGFIELD-DEC-2026-001",
    "buyer": {
      "name": "Springfield Unified School District",
      "type": "school-district",
      "jurisdiction": "US-CA"
    },
    "decision_maker": {
      "role": "Director of Educational Technology",
      "name": "Dr. Jane Doe",
      "authority": "Board Resolution 2026-04"
    },
    "vendor_name": "AcmeTutor Inc.",
    "product_name": "AcmeTutor 3.0",
    "vendor_id": "https://acmetutor.example/.well-known/aeo.json",
    "fetch_targets": [
      { "type": "aeo",                    "url": "https://acmetutor.example/.well-known/aeo.json" },
      { "type": "tutor-card",             "url": "https://acmetutor.example/.well-known/tutor-card.json" },
      { "type": "student-ai-disclosure",  "url": "https://acmetutor.example/.well-known/student-ai-disclosure.json" }
    ],
    "policy_uris": [
      "https://springfield.edu/.well-known/aup.json"
    ],
    "rubric": [
      { "id": "ferpa-compliance",         "result": "pass", "weight": 1.0 },
      { "id": "coppa-compliance",         "result": "pass", "weight": 1.0 },
      { "id": "no-training-on-student-data", "result": "pass-with-condition", "weight": 1.0,
        "notes": "Disclosure asserts no-training; require contractual confirmation." },
      { "id": "bias-audit-completed",     "result": "partial", "weight": 0.8,
        "notes": "Audit current but due for refresh by 2026-09." }
    ],
    "conditions": [
      { "id": "no-training-restriction",
        "description": "Vendor SHALL NOT use Springfield USD student-provided content for model training.",
        "enforcement": "contractual" },
      { "id": "bias-audit-refresh",
        "description": "Vendor SHALL deliver a refreshed third-party bias audit by 2026-12-01.",
        "enforcement": "audit" }
    ]
  }' | jq

The response includes:

  • draft — a v0.1 Decision Card draft. With no proposed_status, decision.status stays pending. The service does not sign or publish it.
  • documents_fetched[] — each vendor URL with its retrieval timestamp and legacy Python sha256 content hash in the v0.1 card shape
  • document_hashes[] — informational RFC 8785 JCS hashes with hash_profile: jcs-rfc8785-v1, aligned with hash-attestation-rs v0.2. This response metadata is outside the unsigned card.
  • fetch_errors[] — per-target retrieval errors (the draft doesn't fail wholesale on one missing URL)
  • suggested_status — an advisory caller proposal, or a status inferred from the buyer-supplied rubric when no proposal is supplied; it does not authorize approval

v0.2.0 status and hash migration

In v0.2.0, the API always emits draft.decision.status=pending, even when proposed_status is approved, rejected, or another nonpending value. That value appears only in suggested_status. Callers of v0.1.1 that treated the generated card as an approved or rejected decision must add a separate buyer review and signing workflow. The validation endpoint now returns structure_valid and claimed_status instead of valid and status; it omits buyer/vendor identity and reports authority_verified:false and signatures_verified:false. Fetched URLs and their legacy Python content hashes remain in documents_fetched[]; draft.subject.documents_reviewed stays empty because fetching is not a buyer review. A buyer review workflow must populate that field before claiming review. Use the profiled document_hashes[] only as informational comparison data. The upstream v0.1 card schema has no hash-profile field, and this service does not verify vendor signatures or sign its response.

What the service does

  1. Fetches up to 16 operator-allowlisted HTTPS /.well-known/*.json URLs, without redirects, encoded paths, or query strings, concurrently with httpx. It rejects hosts whose DNS answers contain private or other non-public IPs at preflight time. Each response is streamed with a 2 MB cap and 10 s total timeout. Missing FETCH_ALLOWED_HOSTS rejects requests with fetch targets.
  2. Suggests a status from the buyer-supplied rubric if you didn't supply proposed_status; the draft itself always remains pending. The suggestion rules:
    • Any fail → rejected-with-remediation
    • Any partial or pass-with-condition → approved-with-conditions
    • All pass → approved
    • Empty / all n/a → pending
  3. Composes a default rationale from the buyer-supplied rubric results if you didn't supply rationale_template. It does not evaluate claims in fetched documents.
  4. Validates the Decision Card against the same conditional rules the upstream zod schema enforces:
    • status ∈ {approved-with-conditions, rejected-with-remediation} → conditions must be non-empty
    • status = withdrawn → withdrawal block required
    • publication.is_public = true → publication_uri required
  5. Returns the draft for human review. It does not create review-completed history events, sign, or publish. A draft request that claims publication.is_public=true is rejected.

Endpoints

Method Path Purpose
GET / Service info + relevant links
GET /healthz Liveness probe (always 200 if the process is running)
POST /decisions/draft Produce a Draft Decision Card
POST /decisions/validate Check a card's v0.1 structure; does not verify authority or signatures
GET /docs Interactive OpenAPI documentation (Swagger UI)
GET /openapi.json Machine-readable API schema

Why this matters

The service offers buyers a structured way to record their own AI procurement review and cite the vendor declarations they considered. Whether a buyer may or must publish a decision depends on its governing law, policy, contracts, and review process. NIST describes the AI RMF as voluntary. OMB M-25-21 and M-25-22 address covered federal executive agencies; they do not turn the fictional school-district example into a federal publication obligation.

The AI Procurement Decision Card spec defines a machine-readable format for buyer decisions. Here, a reviewer enters the rubric judgments and points the service at vendor declarations. The result still needs buyer review, a decision-authority check, and any required privacy or legal review before signing or publication.

The draft cites fetched declarations by URL, retrieval time, and a legacy content hash. Hashes can help detect later content changes only when producer and verifier use the same canonicalization contract. The response also exposes versioned RFC 8785 hashes that match the Rust attestation tool's jcs-rfc8785-v1 profile on a shared Unicode, exponent, negative-zero, and key-order vector. The card's legacy hashes are still not interoperable with Rust for all JSON values. Neither hash proves who published a document; verify a trusted vendor signature and bind the evidence to the buyer's authorized review before relying on it.

Architecture

┌────────────────────────────────────────────────────────────┐
│                  FastAPI app (lifespan-managed)            │
│                                                            │
│   POST /decisions/draft                                    │
│       │                                                    │
│       ▼                                                    │
│   ┌────────────────────────────────────────────────┐       │
│   │ fetcher.fetch_documents (async, httpx)         │       │
│   │   - timeout 10s per doc                        │       │
│   │   - 2 MB size cap                              │       │
│   │   - legacy + profiled JCS sha256 hashes       │       │
│   │   - per-target error collection                │       │
│   └────────────────────────────────────────────────┘       │
│       │                                                    │
│       ▼                                                    │
│   ┌────────────────────────────────────────────────┐       │
│   │ rubric.infer_status                            │       │
│   │ rubric.compose_rationale                       │       │
│   │ rubric.weighted_score                          │       │
│   └────────────────────────────────────────────────┘       │
│       │                                                    │
│       ▼                                                    │
│   ┌────────────────────────────────────────────────┐       │
│   │ drafter.draft_decision_card                    │       │
│   │   - validates conditional rules                │       │
│   │   - assembles history events                   │       │
│   └────────────────────────────────────────────────┘       │
│       │                                                    │
│       ▼                                                    │
│   DraftResponse                                            │
└────────────────────────────────────────────────────────────┘

Pydantic v2 models implement the v0.1 fields and three conditional rules. They are not a substitute for validation against the current upstream JSON Schema. The /decisions/validate endpoint accepts v0.1 cards only.

Operating boundary

  • API_TOKEN is required for both POST endpoints and must contain at least 32 characters. Keep it in a secret store, rotate it through your deployment process, and send it only over TLS outside localhost. The token is a shared service-use credential with no tenant, role, audience, or buyer-authority claim. The CLI binds to 127.0.0.1 by default; the container listens inside its network namespace, so publish its port to localhost or place it behind a private gateway with caller-specific authorization.
  • POST request bodies are capped at 512 KiB and must arrive within 10 s. Authenticated decision operations are limited to 60 requests per rolling minute per process, with 429 and Retry-After on excess. Use the gateway for distributed rate limits and unauthenticated floods.
  • FETCH_ALLOWED_HOSTS is a comma-separated list of exact DNS names controlled by the operator. Only HTTPS port 443, unencoded /.well-known/*.json paths without credentials, queries, fragments, or redirects are fetched. An initial DNS lookup rejects non-public IP answers and HTTP clients ignore proxy environment variables. DNS can change before the connection, so production egress rules must block private, loopback, link-local, and metadata destinations at the network boundary.
  • Fetched JSON is treated as untrusted data. The parser requires an object, rejects duplicate keys, and rejects values outside the RFC 8785 canonicalization domain. The service does not validate a vendor declaration against its own Suite schema, verify a signature, or confirm the vendor's claims. Per-target failures are returned; the buyer must decide whether missing evidence changes the outcome.
  • AUDIT_STREAM_URL is optional. When enabled, the emitted event contains status and counts only; it omits buyer/vendor names and decision IDs. This service has no persistent draft store, but clients, proxies, and the audit destination need access, retention, and deletion policies. Decision responses request Cache-Control: no-store; configure the gateway and clients to honor it. /docs and /openapi.json remain public metadata endpoints.
  • The Docker base is pinned by digest and its runtime dependencies are resolved from uv.lock. The recipe still needs a successful Linux image build and exact image-digest check in CI. No tenant authorization, verified buyer reviewer, signing workflow, or network-level egress rule is provided here. Keep the service private until those deployment controls are verified.

Development

git clone https://github.com/mizcausevic-dev/procurement-decision-api
cd procurement-decision-api
pip install -e ".[dev]"

# Run the test suite (mocks the vendor HTTP layer; no internet required)
pytest -q

# Lint, format, typecheck
ruff check src tests
ruff format src tests
mypy src

# Run the service
python -m procurement_decision_api
# or
uvicorn procurement_decision_api.app:app --reload --port 8088

Composability

This service composes naturally with the rest of the Kinetic Gain ecosystem:

  • Input documents can be fetched from operator-allowlisted vendor /.well-known/ paths. Validate each declaration against its own specification before relying on it; kg-validate-action is one available tool.
  • Output Decision Cards can be inspected by mcp-kinetic-gain (tools: decision_card_inspect, decision_card_validate).
  • Inline validation in the browser is available at validator.kineticgain.com — paste the produced draft, get inline error markers.

License

MIT. The Kinetic Gain Protocol Suite specifications this service produces are also MIT; reference implementations like mcp-kinetic-gain are AGPL-3.0.

Metadata

Release files for procurement-decision-api 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for procurement-decision-api 0.2.0
File Size Uploaded
procurement_decision_api-0.2.0.tar.gz 29.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for procurement-decision-api 0.2.0
File Interpreter ABI Platform
procurement_decision_api-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.8 kB

Release files / procurement_decision_api-0.2.0.tar.gz

Download URL procurement_decision_api-0.2.0.tar.gz
Size 29.1 kB
Tags Source
SHA-256 checksum
How to use checksums
0dceae74e07ad6ec2e5e1b0da8df6cd4ee18418fd18404073701aa19e5880b20
BLAKE2b-256 checksum
How to use checksums
5b1afbfd6638603d5dec8fa033250518d0f56514d94e08ce2d10b7e6b3c68efe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / procurement_decision_api-0.2.0-py3-none-any.whl

Download URL procurement_decision_api-0.2.0-py3-none-any.whl
Size 25.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8f9183f92a9f373859522523394c764e6cf9a2890648fa113090b2ea4bbdbfa
BLAKE2b-256 checksum
How to use checksums
3fc51178448317d355ecc49722c227d60a6d70eaaedb0fa3afcb570e333affbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page