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
9 changes: 3 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
# syntax=docker/dockerfile:1.7
FROM ghcr.io/astral-sh/uv:0.8.15@sha256:a5727064a0de127bdb7c9d3c1383f3a9ac307d9f2d8a391edc7896c54289ced0 AS uv
FROM python:3.14-slim@sha256:cae66f2ef0ec51a9891263eeee7f987dacf0a9879e8aa9353d5606e0530619a5 AS build
FROM python:3.14-slim@sha256:cad9a2c871761c413caa6fdd6441c783451e740a48aaeba60ae62a8b53525ef6 AS build
COPY --from=uv /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock README.md ./
RUN uv sync --locked --no-dev --no-install-project
COPY src ./src
RUN uv sync --locked --no-dev

FROM python:3.14-slim@sha256:cae66f2ef0ec51a9891263eeee7f987dacf0a9879e8aa9353d5606e0530619a5
FROM python:3.14-slim@sha256:cad9a2c871761c413caa6fdd6441c783451e740a48aaeba60ae62a8b53525ef6
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \
libssl3t64=3.5.7-1~deb13u2 \
openssl=3.5.7-1~deb13u2 \
openssl-provider-legacy=3.5.7-1~deb13u2 \
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade --yes --no-install-recommends \
&& PIP_ROOT_USER_ACTION=ignore python -m pip uninstall --yes pip \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --system --uid 10001 --create-home app
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
## Что уже есть

- HTTP API на FastAPI;
- Bearer JWT с проверкой подписи, issuer, audience и срока жизни;
- контракт `portable-agent/contracts` версии `2.1.0`;
- простой MVC-подобный каркас;
- локальные demo-адаптеры модели и policy для разработки без внешних сервисов;
- результат с `proposal` или `clarification` для первого действия `calendar.create_event`;
Expand Down Expand Up @@ -36,6 +38,17 @@ uv sync --all-groups
uv run fastapi dev src/portable_agent/main.py
```

Для `POST /api/v1/proposals` нужен Bearer token с audience `agent-runtime`. Идентификаторы tenant и
пользователя сервис берёт из claims `tenant_id` и `sub`; передать или подменить их в JSON нельзя.

Основные переменные окружения:

- `AGENT_OIDC_ISSUER_URL` — issuer токена;
- `AGENT_OIDC_JWKS_URL` — публичные ключи OIDC;
- `AGENT_OIDC_AUDIENCE` — ожидаемый audience, по умолчанию `agent-runtime`;
- `AGENT_ALLOWED_HOSTS` — JSON-массив разрешённых Host;
- `AGENT_DOCS_ENABLED` — включает Swagger только там, где он нужен.

Проверки:

```bash
Expand All @@ -54,7 +67,11 @@ uv run mkdocs build --strict
Сервис принимает только `calendar.create_event`. Для готового предложения нужны `title`, `startAt`,
`endAt` и `timeZone`; исполнитель первой версии называется `fake-calendar`. Если полей не хватает,
ответ содержит `clarification`, а `proposal` остаётся `null`. Готовое предложение всегда содержит
`requires_approval: true`.
`requiresApproval: true`.

Внешний запрос использует поля `text`, `timeZone` и `availableConnectors`. Проверенная копия схемы
лежит в `contracts/agent-runtime-api.yaml`; безопасное обновление выполняет
`scripts/update-contract.ps1`.

Локальная demo-модель не понимает свободную речь. Для полного сквозного теста используй точный
формат:
Expand Down
1 change: 1 addition & 0 deletions catalog-info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ metadata:
annotations:
github.com/project-slug: portable-agent/agent-runtime
backstage.io/techdocs-ref: dir:.
portable-agent.dev/api-contract: agent-runtime-api@2.1.0
spec:
type: service
lifecycle: experimental
Expand Down
13 changes: 13 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Контракт Agent Runtime

`agent-runtime-api.yaml` — проверенная копия API из релиза
`portable-agent/contracts` версии `2.1.0`.

Обновляй файл только командой:

```powershell
.\scripts\update-contract.ps1 -Version 2.1.0
```

