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
10 changes: 8 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
* @veil-chow-fyaic

# Code ownership is intentionally scoped to security-sensitive implementation and
# governed source/decision material. Dependency manifests are reviewed through CI
# and the maintainer queue without automatically requesting a personal review.
/src/ @veil-chow-fyaic
/migrations/ @veil-chow-fyaic
/api/ @veil-chow-fyaic
/.github/workflows/ @veil-chow-fyaic
/.github/CODEOWNERS @veil-chow-fyaic
/docs/requirements/ @veil-chow-fyaic
/docs/decisions/ @veil-chow-fyaic
/research/ @veil-chow-fyaic
20 changes: 12 additions & 8 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,36 @@ updates:
- package-ecosystem: pip
directory: /
schedule:
interval: monthly
interval: quarterly
groups:
python-runtime:
dependency-type: production
python-development:
dependency-type: development
labels: ["dependencies", "python"]
open-pull-requests-limit: 5
open-pull-requests-limit: 2

- package-ecosystem: npm
directory: /
schedule:
interval: monthly
interval: quarterly
labels: ["dependencies", "documentation"]
open-pull-requests-limit: 2
open-pull-requests-limit: 1

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
interval: quarterly
labels: ["dependencies", "ci"]
open-pull-requests-limit: 3
open-pull-requests-limit: 1

- package-ecosystem: docker
directory: /
schedule:
interval: monthly
interval: quarterly
labels: ["dependencies", "containers"]
open-pull-requests-limit: 3
# Base Python and uv image bumps must be coordinated with workflow, lock,
# documentation and acceptance pins; automated version PRs cannot do that.
# A zero limit disables Docker version-update PRs without suppressing
# Dependabot security-update PRs.
open-pull-requests-limit: 0
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install pinned uv and Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.1"
version: "0.12.8"
python-version: "3.13.14"
enable-cache: true
- name: Verify lock and install dependencies
Expand Down Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Install pinned uv and Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.1"
version: "0.12.8"
python-version: "3.13.14"
enable-cache: true
- name: Install locked dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install pinned uv and Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.1"
version: "0.12.8"
python-version: "3.13.14"
enable-cache: true

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:0.12.1@sha256:cf4eedcaa81655197f625739489effcbe71b61ceb1506f332c3facae5deceded AS uv-bin
FROM ghcr.io/astral-sh/uv:0.12.8@sha256:d1cbaeadc234fe19c0d93daabcf5e98738cd93c6d1dd4918ef6aa30735feb23a AS uv-bin
FROM mirror.gcr.io/library/python:3.13.14-slim-bookworm@sha256:67a1e1f215ccda113cfc024e8639049257e88f273898f595b61476d128d387e8

COPY --from=uv-bin /uv /uvx /usr/local/bin/
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ TradeSieve documentation is decision-oriented: requirements preserve what is nee

- [External services and outbound dependencies](operations/external-dependencies.md)
- [REST/CLI/MCP interface parity evidence](evidence/interface-parity-2026-08-12.json)
- [Dependency and MCP 2.1 smoke test](evidence/dependency-and-mcp-smoke-2026-09-02.md)
- [MCP stdio smoke test](evidence/tradesieve-mcp-smoke-2026-08-11.md)
- [Codex Agent MCP demonstration](evidence/codex-agent-mcp-demo-2026-08-11.md)

Expand Down
62 changes: 62 additions & 0 deletions docs/evidence/dependency-and-mcp-smoke-2026-09-02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Dependency and MCP smoke test — 2026-09-02

**Decision:** GO for the dependency-maintenance change. This is engineering
compatibility evidence, not a production-readiness or legal-effectiveness claim.

## Scope and environment

This smoke verifies the coordinated September dependency update, especially the
`mcp` 2.1.1 error boundary, through the packaged local stdio server and the same
persisted four-source application service used by CLI, authenticated REST and the
synthetic CRM.

- CPython 3.13.14 and `uv` 0.12.8;
- PostgreSQL 18.4 reference container;
- `mcp` 2.1.1 / negotiated protocol `2025-11-25`;
- Docker 29.6.2 on the local acceptance host;
- public synthetic request `examples/requests/official-screening.json` only.

## Test plan and result

