From 23cfa9c3b86a46a5e739cfd3d8c155d8fc8a5de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:30 +0200 Subject: [PATCH 01/24] Add minimal API application boundary --- services/api/app/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/api/app/__init__.py diff --git a/services/api/app/__init__.py b/services/api/app/__init__.py new file mode 100644 index 0000000..e69de29 From f2351149c75999b06ae24b186287ac2754beb6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:35 +0200 Subject: [PATCH 02/24] Add minimal API health endpoint --- services/api/app/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 services/api/app/main.py diff --git a/services/api/app/main.py b/services/api/app/main.py new file mode 100644 index 0000000..4ad9bf6 --- /dev/null +++ b/services/api/app/main.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI(title="SentinelAI API", version="0.1.0") + + +@app.get("/health", tags=["system"]) +def health() -> dict[str, str]: + return {"status": "ok"} From 001cadfa60f0f670751b98954f21391c46c8c122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:37 +0200 Subject: [PATCH 03/24] Add minimal API dependencies --- services/api/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 services/api/requirements.txt diff --git a/services/api/requirements.txt b/services/api/requirements.txt new file mode 100644 index 0000000..4d5bc91 --- /dev/null +++ b/services/api/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.116,<1.0 +uvicorn[standard]>=0.35,<1.0 From 753804356002f7a6d9b4bf688e98bd72818d3741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:42 +0200 Subject: [PATCH 04/24] Add deterministic risk engine package --- services/intelligence/sentinel_risk/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/intelligence/sentinel_risk/__init__.py diff --git a/services/intelligence/sentinel_risk/__init__.py b/services/intelligence/sentinel_risk/__init__.py new file mode 100644 index 0000000..e69de29 From 9c67fcaf6fe34d30fa64d4404216248e29c8c3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:47 +0200 Subject: [PATCH 05/24] Implement minimal deterministic risk policy --- services/intelligence/sentinel_risk/engine.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 services/intelligence/sentinel_risk/engine.py diff --git a/services/intelligence/sentinel_risk/engine.py b/services/intelligence/sentinel_risk/engine.py new file mode 100644 index 0000000..a9a61a7 --- /dev/null +++ b/services/intelligence/sentinel_risk/engine.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from enum import StrEnum + + +class Decision(StrEnum): + ALLOW = "allow" + WARN = "warn" + BLOCK = "block" + + +class ReputationStatus(StrEnum): + TRUSTED = "trusted" + UNKNOWN = "unknown" + MALICIOUS = "malicious" + + +class Confidence(StrEnum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + + +@dataclass(frozen=True) +class Reputation: + status: ReputationStatus + confidence: Confidence + + +@dataclass(frozen=True) +class TransactionFacts: + destination: str + is_unlimited_approval: bool = False + spender: str | None = None + + +def evaluate_transaction( + tx: TransactionFacts, + destination_reputation: Reputation, + spender_reputation: Reputation | None = None, +) -> Decision: + """Apply the minimal deterministic policy. + + Only independently high-confidence malicious evidence can BLOCK. + Unknown or unavailable evidence never becomes an implicit ALLOW. + """ + if ( + destination_reputation.status is ReputationStatus.MALICIOUS + and destination_reputation.confidence is Confidence.HIGH + ): + return Decision.BLOCK + + if ( + tx.is_unlimited_approval + and tx.spender + and spender_reputation + and spender_reputation.status is ReputationStatus.MALICIOUS + and spender_reputation.confidence is Confidence.HIGH + ): + return Decision.BLOCK + + if ( + destination_reputation.status is ReputationStatus.UNKNOWN + or destination_reputation.confidence is Confidence.LOW + ): + return Decision.WARN + + if tx.is_unlimited_approval: + return Decision.WARN + + return Decision.ALLOW From 459ce413b287133ced671fa71ac3815f67dcd360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:51 +0200 Subject: [PATCH 06/24] Add minimal reputation and decision telemetry schema --- services/telemetry/schema.sql | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 services/telemetry/schema.sql diff --git a/services/telemetry/schema.sql b/services/telemetry/schema.sql new file mode 100644 index 0000000..dfd6574 --- /dev/null +++ b/services/telemetry/schema.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS reputation_entries ( + chain_id BIGINT NOT NULL, + address TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('trusted', 'unknown', 'malicious')), + confidence TEXT NOT NULL CHECK (confidence IN ('low', 'medium', 'high')), + source TEXT NOT NULL, + observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (chain_id, address) +); + +CREATE TABLE IF NOT EXISTS decision_events ( + id UUID PRIMARY KEY, + request_id UUID NOT NULL, + chain_id BIGINT NOT NULL, + from_address TEXT NOT NULL, + to_address TEXT NOT NULL, + decision TEXT NOT NULL CHECK (decision IN ('allow', 'warn', 'block')), + risk_score INTEGER NOT NULL CHECK (risk_score BETWEEN 0 AND 100), + category TEXT NOT NULL, + confidence TEXT NOT NULL CHECK (confidence IN ('low', 'medium', 'high')), + reasons JSONB NOT NULL, + latency_ms INTEGER NOT NULL CHECK (latency_ms >= 0), + runtime_mode TEXT NOT NULL CHECK (runtime_mode IN ('shadow', 'enforce')), + policy_version TEXT NOT NULL, + user_action TEXT CHECK (user_action IS NULL OR user_action IN ('accepted', 'cancelled', 'overridden')), + wallet_outcome TEXT CHECK (wallet_outcome IS NULL OR wallet_outcome IN ('blocked', 'submitted', 'rejected', 'failed')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS decision_events_created_idx ON decision_events (created_at); +CREATE INDEX IF NOT EXISTS decision_events_decision_idx ON decision_events (decision); +CREATE INDEX IF NOT EXISTS decision_events_request_idx ON decision_events (request_id); From 5990fe78bda13cddc0b757067960024229d960cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:56:57 +0200 Subject: [PATCH 07/24] Replace placeholder with real API health test --- tests/test_health.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/test_health.py b/tests/test_health.py index 082544f..95a86dc 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,4 +1,13 @@ -def test_placeholder_health(): - # Placeholder test so CI has at least one deterministic test target. - # Replace with real API health test when app entrypoint is confirmed. - assert True +from fastapi.testclient import TestClient + +from services.api.app.main import app + + +client = TestClient(app) + + +def test_health_endpoint() -> None: + response = client.get("/health") + + assert response.status_code == 200 + assert response.json() == {"status": "ok"} From 19a7bd1c064257f40c676caa1fcabac573983c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:57:01 +0200 Subject: [PATCH 08/24] Add deterministic risk policy tests --- tests/test_risk_engine.py | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_risk_engine.py diff --git a/tests/test_risk_engine.py b/tests/test_risk_engine.py new file mode 100644 index 0000000..8f62287 --- /dev/null +++ b/tests/test_risk_engine.py @@ -0,0 +1,58 @@ +from services.intelligence.sentinel_risk.engine import ( + Confidence, + Decision, + Reputation, + ReputationStatus, + TransactionFacts, + evaluate_transaction, +) + + +def reputation(status: ReputationStatus, confidence: Confidence) -> Reputation: + return Reputation(status=status, confidence=confidence) + + +def test_high_confidence_malicious_destination_blocks() -> None: + decision = evaluate_transaction( + TransactionFacts(destination="0xdead"), + reputation(ReputationStatus.MALICIOUS, Confidence.HIGH), + ) + + assert decision is Decision.BLOCK + + +def test_unknown_destination_warns() -> None: + decision = evaluate_transaction( + TransactionFacts(destination="0xunknown"), + reputation(ReputationStatus.UNKNOWN, Confidence.LOW), + ) + + assert decision is Decision.WARN + + +def test_unlimited_approval_to_malicious_spender_blocks() -> None: + decision = evaluate_transaction( + TransactionFacts( + destination="0xtoken", + spender="0xspender", + is_unlimited_approval=True, + ), + reputation(ReputationStatus.TRUSTED, Confidence.HIGH), + reputation(ReputationStatus.MALICIOUS, Confidence.HIGH), + ) + + assert decision is Decision.BLOCK + + +def test_unlimited_approval_without_malicious_spender_only_warns() -> None: + decision = evaluate_transaction( + TransactionFacts( + destination="0xtoken", + spender="0xspender", + is_unlimited_approval=True, + ), + reputation(ReputationStatus.TRUSTED, Confidence.HIGH), + reputation(ReputationStatus.UNKNOWN, Confidence.LOW), + ) + + assert decision is Decision.WARN From 09df6f199f6c54e379b9cea5f5a264f161260ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:57:05 +0200 Subject: [PATCH 09/24] Add test dependencies --- requirements-dev.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..c3ae04d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest>=8.4,<9.0 +httpx>=0.28,<1.0 +-r services/api/requirements.txt From 24b567da9eac9eb86a6eca8a895d5e444ec252be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:57:14 +0200 Subject: [PATCH 10/24] Add strict transaction decision API models --- services/api/app/models.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 services/api/app/models.py diff --git a/services/api/app/models.py b/services/api/app/models.py new file mode 100644 index 0000000..c715fc8 --- /dev/null +++ b/services/api/app/models.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, ConfigDict, Field + + +class ReputationInput(BaseModel): + model_config = ConfigDict(extra="forbid") + + status: str = Field(pattern="^(trusted|unknown|malicious)$") + confidence: str = Field(pattern="^(low|medium|high)$") + + +class CheckTransactionRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + destination: str = Field(min_length=1) + is_unlimited_approval: bool = False + spender: str | None = None + destination_reputation: ReputationInput + spender_reputation: ReputationInput | None = None + + +class CheckTransactionResponse(BaseModel): + decision: str + policy_version: str From 2e92dc003c354d92123efe721a3e108f6bad97a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:57:48 +0200 Subject: [PATCH 11/24] Expose deterministic transaction decision endpoint --- services/api/app/main.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/services/api/app/main.py b/services/api/app/main.py index 4ad9bf6..b1c2285 100644 --- a/services/api/app/main.py +++ b/services/api/app/main.py @@ -1,8 +1,51 @@ from fastapi import FastAPI +from services.api.app.models import CheckTransactionRequest, CheckTransactionResponse +from services.intelligence.sentinel_risk.engine import ( + Confidence, + Reputation, + ReputationStatus, + TransactionFacts, + evaluate_transaction, +) + +POLICY_VERSION = "day13-v3" + app = FastAPI(title="SentinelAI API", version="0.1.0") @app.get("/health", tags=["system"]) def health() -> dict[str, str]: return {"status": "ok"} + + +@app.post("/v1/check-tx", response_model=CheckTransactionResponse, tags=["risk"]) +def check_transaction(request: CheckTransactionRequest) -> CheckTransactionResponse: + tx = TransactionFacts( + destination=request.destination, + is_unlimited_approval=request.is_unlimited_approval, + spender=request.spender, + ) + destination_reputation = Reputation( + status=ReputationStatus(request.destination_reputation.status), + confidence=Confidence(request.destination_reputation.confidence), + ) + spender_reputation = ( + Reputation( + status=ReputationStatus(request.spender_reputation.status), + confidence=Confidence(request.spender_reputation.confidence), + ) + if request.spender_reputation + else None + ) + + decision = evaluate_transaction( + tx, + destination_reputation, + spender_reputation, + ) + + return CheckTransactionResponse( + decision=decision.value, + policy_version=POLICY_VERSION, + ) From 6245401f6ad6938c8a6483a495e2ff449611016c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:57:53 +0200 Subject: [PATCH 12/24] Add API decision contract tests --- tests/test_api.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_api.py diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..d196efb --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,57 @@ +from fastapi.testclient import TestClient + +from services.api.app.main import app + + +client = TestClient(app) + + +def test_high_confidence_malicious_destination_blocks() -> None: + response = client.post( + "/v1/check-tx", + json={ + "destination": "0xdead", + "destination_reputation": { + "status": "malicious", + "confidence": "high", + }, + }, + ) + + assert response.status_code == 200 + assert response.json() == { + "decision": "block", + "policy_version": "day13-v3", + } + + +def test_unknown_destination_never_becomes_allow() -> None: + response = client.post( + "/v1/check-tx", + json={ + "destination": "0xunknown", + "destination_reputation": { + "status": "unknown", + "confidence": "low", + }, + }, + ) + + assert response.status_code == 200 + assert response.json()["decision"] == "warn" + + +def test_extra_request_fields_are_rejected() -> None: + response = client.post( + "/v1/check-tx", + json={ + "destination": "0xdead", + "unexpected": True, + "destination_reputation": { + "status": "trusted", + "confidence": "high", + }, + }, + ) + + assert response.status_code == 422 From b4961aeb42a6f81fff05c7430e4054e65867ff32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:57:56 +0200 Subject: [PATCH 13/24] Configure repository test discovery --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a498f38 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] From dde791398005300574a0c67fc14e7a54706c8830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:58:00 +0200 Subject: [PATCH 14/24] Add deterministic Python CI test gate --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..34339dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: python -m pip install -r requirements-dev.txt + + - name: Run tests + run: python -m pytest -q From e4fdb3fedb6f9d32b8d8e3741b25080579c13d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:58:08 +0200 Subject: [PATCH 15/24] Remove accidental repository scaffold file --- commit | 44 -------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 commit diff --git a/commit b/commit deleted file mode 100644 index 75ee07b..0000000 --- a/commit +++ /dev/null @@ -1,44 +0,0 @@ -SentinelAI/ -├── .github/ -│ ├── workflows/ -│ │ ├── ci.yml -│ │ └── security.yml -│ ├── ISSUE_TEMPLATE/ -│ │ └── bug_report.md -│ ├── PULL_REQUEST_TEMPLATE.md -│ ├── CODEOWNERS -│ └── dependabot.yml -│ -├── apps/ -│ ├── android-demo/ -│ └── dashboard/ -│ -├── sdk/ -│ └── android/ -│ -├── runtime/ -│ └── rust/ -│ -├── services/ -│ ├── api/ -│ ├── intelligence/ -│ └── telemetry/ -│ -├── docs/ -│ ├── architecture.md -│ ├── roadmap.md -│ └── security.md -│ -├── tests/ -├── scripts/ -├── docker/ -│ -├── README.md -├── LICENSE -├── SECURITY.md -├── CONTRIBUTING.md -├── CODE_OF_CONDUCT.md -├── .gitignore -├── .editorconfig -├── docker-compose.yml -└── Makefile From e9f74b4ee226520998083fb4caa2ec14662a4dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:58:12 +0200 Subject: [PATCH 16/24] Make service package imports explicit --- services/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/__init__.py diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 From d6cb9d4af500111526713e5629f0ccb813c9579b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:58:15 +0200 Subject: [PATCH 17/24] Make API package imports explicit --- services/api/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/api/__init__.py diff --git a/services/api/__init__.py b/services/api/__init__.py new file mode 100644 index 0000000..e69de29 From 2f79804479f1bfe6ec96d991fca325aac1c3572f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 15:58:18 +0200 Subject: [PATCH 18/24] Make intelligence package imports explicit --- services/intelligence/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/intelligence/__init__.py diff --git a/services/intelligence/__init__.py b/services/intelligence/__init__.py new file mode 100644 index 0000000..e69de29 From 2531c02812a2e334ece5addacd45d78172c23062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 16:01:23 +0200 Subject: [PATCH 19/24] fix: validate Ethereum addresses at API boundary --- services/api/app/validation.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 services/api/app/validation.py diff --git a/services/api/app/validation.py b/services/api/app/validation.py new file mode 100644 index 0000000..d428090 --- /dev/null +++ b/services/api/app/validation.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import re + +ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$") + + +def normalize_address(value: str) -> str: + if not ADDRESS_RE.fullmatch(value): + raise ValueError("invalid Ethereum address") + return value.lower() From 9d4e36993593fd329941384df10856b5de9d84a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 16:01:28 +0200 Subject: [PATCH 20/24] fix: enforce strict transaction identity validation --- services/api/app/models.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/services/api/app/models.py b/services/api/app/models.py index c715fc8..bbecf78 100644 --- a/services/api/app/models.py +++ b/services/api/app/models.py @@ -1,4 +1,6 @@ -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from services.api.app.validation import normalize_address class ReputationInput(BaseModel): @@ -11,13 +13,26 @@ class ReputationInput(BaseModel): class CheckTransactionRequest(BaseModel): model_config = ConfigDict(extra="forbid") - destination: str = Field(min_length=1) + chain_id: int = Field(gt=0) + destination: str is_unlimited_approval: bool = False spender: str | None = None destination_reputation: ReputationInput spender_reputation: ReputationInput | None = None + @field_validator("destination") + @classmethod + def validate_destination(cls, value: str) -> str: + return normalize_address(value) + + @field_validator("spender") + @classmethod + def validate_spender(cls, value: str | None) -> str | None: + return normalize_address(value) if value is not None else None + class CheckTransactionResponse(BaseModel): - decision: str + model_config = ConfigDict(extra="forbid") + + decision: str = Field(pattern="^(allow|warn|block)$") policy_version: str From 7c952b95ed2766bcdaa28e516f2f36d5f06ea454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 16:01:34 +0200 Subject: [PATCH 21/24] fix: bind risk decisions to chain identity --- services/api/app/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/api/app/main.py b/services/api/app/main.py index b1c2285..27c585d 100644 --- a/services/api/app/main.py +++ b/services/api/app/main.py @@ -22,6 +22,7 @@ def health() -> dict[str, str]: @app.post("/v1/check-tx", response_model=CheckTransactionResponse, tags=["risk"]) def check_transaction(request: CheckTransactionRequest) -> CheckTransactionResponse: tx = TransactionFacts( + chain_id=request.chain_id, destination=request.destination, is_unlimited_approval=request.is_unlimited_approval, spender=request.spender, From 98c48de5f470f34b9afc95e893eebca90b37a20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 16:01:42 +0200 Subject: [PATCH 22/24] fix: make risk transaction identity chain-aware --- services/intelligence/sentinel_risk/engine.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/intelligence/sentinel_risk/engine.py b/services/intelligence/sentinel_risk/engine.py index a9a61a7..3898407 100644 --- a/services/intelligence/sentinel_risk/engine.py +++ b/services/intelligence/sentinel_risk/engine.py @@ -28,6 +28,7 @@ class Reputation: @dataclass(frozen=True) class TransactionFacts: + chain_id: int destination: str is_unlimited_approval: bool = False spender: str | None = None @@ -40,8 +41,8 @@ def evaluate_transaction( ) -> Decision: """Apply the minimal deterministic policy. - Only independently high-confidence malicious evidence can BLOCK. - Unknown or unavailable evidence never becomes an implicit ALLOW. + Only high-confidence malicious evidence can BLOCK. Unknown or + unavailable evidence never becomes an implicit ALLOW. """ if ( destination_reputation.status is ReputationStatus.MALICIOUS @@ -51,8 +52,8 @@ def evaluate_transaction( if ( tx.is_unlimited_approval - and tx.spender - and spender_reputation + and tx.spender is not None + and spender_reputation is not None and spender_reputation.status is ReputationStatus.MALICIOUS and spender_reputation.confidence is Confidence.HIGH ): From e8c4e35466fc8b47bbb2ee4b713ecd73864c9382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 16:01:50 +0200 Subject: [PATCH 23/24] test: align API tests with strict transaction validation --- tests/test_api.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d196efb..009bc0b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,13 +4,16 @@ client = TestClient(app) +VALID_DESTINATION = "0x000000000000000000000000000000000000dEaD" +VALID_UNKNOWN = "0x0000000000000000000000000000000000000001" def test_high_confidence_malicious_destination_blocks() -> None: response = client.post( "/v1/check-tx", json={ - "destination": "0xdead", + "chain_id": 1, + "destination": VALID_DESTINATION, "destination_reputation": { "status": "malicious", "confidence": "high", @@ -29,7 +32,8 @@ def test_unknown_destination_never_becomes_allow() -> None: response = client.post( "/v1/check-tx", json={ - "destination": "0xunknown", + "chain_id": 1, + "destination": VALID_UNKNOWN, "destination_reputation": { "status": "unknown", "confidence": "low", @@ -41,11 +45,28 @@ def test_unknown_destination_never_becomes_allow() -> None: assert response.json()["decision"] == "warn" -def test_extra_request_fields_are_rejected() -> None: +def test_invalid_ethereum_address_is_rejected() -> None: response = client.post( "/v1/check-tx", json={ + "chain_id": 1, "destination": "0xdead", + "destination_reputation": { + "status": "trusted", + "confidence": "high", + }, + }, + ) + + assert response.status_code == 422 + + +def test_extra_request_fields_are_rejected() -> None: + response = client.post( + "/v1/check-tx", + json={ + "chain_id": 1, + "destination": VALID_DESTINATION, "unexpected": True, "destination_reputation": { "status": "trusted", From 4badb27f0f819abfde167fe8e41ca4a6998f8637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cryptofixyup=F0=9F=94=A5?= Date: Thu, 27 Aug 2026 16:01:58 +0200 Subject: [PATCH 24/24] test: cover strict deterministic policy boundaries --- tests/test_risk_engine.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/tests/test_risk_engine.py b/tests/test_risk_engine.py index 8f62287..ed4d648 100644 --- a/tests/test_risk_engine.py +++ b/tests/test_risk_engine.py @@ -12,9 +12,13 @@ def reputation(status: ReputationStatus, confidence: Confidence) -> Reputation: return Reputation(status=status, confidence=confidence) +def tx(**kwargs: object) -> TransactionFacts: + return TransactionFacts(chain_id=1, **kwargs) + + def test_high_confidence_malicious_destination_blocks() -> None: decision = evaluate_transaction( - TransactionFacts(destination="0xdead"), + tx(destination="0x000000000000000000000000000000000000dEaD"), reputation(ReputationStatus.MALICIOUS, Confidence.HIGH), ) @@ -23,7 +27,7 @@ def test_high_confidence_malicious_destination_blocks() -> None: def test_unknown_destination_warns() -> None: decision = evaluate_transaction( - TransactionFacts(destination="0xunknown"), + tx(destination="0x0000000000000000000000000000000000000001"), reputation(ReputationStatus.UNKNOWN, Confidence.LOW), ) @@ -32,9 +36,9 @@ def test_unknown_destination_warns() -> None: def test_unlimited_approval_to_malicious_spender_blocks() -> None: decision = evaluate_transaction( - TransactionFacts( - destination="0xtoken", - spender="0xspender", + tx( + destination="0x0000000000000000000000000000000000000002", + spender="0x0000000000000000000000000000000000000003", is_unlimited_approval=True, ), reputation(ReputationStatus.TRUSTED, Confidence.HIGH), @@ -46,9 +50,9 @@ def test_unlimited_approval_to_malicious_spender_blocks() -> None: def test_unlimited_approval_without_malicious_spender_only_warns() -> None: decision = evaluate_transaction( - TransactionFacts( - destination="0xtoken", - spender="0xspender", + tx( + destination="0x0000000000000000000000000000000000000002", + spender="0x0000000000000000000000000000000000000003", is_unlimited_approval=True, ), reputation(ReputationStatus.TRUSTED, Confidence.HIGH), @@ -56,3 +60,12 @@ def test_unlimited_approval_without_malicious_spender_only_warns() -> None: ) assert decision is Decision.WARN + + +def test_trusted_normal_transaction_allows() -> None: + decision = evaluate_transaction( + tx(destination="0x0000000000000000000000000000000000000002"), + reputation(ReputationStatus.TRUSTED, Confidence.HIGH), + ) + + assert decision is Decision.ALLOW