Скрипт проверяет SHA-256 и GitHub attestation релизного архива до замены файла.
Тест `test_contract.py` проверяет реальные запросы и ответы сервиса по этой схеме.
170 changes: 170 additions & 0 deletions contracts/agent-runtime-api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
openapi: 3.1.0
info:
title: Portable Agent Runtime API
version: 2.1.0
description: Подготовка предложения действия из текста пользователя.
license:
name: Apache-2.0
identifier: Apache-2.0
servers:
- url: https://api.portable-agent.dev/agent-runtime
security:
- bearerAuth: []
paths:
/api/v1/proposals:
post:
tags: [proposals]
operationId: createProposal
summary: Подготовить предложение действия
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProposalRequest"
responses:
"200":
description: Готовое предложение или вопрос для уточнения
content:
application/json:
schema:
$ref: "#/components/schemas/ProposalResponse"
"401":
description: JWT отсутствует или не подходит Agent Runtime
"422":
description: Запрос не соответствует контракту
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
ProposalRequest:
type: object
additionalProperties: false
required: [text, context]
properties:
text:
type: string
minLength: 1
maxLength: 10000
context:
$ref: "#/components/schemas/UserContext"
UserContext:
type: object
additionalProperties: false
properties:
locale:
type: string
minLength: 2
maxLength: 16
default: ru-RU
timeZone:
type: string
minLength: 1
maxLength: 100
default: Europe/Moscow
pattern: "^(UTC|[A-Za-z_]+(?:/[A-Za-z0-9_+-]+)+)$"
availableConnectors:
type: array
uniqueItems: true
items:
type: string
enum: [fake-calendar]
default: []
ProposalResponse:
type: object
additionalProperties: false
required: [proposal, clarification]
properties:
proposal:
anyOf:
- $ref: "#/components/schemas/ActionPlan"
- type: "null"
clarification:
anyOf:
- $ref: "#/components/schemas/Clarification"
- type: "null"
ActionPlan:
type: object
additionalProperties: false
required:
[
proposalId,
kind,
connector,
payload,
explanation,
risk,
requiresApproval,
]
properties:
proposalId: { type: string, format: uuid }
kind:
type: string
enum: [calendar.create_event]
connector:
type: string
enum: [fake-calendar]
payload:
$ref: "#/components/schemas/CalendarCreateEventPayload"
explanation:
type: string
minLength: 1
maxLength: 500
risk:
type: string
enum: [LOW, MEDIUM, HIGH]
requiresApproval:
type: boolean
const: true
Clarification:
type: object
additionalProperties: false
required: [question, missingFields]
properties:
question:
type: string
minLength: 1
maxLength: 500
missingFields:
type: array
uniqueItems: true
items:
type: string
enum: [title, startAt, endAt, timeZone]
CalendarCreateEventPayload:
type: object
additionalProperties: false
required: [title, startAt, endAt, timeZone]
properties:
title:
type: string
minLength: 1
maxLength: 200
pattern: ".*\\S.*"
startAt: { type: string, format: date-time }
endAt: { type: string, format: date-time }
timeZone:
type: string
maxLength: 100
pattern: "^(UTC|[A-Za-z_]+(?:/[A-Za-z0-9_+-]+)+)$"
description: { type: string, maxLength: 2000 }
attendees:
type: array
maxItems: 50
uniqueItems: true
items: { type: string, format: email, maxLength: 254 }
Problem:
type: object
additionalProperties: true
required: [detail]
properties:
detail: { type: array }
20 changes: 15 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
```mermaid
sequenceDiagram
participant Client as Клиент
participant OIDC as OIDC/JWKS
participant Controller as Controller
participant Service as ProposalService
participant Model as ModelRepository
participant Policy as PolicyRepository

Client->>Controller: POST /api/v1/proposals
Client->>Controller: POST /api/v1/proposals + Bearer JWT
Controller->>OIDC: проверить подпись, issuer, audience, exp
OIDC-->>Controller: tenant_id и sub
Controller->>Service: propose(text, context)
Service->>Model: propose(text, context)
Model-->>Service: ModelReply или null
Expand Down Expand Up @@ -53,8 +56,15 @@ config собирает реализации; main подключает controll

Одновременно заполнена только одна часть.

## Внешние контракты
## Доверенная граница

HTTP-путь и старые имена полей (`utterance`, `actor_id`, `available_connectors`) сохранены для
совместимости с репозиторием `contracts`. Внутри используются более простые имена `text`, `user_id`
и `available_tools`; преобразование находится в HTTP-схеме.
Клиент может передать только текст, язык, часовой пояс и доступные коннекторы. `tenant_id` и `user_id`
создаются из проверенных claims `tenant_id` и `sub`, поэтому поля JSON не могут подменить владельца
действия. JWT принимается только с алгоритмом `RS256`, правильными `issuer`, audience
`agent-runtime`, `exp` и `iat`.

## Внешний контракт

HTTP API следует `portable-agent/contracts` версии `2.1.0`: `text`, `timeZone`,
`availableConnectors`, `proposalId`, `requiresApproval` и `missingFields`. Копия релизной схемы
лежит в `contracts/`; contract-тест проверяет по ней настоящий запрос и ответ.
11 changes: 11 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ uv run fastapi dev src/portable_agent/main.py
Swagger UI доступен по адресу `http://127.0.0.1:8000/docs`, health check —
`http://127.0.0.1:8000/health/live`.

