CI deploy 0c6476e
Browse files- amanpay/agentic_orchestration/__init__.py +0 -0
- amanpay/agentic_orchestration/agents.py +89 -0
- amanpay/agentic_orchestration/api.py +85 -0
- amanpay/agentic_orchestration/orchestrator.py +239 -0
- amanpay/agentic_orchestration/schemas.py +83 -0
- amanpay/agentic_orchestration/tools.py +74 -0
- api/main.py +4 -0
- build_info.json +1 -1
amanpay/agentic_orchestration/__init__.py
ADDED
|
File without changes
|
amanpay/agentic_orchestration/agents.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bounded PR C2 agents — Context, Risk, Auth-Rec, Rail, Explanation.
|
| 2 |
+
|
| 3 |
+
Each agent is a reasoning-only actor (PR B registry `can_execute_payment=False`). It produces a
|
| 4 |
+
STRUCTURED, validated output and never carries authority. Tool-using agents (Context, Risk, Rail)
|
| 5 |
+
reach data/models only through the PR B `ToolGateway` with a PoP-checked `CapabilityGrant`.
|
| 6 |
+
|
| 7 |
+
Authority reminders (enforced by the orchestrator + PDP):
|
| 8 |
+
* Risk Agent output is ADVISORY (recommend_step_up may only add caution).
|
| 9 |
+
* Auth-Rec Agent may recommend only a method meeting/exceeding the PDP minimum.
|
| 10 |
+
* Rail Agent ranks only eligible/available rails.
|
| 11 |
+
* Explanation Agent renders reason codes; it cannot change decisions.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from typing import Dict, List, Optional
|
| 18 |
+
|
| 19 |
+
from amanpay.agentic_orchestration import schemas
|
| 20 |
+
from amanpay.agentic_orchestration.tools import (
|
| 21 |
+
TOOL_FEATURES, TOOL_RAIL_OPTIONS, TOOL_RISK_MODEL, ToolClient,
|
| 22 |
+
)
|
| 23 |
+
from amanpay.agentic_risk.reason_codes import render, validate as validate_codes
|
| 24 |
+
|
| 25 |
+
# Assurance ordering is CONTEXT-SENSITIVE (see AGENT_ARCHITECTURE); this rank is used only to
|
| 26 |
+
# reject a recommendation that is *below* the PDP minimum, never to assert a universal hierarchy.
|
| 27 |
+
_ASSURANCE_RANK = {"trusted_session": 0, "webauthn": 2, "oob": 2, "webauthn+oob": 3,
|
| 28 |
+
"manual_review": 4}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ContextAgent:
|
| 32 |
+
spiffe = "spiffe://amanpay/agent/context"
|
| 33 |
+
|
| 34 |
+
def run(self, client: ToolClient, cap, txn_id: str, *, now: float, intent_hash: str) -> Dict:
|
| 35 |
+
out = client.call(cap, TOOL_FEATURES, {"txn_id": txn_id}, now=now, intent_hash=intent_hash)
|
| 36 |
+
return schemas.validate_context(out)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class RiskAgent:
|
| 40 |
+
spiffe = "spiffe://amanpay/agent/risk"
|
| 41 |
+
|
| 42 |
+
def run(self, client: ToolClient, cap, txn_id: str, *, now: float, intent_hash: str) -> Dict:
|
| 43 |
+
out = client.call(cap, TOOL_RISK_MODEL, {"txn_id": txn_id}, now=now, intent_hash=intent_hash)
|
| 44 |
+
return schemas.validate_risk(out) # advisory only
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class AuthRecommendationAgent:
|
| 48 |
+
spiffe = "spiffe://amanpay/agent/auth"
|
| 49 |
+
|
| 50 |
+
def run(self, *, pdp_required: List[str], risk_band: str,
|
| 51 |
+
supported: List[str]) -> Dict:
|
| 52 |
+
"""Recommend a method that MEETS/EXCEEDS the PDP minimum. Never weakens it."""
|
| 53 |
+
min_rank = max((_ASSURANCE_RANK.get(m, 0) for m in pdp_required), default=0)
|
| 54 |
+
# prefer the least-intrusive supported method that still meets the minimum
|
| 55 |
+
eligible = [m for m in supported
|
| 56 |
+
if _ASSURANCE_RANK.get(m, 0) >= min_rank]
|
| 57 |
+
# elevated/high risk -> lean to a stronger eligible method
|
| 58 |
+
if risk_band in ("elevated", "high") and "webauthn+oob" in supported:
|
| 59 |
+
rec = "webauthn+oob"
|
| 60 |
+
elif eligible:
|
| 61 |
+
rec = sorted(eligible, key=lambda m: _ASSURANCE_RANK.get(m, 0))[0]
|
| 62 |
+
else:
|
| 63 |
+
rec = "manual_review"
|
| 64 |
+
meets = _ASSURANCE_RANK.get(rec, 0) >= min_rank
|
| 65 |
+
return schemas.validate_auth({"recommended_method": rec, "meets_pdp_minimum": bool(meets)})
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class RailRecommendationAgent:
|
| 69 |
+
spiffe = "spiffe://amanpay/agent/rail"
|
| 70 |
+
|
| 71 |
+
def run(self, client: ToolClient, cap, *, country: str, currency: str, now: float,
|
| 72 |
+
intent_hash: str, user_pref: str = "") -> Dict:
|
| 73 |
+
opts = client.call(cap, TOOL_RAIL_OPTIONS, {"country": country, "currency": currency},
|
| 74 |
+
now=now, intent_hash=intent_hash)
|
| 75 |
+
rails = list(opts.get("rails", []))
|
| 76 |
+
# order signals: user preference first, otherwise stable configured order (eligibility
|
| 77 |
+
# already applied by the tool). Never adds a rail not returned by the tool.
|
| 78 |
+
ranked = sorted(rails, key=lambda r: (0 if r == user_pref else 1))
|
| 79 |
+
return schemas.validate_rail({"ranked": ranked, "eligible_only": True})
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class ExplanationAgent:
|
| 83 |
+
spiffe = "spiffe://amanpay/agent/explain"
|
| 84 |
+
|
| 85 |
+
def run(self, *, reason_codes: List[str], locale: str = "en") -> Dict:
|
| 86 |
+
codes = validate_codes(reason_codes) # allowlist only; no invented codes
|
| 87 |
+
messages = [render(c, locale) for c in codes]
|
| 88 |
+
return schemas.validate_explanation({"locale": locale, "messages": messages,
|
| 89 |
+
"reason_codes": codes})
|
amanpay/agentic_orchestration/api.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Internal /ai/v1 API surface for PR C2 (labelled demo).
|
| 2 |
+
|
| 3 |
+
Per the checkpoint API-exposure classification, the per-agent endpoints (context/risk/auth/rails/
|
| 4 |
+
explain) are internal-only and are NOT exposed as public HTTP routes. This module exposes only:
|
| 5 |
+
* GET /ai/v1/models/status — safe aggregate status (no scores/thresholds).
|
| 6 |
+
* POST /ai/v1/demo/payee — register a demo payee (returns ref, never the raw IBAN).
|
| 7 |
+
* POST /ai/v1/demo/orchestrate — run the secured agent workflow (mock provider, shadow model).
|
| 8 |
+
|
| 9 |
+
The demo endpoint is gated: it runs only when AMANPAY_ENABLE_AI_DEMO is truthy (default on for the
|
| 10 |
+
trusted HF demo; disable in real production). It exposes no keys/capabilities/approval secrets/raw
|
| 11 |
+
IBANs and cannot mint execution capabilities or move real money.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
from fastapi import APIRouter, Body, HTTPException
|
| 20 |
+
|
| 21 |
+
from amanpay.agentic_orchestration.orchestrator import DEMO_LABEL, SecureAgentOrchestrator
|
| 22 |
+
from amanpay.agentic_risk.features import UserHistory
|
| 23 |
+
from amanpay.agentic_risk.reason_codes import REASON_CODES_VERSION
|
| 24 |
+
from amanpay.agentic_risk.features import FEATURE_SCHEMA_VERSION
|
| 25 |
+
|
| 26 |
+
router = APIRouter(prefix="/ai/v1", tags=["agentic-risk"])
|
| 27 |
+
|
| 28 |
+
_orch = SecureAgentOrchestrator()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _demo_enabled() -> bool:
|
| 32 |
+
v = os.getenv("AMANPAY_ENABLE_AI_DEMO", "1").strip().lower()
|
| 33 |
+
return v not in ("", "0", "false", "no")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@router.get("/models/status")
|
| 37 |
+
def models_status() -> dict:
|
| 38 |
+
"""Safe status — versions + shadow flag, NO scores or thresholds."""
|
| 39 |
+
return {"behavioural_model": _orch.shadow.model.name if _orch.shadow.model else "none",
|
| 40 |
+
"model_version": _orch.shadow.model.version if _orch.shadow.model else "none",
|
| 41 |
+
"feature_version": FEATURE_SCHEMA_VERSION,
|
| 42 |
+
"reason_codes_version": REASON_CODES_VERSION,
|
| 43 |
+
"shadow_only": True, "affects_payment": False, "label": DEMO_LABEL}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@router.post("/demo/payee")
|
| 47 |
+
def demo_payee(body: dict = Body(...)) -> dict:
|
| 48 |
+
try:
|
| 49 |
+
p = _orch.base.payees.register(body["iban"], country=body.get("country", "SA"))
|
| 50 |
+
except (KeyError, ValueError) as exc:
|
| 51 |
+
raise HTTPException(status_code=422, detail=str(exc))
|
| 52 |
+
return {"payee_ref": p.payee_ref, "display": p.display, "label": DEMO_LABEL}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@router.post("/demo/orchestrate")
|
| 56 |
+
def demo_orchestrate(body: dict = Body(...)) -> dict:
|
| 57 |
+
if not _demo_enabled():
|
| 58 |
+
raise HTTPException(status_code=403, detail="AI demo disabled")
|
| 59 |
+
try:
|
| 60 |
+
hist = UserHistory(
|
| 61 |
+
payments=int(body.get("history_payments", 20)),
|
| 62 |
+
payees=set(body.get("known_payees", [])),
|
| 63 |
+
merchants=set(body.get("known_merchants", ["m1"])),
|
| 64 |
+
amount_mean=float(body.get("amount_mean", 5000)),
|
| 65 |
+
amount_std=float(body.get("amount_std", 1500)),
|
| 66 |
+
hour_hist={int(body.get("usual_hour", 12)): 10},
|
| 67 |
+
recent_24h=int(body.get("recent_24h", 1)),
|
| 68 |
+
device_trust_days=float(body.get("device_trust_days", 200)),
|
| 69 |
+
rail_pref=body.get("rail_pref", "sarie"))
|
| 70 |
+
r = _orch.run(user_id=body.get("user_id", "demo"), payee_ref=body["payee_ref"],
|
| 71 |
+
amount_minor=int(body["amount_minor"]), hist=hist,
|
| 72 |
+
consent=bool(body.get("consent", True)),
|
| 73 |
+
approve=bool(body.get("approve", False)),
|
| 74 |
+
country=body.get("country", "SA"), currency=body.get("currency", "SAR"),
|
| 75 |
+
merchant=body.get("merchant", "m1"),
|
| 76 |
+
now=float(body.get("now", time.time())), locale=body.get("locale", "en"))
|
| 77 |
+
except (KeyError, ValueError) as exc:
|
| 78 |
+
raise HTTPException(status_code=422, detail=str(exc))
|
| 79 |
+
# Return only safe fields (no keys/capabilities/approval secrets/raw IBAN).
|
| 80 |
+
return {"decision": r.decision, "reason_codes": r.reason_codes,
|
| 81 |
+
"required_auth": r.required_auth, "agent_state": r.agent_state,
|
| 82 |
+
"risk": r.risk, "auth_recommendation": r.auth_recommendation,
|
| 83 |
+
"rails": r.rails, "explanation": r.explanation,
|
| 84 |
+
"payment": r.payment, "audit": r.audit, "fallback": r.fallback,
|
| 85 |
+
"labels": r.labels}
|
amanpay/agentic_orchestration/orchestrator.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PR C2 — Secure Agent Orchestration.
|
| 2 |
+
|
| 3 |
+
Runs the bounded agents through PR B security (identities, signed AgentMessages, PoP-checked
|
| 4 |
+
capabilities, ToolGateway, ReplayStore, AuditChain, budgets) and feeds their outputs to the
|
| 5 |
+
deterministic PR B PDP, which stays authoritative. The C1 behavioural-risk model runs in SHADOW —
|
| 6 |
+
its output is an advisory input (`recommend_step_up`) that can only ADD caution.
|
| 7 |
+
|
| 8 |
+
Guarantees (tested): Risk Agent cannot call Payment Core; agent "allow" cannot override a PDP deny;
|
| 9 |
+
agent cannot lower a PDP step-up; malformed/absent agent output falls back to deterministic policy;
|
| 10 |
+
only Payment Core initiates.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import secrets
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from typing import Dict, List, Optional
|
| 18 |
+
|
| 19 |
+
from amanpay.agent_security import envelopes as E
|
| 20 |
+
from amanpay.agent_security.budgets import AgentBudget, BudgetExceeded
|
| 21 |
+
from amanpay.agent_security.gateway import MessageRejected, ToolError, validate_message
|
| 22 |
+
from amanpay.agent_security.orchestrator import AgentPaymentOrchestrator, ENV
|
| 23 |
+
from amanpay.agentic_orchestration import schemas
|
| 24 |
+
from amanpay.agentic_orchestration.agents import (
|
| 25 |
+
AuthRecommendationAgent, ContextAgent, ExplanationAgent, RailRecommendationAgent, RiskAgent,
|
| 26 |
+
)
|
| 27 |
+
from amanpay.agentic_orchestration.tools import (
|
| 28 |
+
TOOL_FEATURES, TOOL_RAIL_OPTIONS, TOOL_RISK_MODEL, ToolClient, register_tools,
|
| 29 |
+
)
|
| 30 |
+
from amanpay.agentic_risk.calibration import PlattCalibrator
|
| 31 |
+
from amanpay.agentic_risk.evaluation import temporal_user_split
|
| 32 |
+
from amanpay.agentic_risk.features import FeatureProvider, Transaction, UserHistory
|
| 33 |
+
from amanpay.agentic_risk.models import LogisticRegression
|
| 34 |
+
from amanpay.agentic_risk.shadow import ShadowRiskService
|
| 35 |
+
from amanpay.agentic_risk.synthetic import SyntheticConfig, generate
|
| 36 |
+
|
| 37 |
+
DEMO_LABEL = ("DEMO ONLY — local identity issuer, mock agents, mock provider, shadow-only "
|
| 38 |
+
"behavioural model (no payment authority), no real-money authority.")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _default_shadow() -> ShadowRiskService:
|
| 42 |
+
"""Fit the C1 logistic model on deterministic synthetic data for the demo (shadow-only)."""
|
| 43 |
+
import numpy as np
|
| 44 |
+
ex = generate(SyntheticConfig(n_users=40, seed=1234))
|
| 45 |
+
fp = FeatureProvider()
|
| 46 |
+
X = np.array([fp.derive(e.hist, e.txn)["vector"] for e in ex])
|
| 47 |
+
y = np.array([e.label for e in ex])
|
| 48 |
+
uid = np.array([e.user_id for e in ex]); t = np.array([e.t for e in ex])
|
| 49 |
+
tr, _ = temporal_user_split(uid, t, test_frac=0.3)
|
| 50 |
+
m = LogisticRegression().fit(X[tr], y[tr])
|
| 51 |
+
cal = PlattCalibrator().fit(m.predict_proba(X[tr]), y[tr])
|
| 52 |
+
return ShadowRiskService(m, cal)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class OrchestrationResult:
|
| 57 |
+
decision: str
|
| 58 |
+
reason_codes: List[str]
|
| 59 |
+
required_auth: List[str]
|
| 60 |
+
agent_state: str
|
| 61 |
+
context: Optional[Dict] = None
|
| 62 |
+
risk: Optional[Dict] = None
|
| 63 |
+
auth_recommendation: Optional[Dict] = None
|
| 64 |
+
rails: Optional[Dict] = None
|
| 65 |
+
explanation: Optional[Dict] = None
|
| 66 |
+
payment: Optional[Dict] = None
|
| 67 |
+
audit: Optional[Dict] = None
|
| 68 |
+
fallback: Optional[str] = None
|
| 69 |
+
labels: Dict[str, str] = field(default_factory=lambda: {"note": DEMO_LABEL})
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class SecureAgentOrchestrator:
|
| 73 |
+
"""Composes the PR B payment orchestrator + C1 shadow model + the C2 agent layer."""
|
| 74 |
+
|
| 75 |
+
def __init__(self, shadow: Optional[ShadowRiskService] = None) -> None:
|
| 76 |
+
self.base = AgentPaymentOrchestrator() # PR B: keyring/issuer/pdp/replay/audit/core
|
| 77 |
+
self.shadow = shadow or _default_shadow()
|
| 78 |
+
self.gw = __import__("amanpay.agent_security.gateway", fromlist=["ToolGateway"]).ToolGateway(
|
| 79 |
+
self.base.kr, self.base.replay, env=ENV)
|
| 80 |
+
self._txns: Dict[str, Dict] = {} # txn_id -> {hist, txn, consent}
|
| 81 |
+
register_tools(self.gw, features_handler=self._features, risk_handler=self._risk,
|
| 82 |
+
rail_handler=self._rails)
|
| 83 |
+
# short-lived agent identities (PR B LocalDemoIssuer)
|
| 84 |
+
self.ids = {a: self.base.issuer.issue(a, now=0.0, ttl=1_000_000.0)
|
| 85 |
+
for a in ("spiffe://amanpay/agent/context", "spiffe://amanpay/agent/risk",
|
| 86 |
+
"spiffe://amanpay/agent/rail")}
|
| 87 |
+
self.context_agent = ContextAgent(); self.risk_agent = RiskAgent()
|
| 88 |
+
self.auth_agent = AuthRecommendationAgent(); self.rail_agent = RailRecommendationAgent()
|
| 89 |
+
self.explain_agent = ExplanationAgent()
|
| 90 |
+
|
| 91 |
+
# -- tool handlers (resolve behavioural data by txn_id from the trusted store) ------ #
|
| 92 |
+
def _features(self, args: Dict, ctx: Dict) -> Dict:
|
| 93 |
+
rec = self._txns[args["txn_id"]]
|
| 94 |
+
f = FeatureProvider().derive(rec["hist"], rec["txn"])
|
| 95 |
+
return {"channel": rec.get("channel", "app"), "recent_count": int(rec["hist"].recent_24h),
|
| 96 |
+
"cold_start": bool(f["cold_start"]), "missing": list(f["missing"]),
|
| 97 |
+
"feature_version": f["schema_version"]}
|
| 98 |
+
|
| 99 |
+
def _risk(self, args: Dict, ctx: Dict) -> Dict:
|
| 100 |
+
rec = self._txns[args["txn_id"]]
|
| 101 |
+
a = self.shadow.assess(rec["hist"], rec["txn"], consent=rec["consent"])
|
| 102 |
+
return {"band": a.band,
|
| 103 |
+
"recommend_step_up": a.band in ("elevated", "high", "uncertain")
|
| 104 |
+
or a.confidence == "uncertain",
|
| 105 |
+
"reason_codes": a.reason_codes, "confidence": a.confidence,
|
| 106 |
+
"model_version": a.model_version, "feature_version": a.feature_version,
|
| 107 |
+
"shadow": bool(a.shadow)}
|
| 108 |
+
|
| 109 |
+
def _rails(self, args: Dict, ctx: Dict) -> Dict:
|
| 110 |
+
rails = self.base.provider.rails_by_country.get(args["country"].upper(), [])
|
| 111 |
+
return {"rails": list(rails), "eligible_only": True}
|
| 112 |
+
|
| 113 |
+
def _cap(self, subject_spiffe: str, cnf_kid: str, audience: str, intent, *, now: float):
|
| 114 |
+
return E.make_capability(
|
| 115 |
+
self.base.kr, kid=self.base.pdp_k.kid, issuer="spiffe://amanpay/pdp", key_version=1,
|
| 116 |
+
env=ENV, subject_agent=subject_spiffe, cnf=cnf_kid, audience=audience,
|
| 117 |
+
operation=audience, resource=intent.intent_id, intent_id=intent.intent_id,
|
| 118 |
+
intent_hash=intent.intent_hash, now=now)
|
| 119 |
+
|
| 120 |
+
def _msg(self, sender: str, recipient: str, mtype: str, payload: Dict, intent, flow: str,
|
| 121 |
+
*, now: float) -> None:
|
| 122 |
+
"""Send + validate one signed AgentMessage (proves the PR B envelope path)."""
|
| 123 |
+
kid = self.ids[sender].kid if sender in self.ids else self.base.pdp_k.kid
|
| 124 |
+
m = E.make_message(self.base.kr, kid=kid, sender=sender, recipient=recipient,
|
| 125 |
+
audience=recipient, intent_id=intent.intent_id,
|
| 126 |
+
intent_hash=intent.intent_hash, message_type=mtype, payload=payload,
|
| 127 |
+
flow_id=flow, env=ENV, now=now)
|
| 128 |
+
validate_message(self.base.kr, self.base.replay, m, now=now, env=ENV,
|
| 129 |
+
expected_intent_hash=intent.intent_hash, expected_flow_id=flow)
|
| 130 |
+
|
| 131 |
+
# -- the secured workflow ----------------------------------------------------------- #
|
| 132 |
+
def run(self, *, user_id: str, payee_ref: str, amount_minor: int, hist: UserHistory,
|
| 133 |
+
consent: bool = True, approve: bool = False, country: str = "SA",
|
| 134 |
+
currency: str = "SAR", merchant: str = "m1", now: float = 1000.0,
|
| 135 |
+
locale: str = "en") -> OrchestrationResult:
|
| 136 |
+
payee = self.base.payees.get(payee_ref)
|
| 137 |
+
if payee is None:
|
| 138 |
+
raise ValueError("unknown payee_ref")
|
| 139 |
+
flow = "flow_" + secrets.token_hex(6)
|
| 140 |
+
txn_id = "txn_" + secrets.token_hex(6)
|
| 141 |
+
txn = Transaction(amount_minor=amount_minor, hour=int(now // 3600) % 24,
|
| 142 |
+
payee_ref=payee_ref, merchant=merchant, country=country,
|
| 143 |
+
currency=currency, rail="sarie", country_currency_ok=True)
|
| 144 |
+
self._txns[txn_id] = {"hist": hist, "txn": txn, "consent": consent, "channel": "app"}
|
| 145 |
+
|
| 146 |
+
intent = E.make_intent(
|
| 147 |
+
self.base.kr, kid=self.base.fe_k.kid, issuer="spiffe://amanpay/frontend", key_version=1,
|
| 148 |
+
env=ENV, user_id=user_id, operation="payment.create", amount_minor=amount_minor,
|
| 149 |
+
currency=currency, payee_ref=payee.payee_ref, payee_display=payee.display,
|
| 150 |
+
payee_binding_hash=payee.binding_hash, merchant=merchant, country=country,
|
| 151 |
+
reference="invoice", now=now)
|
| 152 |
+
self.base.audit.append(actor_identity="spiffe://amanpay/frontend", action="intent.created",
|
| 153 |
+
result="ok", intent_id=intent.intent_id,
|
| 154 |
+
intent_hash=intent.intent_hash, now=now)
|
| 155 |
+
|
| 156 |
+
budget = AgentBudget(started_at=now)
|
| 157 |
+
fallback = None
|
| 158 |
+
context = risk = None
|
| 159 |
+
# ----- Context + Risk agents (advisory), with fail-closed fallback -----
|
| 160 |
+
try:
|
| 161 |
+
cctx = ToolClient(self.gw, self.base.kr, self.context_agent.spiffe,
|
| 162 |
+
self.ids[self.context_agent.spiffe].kid, budget)
|
| 163 |
+
ccap = self._cap(self.context_agent.spiffe, cctx.kid, TOOL_FEATURES, intent, now=now)
|
| 164 |
+
context = self.context_agent.run(cctx, ccap, txn_id, now=now,
|
| 165 |
+
intent_hash=intent.intent_hash)
|
| 166 |
+
self._msg(self.context_agent.spiffe, self.risk_agent.spiffe, schemas.CONTEXT_RESULT,
|
| 167 |
+
context, intent, flow, now=now)
|
| 168 |
+
|
| 169 |
+
rctx = ToolClient(self.gw, self.base.kr, self.risk_agent.spiffe,
|
| 170 |
+
self.ids[self.risk_agent.spiffe].kid, budget)
|
| 171 |
+
rcap = self._cap(self.risk_agent.spiffe, rctx.kid, TOOL_RISK_MODEL, intent, now=now)
|
| 172 |
+
risk = self.risk_agent.run(rctx, rcap, txn_id, now=now, intent_hash=intent.intent_hash)
|
| 173 |
+
self._msg(self.risk_agent.spiffe, "spiffe://amanpay/pdp", schemas.RISK_RESULT,
|
| 174 |
+
risk, intent, flow, now=now)
|
| 175 |
+
except (BudgetExceeded, ToolError, MessageRejected, schemas.SchemaError) as exc:
|
| 176 |
+
fallback = f"agent-failure->deterministic ({type(exc).__name__})"
|
| 177 |
+
self.base.audit.append(actor_identity="spiffe://amanpay/agent/risk",
|
| 178 |
+
action="conflicting.action", result="fallback",
|
| 179 |
+
intent_id=intent.intent_id, reason_codes=[], now=now)
|
| 180 |
+
risk = None
|
| 181 |
+
|
| 182 |
+
# ----- Deterministic PDP (authoritative); risk is ADVISORY (add-caution only) -----
|
| 183 |
+
agent_rec = {"recommend_step_up": bool(risk["recommend_step_up"])} if risk else None
|
| 184 |
+
ctx_for_pdp = {"biometric_ok": True, "confidence": 0.95,
|
| 185 |
+
"modalities": ["face", "fp", "voice"],
|
| 186 |
+
"recent_count": int(hist.recent_24h)}
|
| 187 |
+
d1 = self.base.pdp.evaluate(intent, None, context=ctx_for_pdp, now=now + 1,
|
| 188 |
+
idempotency_key=txn_id, agent_recommendation=agent_rec)
|
| 189 |
+
|
| 190 |
+
# ----- Auth recommendation (advisory; PDP already chose the minimum) -----
|
| 191 |
+
auth_rec = self.auth_agent.run(pdp_required=d1.required_auth,
|
| 192 |
+
risk_band=(risk["band"] if risk else "model_unavailable"),
|
| 193 |
+
supported=["trusted_session", "webauthn", "oob",
|
| 194 |
+
"webauthn+oob"])
|
| 195 |
+
# ----- Rail ranking (only configured/available) -----
|
| 196 |
+
try:
|
| 197 |
+
rcl = ToolClient(self.gw, self.base.kr, self.rail_agent.spiffe,
|
| 198 |
+
self.ids[self.rail_agent.spiffe].kid, budget)
|
| 199 |
+
rlcap = self._cap(self.rail_agent.spiffe, rcl.kid, TOOL_RAIL_OPTIONS, intent, now=now)
|
| 200 |
+
rails = self.rail_agent.run(rcl, rlcap, country=country, currency=currency, now=now,
|
| 201 |
+
intent_hash=intent.intent_hash, user_pref=hist.rail_pref)
|
| 202 |
+
except (ToolError, schemas.SchemaError):
|
| 203 |
+
rails = {"ranked": [], "eligible_only": True}
|
| 204 |
+
|
| 205 |
+
reason_codes = list(risk["reason_codes"]) if risk else ["MODEL_UNAVAILABLE"]
|
| 206 |
+
explanation = self.explain_agent.run(reason_codes=reason_codes, locale=locale)
|
| 207 |
+
|
| 208 |
+
result = OrchestrationResult(
|
| 209 |
+
decision=d1.decision, reason_codes=d1.reason_codes, required_auth=d1.required_auth,
|
| 210 |
+
agent_state="policy_evaluated", context=context, risk=risk,
|
| 211 |
+
auth_recommendation=auth_rec, rails=rails, explanation=explanation,
|
| 212 |
+
audit=self.base.audit.summary(), fallback=fallback)
|
| 213 |
+
|
| 214 |
+
if d1.decision != "step_up" or not approve:
|
| 215 |
+
return result
|
| 216 |
+
|
| 217 |
+
# ----- User approval -> PDP validates -> single-use ExecutionCapability -> Payment Core -----
|
| 218 |
+
ap = self.base.approve(intent, now=now + 2)
|
| 219 |
+
d2 = self.base.pdp.evaluate(intent, ap, context=ctx_for_pdp, now=now + 3,
|
| 220 |
+
idempotency_key=txn_id, agent_recommendation=agent_rec)
|
| 221 |
+
if d2.decision != "allow":
|
| 222 |
+
result.decision = d2.decision; result.reason_codes = d2.reason_codes
|
| 223 |
+
return result
|
| 224 |
+
ec = d2.execution_capability
|
| 225 |
+
E.verify_execution_capability(self.base.kr, ec, now=now + 3, env=ENV)
|
| 226 |
+
if not self.base.replay.consume("exec-cap", ec.execution_id, now=now + 3):
|
| 227 |
+
result.decision = "deny"; return result
|
| 228 |
+
payment = self.base.core.create_payment(
|
| 229 |
+
user_id=user_id, amount_minor=amount_minor, country=country, currency=currency,
|
| 230 |
+
merchant_id=merchant, payee_iban=payee.iban,
|
| 231 |
+
consent={"granted": True, "purpose": "agent-orchestrated payment (demo)"},
|
| 232 |
+
idempotency_key=txn_id, now=now + 3)
|
| 233 |
+
self.base.audit.append(actor_identity="spiffe://amanpay/payment-core",
|
| 234 |
+
action="payment.initiated", result="ok",
|
| 235 |
+
intent_id=intent.intent_id, now=now + 3)
|
| 236 |
+
result.decision = "allow"; result.reason_codes = d2.reason_codes
|
| 237 |
+
result.agent_state = "initiated"; result.payment = payment.view()
|
| 238 |
+
result.audit = self.base.audit.summary()
|
| 239 |
+
return result
|
amanpay/agentic_orchestration/schemas.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Structured, validated agent-output schemas (PR C2).
|
| 2 |
+
|
| 3 |
+
Every model/agent result is treated as UNTRUSTED input until validated: it must match a strict
|
| 4 |
+
schema, carry versions + allowlisted reason codes, and must NOT carry executable instructions or
|
| 5 |
+
free-form text that could reach authorization logic. Malformed output is rejected (fail-closed).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Any, Dict, List
|
| 11 |
+
|
| 12 |
+
from amanpay.agentic_risk.reason_codes import validate as validate_codes
|
| 13 |
+
|
| 14 |
+
# Message types (must be a subset of PR B gateway MESSAGE_TYPES).
|
| 15 |
+
CONTEXT_RESULT = "context.result"
|
| 16 |
+
RISK_RESULT = "risk.result"
|
| 17 |
+
AUTH_RESULT = "auth.result"
|
| 18 |
+
RAIL_RESULT = "rail.result"
|
| 19 |
+
EXPLAIN_RESULT = "explain.result"
|
| 20 |
+
|
| 21 |
+
# Keys that must never appear in an agent payload (no executable/authorization instructions).
|
| 22 |
+
_FORBIDDEN_KEYS = {"command", "run", "exec", "sql", "url", "capability", "execution_capability",
|
| 23 |
+
"approve", "authorize", "decision", "override", "signature", "private_key",
|
| 24 |
+
"secret", "token"}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class SchemaError(ValueError):
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _check_forbidden(payload: Dict[str, Any]) -> None:
|
| 32 |
+
bad = [k for k in payload if k.lower() in _FORBIDDEN_KEYS]
|
| 33 |
+
if bad:
|
| 34 |
+
raise SchemaError(f"agent payload contains forbidden key(s): {bad}")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _require(payload: Dict[str, Any], fields: Dict[str, type]) -> None:
|
| 38 |
+
for k, t in fields.items():
|
| 39 |
+
if k not in payload:
|
| 40 |
+
raise SchemaError(f"missing field {k!r}")
|
| 41 |
+
if not isinstance(payload[k], t):
|
| 42 |
+
raise SchemaError(f"field {k!r} wrong type (want {t.__name__})")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def validate_context(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 46 |
+
_check_forbidden(payload)
|
| 47 |
+
_require(payload, {"channel": str, "recent_count": int, "cold_start": bool,
|
| 48 |
+
"missing": list, "feature_version": str})
|
| 49 |
+
return payload
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def validate_risk(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 53 |
+
"""Risk Agent output — advisory ONLY. It may set recommend_step_up (add caution) but carries
|
| 54 |
+
no decision/authority. Reason codes must be allowlisted."""
|
| 55 |
+
_check_forbidden(payload)
|
| 56 |
+
_require(payload, {"band": str, "recommend_step_up": bool, "reason_codes": list,
|
| 57 |
+
"confidence": str, "model_version": str, "feature_version": str,
|
| 58 |
+
"shadow": bool})
|
| 59 |
+
if payload["band"] not in ("low", "uncertain", "elevated", "high", "model_unavailable"):
|
| 60 |
+
raise SchemaError(f"invalid band: {payload['band']}")
|
| 61 |
+
validate_codes(payload["reason_codes"]) # allowlist enforcement
|
| 62 |
+
if payload.get("shadow") is not True:
|
| 63 |
+
raise SchemaError("risk output must be shadow=True in this phase")
|
| 64 |
+
return payload
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def validate_auth(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 68 |
+
_check_forbidden(payload)
|
| 69 |
+
_require(payload, {"recommended_method": str, "meets_pdp_minimum": bool})
|
| 70 |
+
return payload
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def validate_rail(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 74 |
+
_check_forbidden(payload)
|
| 75 |
+
_require(payload, {"ranked": list, "eligible_only": bool})
|
| 76 |
+
return payload
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def validate_explanation(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 80 |
+
_check_forbidden(payload)
|
| 81 |
+
_require(payload, {"locale": str, "messages": list, "reason_codes": list})
|
| 82 |
+
validate_codes(payload["reason_codes"])
|
| 83 |
+
return payload
|
amanpay/agentic_orchestration/tools.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tool handlers + ToolClient for PR C2 agents.
|
| 2 |
+
|
| 3 |
+
Tools are registered in the PR B `ToolGateway` (static allowlist, typed schemas, PoP-checked
|
| 4 |
+
capability, budgets, secret scrubbing). Agents reach data/models ONLY through these tools — never
|
| 5 |
+
the Payment Core, shell, DB, or arbitrary URLs.
|
| 6 |
+
|
| 7 |
+
Only a `txn_id` crosses the (signed) tool args; the handler resolves the actual behavioural data
|
| 8 |
+
from the orchestrator's trusted in-process store, so raw features never travel through signed
|
| 9 |
+
request bodies or agent messages.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from typing import Callable, Dict
|
| 15 |
+
|
| 16 |
+
from amanpay.agent_security import envelopes as E
|
| 17 |
+
from amanpay.agent_security.budgets import AgentBudget
|
| 18 |
+
from amanpay.agent_security.gateway import ToolGateway, ToolSpec
|
| 19 |
+
|
| 20 |
+
# Tool names (also the capability audiences).
|
| 21 |
+
TOOL_FEATURES = "tool:read_derived_features"
|
| 22 |
+
TOOL_RISK_MODEL = "tool:invoke_risk_model"
|
| 23 |
+
TOOL_RAIL_OPTIONS = "tool:rail_options"
|
| 24 |
+
|
| 25 |
+
Handler = Callable[[Dict, Dict], Dict]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def register_tools(gw: ToolGateway, *, features_handler: Handler, risk_handler: Handler,
|
| 29 |
+
rail_handler: Handler) -> None:
|
| 30 |
+
"""Register the C2 tools with orchestrator-provided handlers (closures over its txn store)."""
|
| 31 |
+
gw.register_tool(ToolSpec(
|
| 32 |
+
name=TOOL_FEATURES,
|
| 33 |
+
allowed_agents=frozenset({"spiffe://amanpay/agent/context"}),
|
| 34 |
+
input_schema={"txn_id": str},
|
| 35 |
+
output_schema={"channel": str, "recent_count": int, "cold_start": bool,
|
| 36 |
+
"missing": list, "feature_version": str},
|
| 37 |
+
handler=features_handler))
|
| 38 |
+
|
| 39 |
+
gw.register_tool(ToolSpec(
|
| 40 |
+
name=TOOL_RISK_MODEL,
|
| 41 |
+
allowed_agents=frozenset({"spiffe://amanpay/agent/risk"}),
|
| 42 |
+
input_schema={"txn_id": str},
|
| 43 |
+
output_schema={"band": str, "recommend_step_up": bool, "reason_codes": list,
|
| 44 |
+
"confidence": str, "model_version": str, "feature_version": str,
|
| 45 |
+
"shadow": bool},
|
| 46 |
+
handler=risk_handler))
|
| 47 |
+
|
| 48 |
+
gw.register_tool(ToolSpec(
|
| 49 |
+
name=TOOL_RAIL_OPTIONS,
|
| 50 |
+
allowed_agents=frozenset({"spiffe://amanpay/agent/rail"}),
|
| 51 |
+
input_schema={"country": str, "currency": str},
|
| 52 |
+
output_schema={"rails": list, "eligible_only": bool},
|
| 53 |
+
handler=rail_handler))
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class ToolClient:
|
| 57 |
+
"""Wraps a single agent's capability + key so it can call the gateway with a fresh PoP."""
|
| 58 |
+
|
| 59 |
+
def __init__(self, gw: ToolGateway, keyring, agent_spiffe: str, subject_kid: str,
|
| 60 |
+
budget: AgentBudget, env: str = "demo") -> None:
|
| 61 |
+
self.gw = gw
|
| 62 |
+
self.keyring = keyring
|
| 63 |
+
self.agent = agent_spiffe
|
| 64 |
+
self.kid = subject_kid
|
| 65 |
+
self.budget = budget
|
| 66 |
+
self.env = env
|
| 67 |
+
|
| 68 |
+
def call(self, capability: E.CapabilityGrant, tool_name: str, args: Dict, *,
|
| 69 |
+
now: float, intent_hash: str | None = None) -> Dict:
|
| 70 |
+
proof = E.make_use_proof(self.keyring, self.kid, capability, operation=tool_name,
|
| 71 |
+
audience=tool_name, request=args, now=now)
|
| 72 |
+
return self.gw.call(agent_spiffe=self.agent, tool_name=tool_name, args=args,
|
| 73 |
+
capability=capability, use_proof=proof, subject_kid=self.kid,
|
| 74 |
+
budget=self.budget, now=now, intent_hash=intent_hash)
|
api/main.py
CHANGED
|
@@ -117,6 +117,10 @@ app.include_router(notifications_router) # torch-free /notify/* endpoints
|
|
| 117 |
from api.agent_security_routes import router as agent_security_router # noqa: E402
|
| 118 |
app.include_router(agent_security_router)
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
_ROOT = os.path.dirname(os.path.dirname(__file__))
|
| 122 |
_FRONTEND = os.path.join(_ROOT, "frontend", "index.html") # legacy (rollback)
|
|
|
|
| 117 |
from api.agent_security_routes import router as agent_security_router # noqa: E402
|
| 118 |
app.include_router(agent_security_router)
|
| 119 |
|
| 120 |
+
# Agentic risk orchestration (/ai/v1 — shadow-only behavioural model, labelled demo).
|
| 121 |
+
from amanpay.agentic_orchestration.api import router as ai_router # noqa: E402
|
| 122 |
+
app.include_router(ai_router)
|
| 123 |
+
|
| 124 |
|
| 125 |
_ROOT = os.path.dirname(os.path.dirname(__file__))
|
| 126 |
_FRONTEND = os.path.join(_ROOT, "frontend", "index.html") # legacy (rollback)
|
build_info.json
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{"commit":"
|
|
|
|
| 1 |
+
{"commit":"0c6476e","build_time":"2026-07-13T04:57:19Z","frontend":"1.0.0"}
|