| Check | Expected result | Side effect / cleanup | Result |
| --- | --- | --- | --- |
| Repository gate | Lock, format, lint, typing, contracts, unit/architecture, build, docs, OpenAPI, secret and vulnerability checks pass | Build artifacts only | PASS |
| Reference Compose | Fresh migration, bootstrap, runtime/failure controls and teardown pass | Isolated containers, networks and volumes removed | PASS |
| MCP handshake | Initialize over stdio and negotiate the supported protocol | Server subprocess exits | PASS |
| Tool inventory | Exactly `screen_transaction`, marked read-only, non-destructive, idempotent and closed-world | None | PASS |
| Negative boundary | Invalid input is rejected; no clearance/mutating tool exists; unavailable or stale source returns a bounded HOLD instruction without an internal exception | None | PASS |
| Live happy path | Fresh official bundle returns `RED` / `HOLD`, `automatic_clearance=false` and the same bundle identity through CLI, REST, CRM and MCP | Isolated official-source database removed | PASS |

## Evidence summary

- 2,088 repository tests passed with 100% statement and branch coverage.
- Package build, documentation checks, OpenAPI lint and dependency vulnerability
audit passed; the audit reported no known vulnerabilities.
- The full Compose reference deployment passed on isolated ports and left zero
project residue.
- The live gate applied one official-source refresh and accepted its replay as
idempotent. It parsed 6,234 EU FSF entities, 384 EU Annex I entries, 19,321 OFAC
SDN entries and 481 OFAC Consolidated entries.
- CLI and authenticated REST returned the same canonical result. The synthetic CRM
and MCP stdio server used the same active four-source bundle.
- The public SOVCOMFLOT candidate remained `RED` / `HOLD`; MCP never returned
automatic clearance.
- A deliberately stale pre-existing snapshot was rejected by the upgraded MCP
adapter with the bounded instruction to keep the business action on hold.
- The live gate removed its temporary evidence directory, containers, networks and
volumes after the assertions completed.

## Compatibility finding

MCP SDK 2.1 redacts unexpected tool exceptions as a generic execution failure. The
adapter now raises the SDK's anticipated `ToolError` for known source/database
failures. This preserves the sanitized, fail-closed HOLD guidance without exposing
the underlying exception. Unit tests cover this boundary and the direct stdio live
gate covers the packaged behavior.

## Boundary

The result demonstrates dependency compatibility and the existing prototype
contract on the tested source bundle. It does not add remote MCP/OAuth, case
mutation, human clearance, ownership/control propagation, full Russia-specific
goods/route rules, comprehensive legal coverage or production approval.
2 changes: 1 addition & 1 deletion docs/getting-started/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Prerequisites

- Git;
- `uv` 0.12.1;
- `uv` 0.12.8;
- Node.js/npm for the pinned Redocly contract lint command.

The repository pins CPython 3.13.14 in `.python-version`. `uv` downloads that interpreter when it is not already available.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/docker-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ This endpoint is a bounded technical preview. It does not persist the request/re

`docker compose up` starts PostgreSQL, runs the real Alembic migrations, loads the governed synthetic registration, creates and verifies two immutable source snapshots through retrieve/parse/validate/approve/activate, and uses authorized application services to activate one immutable synthetic rule bundle. It then starts the non-root read-only app and worker containers. The app, worker, and PostgreSQL services each have a healthcheck. No `runtime_coverage` source/rule marker is written or trusted.

The application image uses the Docker Official Image for Python 3.13.14 through Google's Docker Hub pull-through cache and copies the separately pinned `uv` 0.12.1 binary. The acceptance script verifies both versions, UID `10001`, and read-only `/app` behavior inside the final image.
The application image uses the Docker Official Image for Python 3.13.14 through Google's Docker Hub pull-through cache and copies the separately pinned `uv` 0.12.8 binary. The acceptance script verifies both versions, UID `10001`, and read-only `/app` behavior inside the final image.