Swagger предназначен для локальной разработки. В production выставляй
`AGENT_DOCS_ENABLED=false` и явно задавай `AGENT_ALLOWED_HOSTS`.

## TDD

Работа идёт коротким циклом:
Expand All @@ -36,3 +39,11 @@ uv run mypy src tests
uv run pytest
uv run mkdocs build --strict
```

При обновлении общего API сначала выпусти релиз в `portable-agent/contracts`, затем выполни:

```powershell
.\scripts\update-contract.ps1 -Version 2.1.0
```

Ручное копирование схемы запрещено: команда проверяет checksum и GitHub attestation.
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Agent Runtime — stateless-сервис, который превращает т
Сервис отвечает за:

- приём текста и контекста;
- проверку Bearer JWT и получение tenant/user только из токена;
- вызов внешней AI-модели через repository-адаптер;
- получение уровня риска через policy-адаптер;
- создание типизированного предложения;
Expand All @@ -31,6 +32,9 @@ Agent Runtime — stateless-сервис, который превращает т
могут быть `null`, если модель не нашла действие или коннектор недоступен. Создание встречи всегда
требует явного подтверждения в `action-service`.

Публичный API соответствует `portable-agent/contracts` версии `2.1.0`. Поля называются `text`,
`timeZone`, `availableConnectors`, `proposalId`, `requiresApproval` и `missingFields`.

Перед созданием предложения сервис проверяет payload: типы и длину полей, формат времени и часового
пояса, отсутствие лишних полей и правило `endAt > startAt`. Локальная demo-модель поддерживает один
строгий формат полной команды, описанный в README; это тестовый путь, а не замена AI-модели.
9 changes: 8 additions & 1 deletion docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ GET /health/live
2. Обнови окружение: `uv sync --locked --all-groups`.
3. Запусти проверки из `docs/development.md`.
4. Проверь, что порт 8080 свободен при запуске контейнера.
5. Проверь `AGENT_OIDC_ISSUER_URL`, `AGENT_OIDC_JWKS_URL` и доступность JWKS из контейнера.

## API возвращает 401

Проверь, что заголовок имеет вид `Authorization: Bearer <token>`. Токен должен быть подписан ключом
из настроенного JWKS, иметь правильный issuer, audience `agent-runtime`, актуальный `exp`, а также
UUID в claims `tenant_id` и `sub`.

## API возвращает null proposal

Это допустимо, если модель не предложила действие или нужного коннектора нет в
`available_connectors`. В текущем каркасе работает demo-модель с ограниченным поведением.
`availableConnectors`. В текущем каркасе работает demo-модель с ограниченным поведением.

## Внешняя модель или policy недоступны

Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@ requires-python = ">=3.14"
license = "Apache-2.0"
dependencies = [
"fastapi==0.141.1",
"PyJWT[crypto]>=2.13,<3",
"pydantic-settings>=2.10,<3",
"uvicorn[standard]>=0.35,<1",
]

[dependency-groups]
dev = [
"httpx>=0.28,<1",
"jsonschema[format]>=4.25,<5",
"mkdocs>=1.6,<2",
"mkdocs-material>=9.6,<10",
"mypy>=1.17,<2",
"pytest>=8.4,<9",
"pytest-asyncio>=1.1,<2",
"pytest-cov>=6.2,<8",
"PyYAML>=6.0,<7",
"ruff>=0.12,<1",
"types-jsonschema>=4.25,<5",
"types-PyYAML>=6.0,<7",
]

[build-system]
Expand Down
Loading