Metadata-Version: 2.5
Name: koda-agent-runtime
Version: 0.2.0rc1
Summary: Reusable runtime, config loader and bootstrap for KODA-ecosystem A2A agents on Amazon Bedrock AgentCore.
Project-URL: Homepage, https://bitbucket.org/kushki/koda-agent-runtime
Project-URL: Repository, https://bitbucket.org/kushki/koda-agent-runtime
Author: Kushki
License-Expression: MIT
License-File: LICENSE
Keywords: a2a,agent,agentcore,bedrock,koda,strands
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.110.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: bootstrap
Requires-Dist: bedrock-agentcore>=0.1.0; extra == 'bootstrap'
Requires-Dist: boto3>=1.35.0; extra == 'bootstrap'
Requires-Dist: strands-agents[a2a,anthropic]>=1.0.0; extra == 'bootstrap'
Requires-Dist: uvicorn>=0.30.0; extra == 'bootstrap'
Provides-Extra: cdk
Requires-Dist: aws-cdk-aws-bedrock-agentcore-alpha; extra == 'cdk'
Requires-Dist: aws-cdk-lib>=2.150.0; extra == 'cdk'
Requires-Dist: constructs>=10.0.0; extra == 'cdk'
Description-Content-Type: text/markdown

# koda-agent-runtime

Runtime compartido para agentes Koda A2A sobre Amazon Bedrock AgentCore.
Extrae en un solo paquete versionado las piezas que cada `koda-agent-*`
repite: constantes de custom headers, middleware FastAPI, cliente HTTP
hacia el registry, decoradores de tools, loader del `agent.yaml`, y un
bootstrap `build_agent_app(cfg)` que ensambla Strands + A2A + FastAPI
en una llamada.

Ver `KODA_AGENT_RUNTIME_ARCHITECTURE.md` en el monorepo `koda-platform`
para el diseño completo, precedente (`koda-mcp-access`), roadmap y
compatibilidad.

## Instalación

Elige el `extra` según lo que consuma el agente:

```toml
# El mínimo — headers, middleware, auth, registry client, config loader.
"koda-agent-runtime>=0.2.0,<0.3"

# El agente típico — además Strands + A2A + Bedrock (y AnthropicModel) para
# build_agent_app.
"koda-agent-runtime[bootstrap]>=0.2.0,<0.3"

# El stack CDK del agente — Construct KodaAgentRuntime.
"koda-agent-runtime[cdk]>=0.2.0,<0.3"
```

Mientras 0.2 está en release candidate (`0.2.0rc1`), `>=0.2.0` no la toma
(PEP 440: `0.2.0rc1 < 0.2.0`). Para probarla, fija
`"koda-agent-runtime[bootstrap]>=0.2.0rc1,<0.3"`: acepta la RC y, cuando salga,
toma 0.2.0 sin cambiar el pin. Los agentes que fijan `<0.2` no la toman.

> **0.2 rompe compatibilidad con 0.1.x.** El agente ya no recibe ni reenvía
> bearers: se eliminan `get_bearer`, `set_context`, `KODA_AUTHZ_HEADER` y el
> header `Koda-Authorization` del allowlist. La identidad es un
> `CallerContext` verificado por request (`get_caller()`) y las llamadas al
> registry van firmadas (ver "Auth M2M"). Un agente que se invoca directo por
> `InvokeAgentRuntime`, sin identidad, y no llama a nada puede tomar 0.2 con
> `KODA_AGENT_M2M=off`.

El extra `bootstrap` instala `strands-agents[a2a,anthropic]`, de modo que
`build_agent_app` soporta tanto `provider: bedrock` como `provider: anthropic`
(este último vía `AnthropicModel`, con `ANTHROPIC_API_KEY` en el entorno).

## Uso — un agente en 10 líneas

`src/agent.py`:

```python
from pathlib import Path
import uvicorn
from koda_agent_runtime.config import load_agent_config
from koda_agent_runtime.app import build_agent_app

_BASE = Path(__file__).resolve().parent.parent
app = build_agent_app(load_agent_config(_BASE / "agent.yaml"), base_dir=_BASE)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=9000)
```

`agent.yaml`:

```yaml
name: koda-agent-example
version: 0.1.0
description: |
  Un agente de ejemplo.

model:
  provider: bedrock              # bedrock | anthropic
  model_id: us.amazon.nova-lite-v1:0
  temperature: 0.3               # opcional; OMITIR en modelos que la tienen
                                 # deprecada (Sonnet 5, Opus 4.8) o dan error
  max_tokens: 4000

tools:
  - discover_skills
  - fetch_registry_skill
  - invoke_agent
```

## Módulos

| Módulo | Responsabilidad |
|---|---|
| `koda_agent_runtime.headers` | Constantes `KODA_*_HEADER` + `ALLOWLISTED_HEADERS` (para `agentcore configure --request-header-allowlist`). |
| `koda_agent_runtime.context` | `CallerContext` por request en un `ContextVar`: `get_caller()`, `get_request_id()`, `delegation_claims()`. |
| `koda_agent_runtime.middleware` | `KodaIdentityMiddleware` (ASGI puro, para `app.add_middleware`) y el helper `koda_headers_middleware(app)`. Verifica la firma del registry y arma el `CallerContext` (ver `KODA_AGENT_M2M`). |
| `koda_agent_runtime.auth` | `check_m2m_config`, `IdentityVerifier` (`koda-agent-identity-v1`), `AgentM2MAuth` (`httpx.Auth` que firma `koda-agent-m2m-v1`), `m2m_headers` y el keyring `agent-m2m`. |
| `koda_agent_runtime.registry` | `discover_agents`, `discover_skills`, `fetch_skill_bundle`, `invoke_peer` — hablan con el registry firmando `koda-agent-m2m-v1`. |
| `koda_agent_runtime.tools` | Decoradores `@strands.tools.tool` sobre los helpers de `registry`. `build_tools(cfg)` filtra por lo declarado en `agent.yaml.tools`. |
| `koda_agent_runtime.config` | `load_agent_config(path)` → dataclass tipado `AgentConfig`. |
| `koda_agent_runtime.app` | `build_agent_app(cfg, base_dir)` → `FastAPI` con Strands `Agent` + `A2AServer` + middleware montados. Requiere `[bootstrap]` extra. |

## Contrato de headers Koda-*

koda-proxy (broker del registry) inyecta estos custom headers al invocar
tu runtime. AgentCore solo forwardea headers con prefix
`X-Amzn-Bedrock-AgentCore-Runtime-Custom-` **y** que estén en el allowlist
del runtime: usa `ALLOWLISTED_HEADERS` completo (más los extras de tu agente).

| Header | Contenido | Estable en cadenas A→B→C |
|---|---|---|
| `Koda-Sub` | Usuario humano original. | Sí. |
| `Koda-Email` | Email del usuario. | Sí. |
| `Koda-Roles` | Groups Okta del usuario. | Sí. |
| `Koda-Chain-Depth` | Hops desde el usuario. | +1 por hop. |
| `Koda-Request-Id` | uuid para trace end-to-end. | Sí. |
| `Koda-Agent-Path` | Path del agente que recibe. | N/A. |
| `Koda-Caller-Type` | `user` \| `agent`. | Depende del hop. |
| `Koda-Caller-Agent` | `agent:<name>` del agente que llama, o vacío. | Depende del hop. |
| `Koda-Auth-Groups` | Grupos de autorización, separados por espacio. | Sí. |
| `Koda-Delegation` | Ticket de delegación (JWT opaco; se reenvía, no se verifica). | Sí. |
| `Koda-Signed-At`, `Koda-Key-Id`, `Koda-Signature` | Firma `koda-agent-identity-v1` del registry sobre Sub…Delegation. | Por request. |