The reference database is the Docker Official Image for PostgreSQL 18.4, accessed through Google's Docker Hub pull-through cache and pinned to the same official multi-platform digest. The acceptance script verifies the server and `psql` minor versions. PostgreSQL 18 stores its versioned data below `/var/lib/postgresql`; the named volume mounts that parent path so data survives container recreation.

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/official-screening-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ With network access to the official publications and Docker available:
./scripts/test_official_screening_live.sh
```

The gate uses an isolated project and temporary evidence directory. It runs migration 0007, performs an applied refresh plus an idempotent replay, screens the committed public candidate through CLI and authenticated REST, invokes the fixed CRM official-source route, and proves all three interfaces bind the same four-source bundle. Output is reduced to counts and booleans; containers, networks, volumes and temporary evidence are removed.
The gate uses an isolated project and temporary evidence directory. It runs migration 0007, performs an applied refresh plus an idempotent replay, screens the committed public candidate through CLI and authenticated REST, invokes the fixed CRM official-source route and directly drives the packaged MCP server over stdio. It proves all four interfaces bind the same four-source bundle. Output is reduced to counts and booleans; containers, networks, volumes and temporary evidence are removed.

## Output and decisions

Expand Down
20 changes: 10 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling==1.31.0"]
requires = ["hatchling==1.32.0"]
build-backend = "hatchling.build"

[project]
Expand All @@ -19,14 +19,14 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"alembic==1.19.0",
"alembic==1.19.1",
"certifi==2026.7.22",
"fastapi==0.141.1",
"mcp==2.0.0",
"mcp==2.1.1",
"psycopg[binary]==3.3.4",
"pydantic-settings==2.14.2",
"sqlalchemy==2.0.51",
"uvicorn==0.52.1",
"pydantic-settings==2.15.0",
"sqlalchemy==2.0.52",
"uvicorn==0.52.4",
]

[project.scripts]
Expand All @@ -36,17 +36,17 @@ tradesieve-mcp = "tradesieve.mcp_server:main"
[dependency-groups]
dev = [
"detect-secrets==1.5.0",
"hatchling==1.31.0",
"hatchling==1.32.0",
"httpx==0.28.1",
"mypy==2.3.0",
"mypy==2.3.1",
"pip-audit==2.10.1",
"pytest==9.1.1",
"pytest-cov==7.1.0",
"ruff==0.16.1",
"ruff==0.16.5",
]

[tool.uv]
required-version = "==0.12.1"
required-version = "==0.12.8"

[tool.hatch.build.targets.wheel]
packages = ["src/tradesieve"]
Expand Down
2 changes: 1 addition & 1 deletion research/sources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ sources:
jurisdiction: Global
url: https://github.com/Redocly/redocly-cli
use: Pinned Phase 1 OpenAPI linting in repository CI
access: MIT code; CI pins npm package version 2.44.2
access: MIT code; repository tooling pins npm package version 2.49.0

- id: json-schema
title: JSON Schema specification
Expand Down
3 changes: 2 additions & 1 deletion scripts/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ uv run --locked python scripts/generate_contract.py --check
uv run --locked pytest
uv build
./scripts/check_docs.sh
npx --yes @redocly/cli@2.44.2 lint api/openapi/tradesieve.v1.json
npm ci --ignore-scripts --prefer-offline --no-audit
npx --no-install redocly lint api/openapi/tradesieve.v1.json
git ls-files --cached --others --exclude-standard -z \
| xargs -0 uv run --locked detect-secrets-hook --baseline .secrets.baseline
uv export --locked --all-groups --no-emit-project --format requirements-txt \
Expand Down
22 changes: 19 additions & 3 deletions scripts/official_screening_live_assert.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Validate bounded live four-source CLI, REST, and CRM evidence."""
"""Validate bounded live four-source CLI, REST, CRM, and MCP evidence."""

from __future__ import annotations

Expand Down Expand Up @@ -29,19 +29,32 @@ def _ofac_by_kind(result: OfficialScreeningResult) -> dict[str, OfficialOfacList


def main() -> None:
if len(sys.argv) != 6:
raise SystemExit("five evidence file paths are required")
if len(sys.argv) != 7:
raise SystemExit("six evidence file paths are required")
refresh = OfficialSourceRefreshResult.model_validate_json(_read(sys.argv[1]))
replay = OfficialSourceRefreshResult.model_validate_json(_read(sys.argv[2]))
cli = OfficialScreeningResult.model_validate_json(_read(sys.argv[3]))
rest = OfficialScreeningResult.model_validate_json(_read(sys.argv[4]))
crm_payload = json.loads(_read(sys.argv[5]))
mcp_payload = json.loads(_read(sys.argv[6]))
if (
not isinstance(crm_payload, dict)
or crm_payload.get("live_official_sources") is not True
):
raise RuntimeError("CRM response is not live official-source evidence")
crm = OfficialScreeningResult.model_validate(crm_payload.get("result"))
if not isinstance(mcp_payload, dict) or set(mcp_payload.get("tools", [])) != {
"screen_transaction"
}:
raise RuntimeError("MCP response is outside the approved tool boundary")
if (
mcp_payload.get("protocol") != "stdio"
or mcp_payload.get("invalid_request_rejected") is not True
or mcp_payload.get("signal") != "RED"
or mcp_payload.get("business_action") != "HOLD"
or mcp_payload.get("automatic_clearance") is not False
):
raise RuntimeError("MCP response did not enforce the live safety gate")

if refresh.outcome is not OfficialSourceWriteOutcome.APPLIED:
raise RuntimeError("first live refresh was not applied")
Expand Down Expand Up @@ -98,6 +111,8 @@ def main() -> None:
kind: item.source_snapshot_id for kind, item in crm_ofac.items()
} != {kind: item.source_snapshot_id for kind, item in cli_ofac.items()}:
raise RuntimeError("CRM did not use the same active four-source bundle")
if mcp_payload.get("source_bundle_id") != refresh.bundle_id:
raise RuntimeError("MCP did not use the same active four-source bundle")

print(
json.dumps(
Expand All @@ -107,6 +122,7 @@ def main() -> None:
"dual_use_entries": refresh.dual_use_entry_count,
"fsf_entities": refresh.fsf_entity_count,
"idempotent_replay": True,
"mcp_same_bundle": True,
"ofac_consolidated_entries": (refresh.ofac_consolidated_entry_count),
"ofac_russia_candidate_held": True,
"ofac_sdn_entries": refresh.ofac_sdn_entry_count,
Expand Down
2 changes: 1 addition & 1 deletion scripts/test_compose.sh
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ if [[ "$python_version" != "Python 3.13.14" ]]; then
fi
uv_version="$("${compose[@]}" run --rm --no-deps app uv --version)"
read -r uv_name uv_semver _ <<<"$uv_version"
if [[ "$uv_name" != "uv" || "$uv_semver" != "0.12.1" ]]; then
if [[ "$uv_name" != "uv" || "$uv_semver" != "0.12.8" ]]; then
echo "unexpected container uv version: $uv_version" >&2
exit 1
fi
Expand Down
8 changes: 7 additions & 1 deletion scripts/test_official_screening_live.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,19 @@ run_bounded "$http_timeout_seconds" curl --fail-with-body --silent --show-error
-X POST \
"http://127.0.0.1:${host_port}/demo/api/crm/records/crm-quote-260810-0047/screen-official" \
--output "$evidence_dir/crm.json"
run_bounded "$http_timeout_seconds" \
uv run --locked python scripts/mcp_stdio_probe.py \
--project-name "$compose_project" \
--request examples/requests/official-screening.json \
>"$evidence_dir/mcp.json"

uv run python scripts/official_screening_live_assert.py \
"$evidence_dir/refresh.json" \
"$evidence_dir/replay.json" \
"$evidence_dir/cli.json" \
"$evidence_dir/rest.json" \
"$evidence_dir/crm.json"
"$evidence_dir/crm.json" \
"$evidence_dir/mcp.json"

"${compose[@]}" down --volumes --remove-orphans
assert_zero_residue
Expand Down
6 changes: 5 additions & 1 deletion src/tradesieve/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import psycopg
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp_types import ToolAnnotations

from tradesieve.adapters.postgres_official_sources import (
Expand Down Expand Up @@ -95,7 +96,10 @@ def screen_transaction(
RuntimeError,
ValueError,
):
raise RuntimeError(UNAVAILABLE_MESSAGE) from None
# MCP SDK 2.1+ deliberately redacts unexpected exceptions. Use the
# SDK's anticipated tool-error type so callers receive our bounded,
# fail-closed instruction without exposing the underlying failure.
raise ToolError(UNAVAILABLE_MESSAGE) from None

return server

Expand Down
4 changes: 4 additions & 0 deletions tests/unit/test_official_screening_live_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def test_live_gate_is_bounded_isolated_authenticated_and_zero_residue() -> None:
assert "Authorization: Bearer local_demo_only_official_screening_token" in text
assert "/v1/official-screenings" in text
assert "/screen-official" in text
assert "mcp_stdio_probe.py" in text
assert '"$evidence_dir/mcp.json"' in text
assert "official_screening_live_assert.py" in text
assert "mktemp -d" in text
assert 'rm -r "$evidence_dir"' in text
Expand All @@ -44,6 +46,8 @@ def test_assertion_binds_all_interfaces_and_current_ofac_candidate() -> None:
assert 'set(values) != {"SDN", "CONSOLIDATED"}' in text
assert '"RUSSIA-EO14024"' in text
assert "cli.model_dump() != rest.model_dump()" in text
assert 'mcp_payload.get("source_bundle_id") != refresh.bundle_id' in text
assert '"mcp_same_bundle": True' in text
assert '"ofac_russia_candidate_held": True' in text
assert "whole_name" not in text
assert "identifier.number" not in text
Expand Down
Loading
Loading