procurement-decision-api
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 noproposed_status,decision.statusstayspending. 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 shapedocument_hashes[]— informational RFC 8785 JCS hashes withhash_profile: jcs-rfc8785-v1, aligned withhash-attestation-rsv0.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
- Fetches up to 16 operator-allowlisted HTTPS
/.well-known/*.jsonURLs, 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. MissingFETCH_ALLOWED_HOSTSrejects requests with fetch targets. - Suggests a status from the buyer-supplied rubric if you didn't supply
proposed_status; the draft itself always remainspending. The suggestion rules:- Any
fail→rejected-with-remediation - Any
partialorpass-with-condition→approved-with-conditions - All
pass→approved - Empty / all
n/a→pending
- Any
- 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. - Validates the Decision Card against the same conditional rules the upstream zod schema enforces:
status∈ {approved-with-conditions,rejected-with-remediation} →conditionsmust be non-emptystatus=withdrawn→withdrawalblock requiredpublication.is_public=true→publication_urirequired
- 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=trueis 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_TOKENis 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 to127.0.0.1by 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
429andRetry-Afteron excess. Use the gateway for distributed rate limits and unauthenticated floods. FETCH_ALLOWED_HOSTSis a comma-separated list of exact DNS names controlled by the operator. Only HTTPS port 443, unencoded/.well-known/*.jsonpaths 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_URLis 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 requestCache-Control: no-store; configure the gateway and clients to honor it./docsand/openapi.jsonremain 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-actionis 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.
Related
- Spec repo:
ai-procurement-decision-spec - Hosted validator: validator.kineticgain.com
- MCP server:
mcp-kinetic-gain— install withnpx -y mcp-kinetic-gain - GitHub Action:
kg-validate-action - NIST AI RMF crosswalk: suite.kineticgain.com/docs/nist-rmf-crosswalk.md
- Apex: kineticgain.com
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)
| File | Size | Uploaded | |
|---|---|---|---|
| procurement_decision_api-0.2.0.tar.gz | 29.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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