Todos los strings viven en `koda_agent_runtime.headers` como constantes.
Nunca los repitas en tu código.

## Auth M2M (0.2.0)

Una sola flag, `KODA_AGENT_M2M`:

| Valor | Entrada | Salida (`registry.py`, `AgentM2MAuth`) | Variables obligatorias |
|---|---|---|---|
| `on` (default, también sin definir) | Verifica la firma `koda-agent-identity-v1` del registry: sin firma válida, 403. `/ping` y `/.well-known/*` no se verifican. | Firma `koda-agent-m2m-v1` como `agent:<KODA_AGENT_NAME>` y reenvía `X-Koda-Delegation`. | `KODA_AGENT_NAME`, `KODA_AGENT_M2M_SECRET_ID` |
| `off` | No verifica; el `CallerContext` queda vacío (`verified=False`). | Falla con `KODA_AGENT_M2M=off: agent is outside the M2M network`; nunca manda una request sin firmar. | Ninguna |

| Variable | Ejemplo | Qué es |
|---|---|---|
| `KODA_AGENT_NAME` | `koda-qa-test-design` | El agente firma y verifica como `agent:<name>`. |
| `KODA_AGENT_M2M_SECRET_ID` | `ai-projects/koda-platform/agent-m2m` | Keyring en Secrets Manager (caché de 300 s, `kid` actual y anterior). El rol del runtime necesita `secretsmanager:GetSecretValue` sobre el secret (y `kms:Decrypt` si usa una CMK). |

Cualquier otro valor de `KODA_AGENT_M2M`, o `on` sin las dos variables, falla
al arrancar: un typo no puede apagar la seguridad. El valor efectivo se loguea
al arrancar. La firma la implementa `koda_agent_runtime._vendor.koda_signing`
(copia de koda-mcp-access 1.8.0); los vectores compartidos están en
`tests/vectors/`.

Para firmar un cliente httpx propio (p. ej. el `MCPClient` de Strands),
construye el `AgentM2MAuth` **dentro de la request**, porque captura la
delegación y el request_id del caller al construirse:

```python
from koda_agent_runtime.auth import AgentM2MAuth

auth = AgentM2MAuth()  # agent:<KODA_AGENT_NAME>, keyring de KODA_AGENT_M2M_SECRET_ID
```

## Anti-patrones

- **No** lances threads propios sin el contexto: un `threading.Thread`
  pelado no ve el `CallerContext`. Usa `asyncio.to_thread` o
  `contextvars.copy_context().run` (Strands >= 1.0 ya lo hace con las tools).
- **No** firmes SigV4 desde el agente para invocar peers. Delega via
  `invoke_peer(<path>, <msg>)` — el registry firma por ti.
- **No** trates un `CallerContext` vacío como un usuario: con
  `KODA_AGENT_M2M=off` (o en `/ping`) no hay identidad. Para decisiones de
  seguridad, exige `get_caller().verified`.

## Desarrollo

```bash
uv sync --all-extras   # los tests importan strands (extra bootstrap)
uv run pytest
uv run ruff check .
uv build
```

Release (manual, no hay CI de publicación):

```bash
# 1. version en pyproject.toml (p. ej. 0.2.0rc1 / 0.2.0) y uv lock
uv lock
uv run pytest && uv run ruff check .
rm -rf dist && uv build && uvx twine check dist/*
# 2. commit, tag vX.Y.Z y push del branch y del tag
# 3. publicar (el token vive en .pypi-token, que NUNCA se versiona)
uv publish --token "$(cat .pypi-token)" dist/*
```

Una versión subida a PyPI no se puede reemplazar (solo yank): verifica antes.

## Versionado

- **v0.x** — alpha, breaking changes tolerados. Todos los agentes en
  migración inicial.
- **v1.0.0** — cuando la API sea estable y >=3 agentes la consuman en
  producción. A partir de acá, SemVer estricto.
