Stipendex

Мандат и кошелёк-ядро

Полномочия (что агенту можно) и компонент, который их enforce'ит (кошелёк). Ключевой принцип Stipendex: LLM не прикасается к деньгам — см. инвариант I1 в overview.md.


Mandate — делегирование полномочий

Подписанный принципалом JSON. Хранится в кошельке; получателям не раскрывается (в Proof уходит только hash).

{
  "stipendex": "mandate/0.1",
  "mandate_id": "m_7c1f…",
  "principal_key": "ed25519:AAAA…",
  "wallet_key": "ed25519:BBBB…",
  "limits": {
    "currency": "USD",
    "per_tx": "1.00",
    "per_day": "20.00",
    "total": "100.00"
  },
  "allow": [
    "registry:4agents/verified",
    "mcp:exa-search",
    "https://api.example.com"
  ],
  "approval_over": "1.00",
  "approval_mode": "async",
  "valid_until": "2026-08-15T00:00:00Z",
  "purpose": "research tools + purchases for project X",
  "sig": "<jws принципала>"
}
Поле Правила
limits все три уровня обязательны; кошелёк ведёт счётчики durable (переживают рестарт)
allow[] источники доверия: прямой id получателя, URL, либо реестр (registry:<провайдер>/<уровень>, см. trust-registry.md). Пустой список = платить нельзя никому
approval_over суммы выше — только с Confirmation; 0 = подтверждать всё
approval_mode interactive | async | deny — см. HITL ниже
valid_until SHOULD: дни, не месяцы — короткий TTL вместо инфраструктуры отзыва; отзыв досрочно — через revocation фасилитатора
purpose человекочитаемое назначение; показывается в каждом запросе подтверждения

Мандатов может быть несколько (по проекту/агенту); кошелёк выбирает по получателю и назначению, при неоднозначности — самый ограничивающий.

Кошелёк-ядро (Wallet Kernel)

Детерминированный компонент при хосте агента (библиотека, сайдкар-процесс или удалённый сервис). Нормативное поведение:

Интерфейс для агента — ровно одна операция

wallet.pay(quote) → { status: "paid",  proof }
                  | { status: "pending_approval", eta }
                  | { status: "denied", reason }

Порядок проверки (MUST, именно в этом порядке)

  1. Подпись и срок Quote; quote_id не израсходован ранее. Клейм quote_id атомарен; при отказе без движения денег он снимается (легитимный повтор возможен) и становится постоянным только после успешного расчёта.
  2. Получатель ∈ allow, default deny. Прямая запись (id/URL) пропускает квоту только если quote.receiver.key совпадает с ключом, которому кошелёк уже доверяет для этого id (пиннинг в конфигурации или однократно полученный дескриптор) — иначе атакующий мог бы выпускать самоподписанные квоты под доверенным именем (T1). Registry-записи проверяются по подписанному провайдером дескриптору.
  3. Лимиты: per_tx → дневной счётчик → total. Для estimate — считается estimate + tolerance (худший случай). Прохождение проверки атомарно резервирует сумму (без окна check-then-record): резерв держится на время HITL-ожидания, освобождается при отказе/сбое и остаётся записью о трате при успехе — конкурентные платежи и очередь подтверждений не могут совместно превысить мандат.
  4. Порог approval_over → HITL (ниже). Checkout-профиль — Confirmation всегда. После долгого интерактивного ожидания кошелёк перепроверяет срок квоты.
  5. Выбор рельса (пересечение quote.rails и настроенных адаптеров; приоритет — настройка кошелька) → расчёт → Proof.

Ключи

HITL — подтверждение человеком

Три режима (approval_mode), единый объект Confirmation на выходе:

Режим Поведение
interactive вопрос принципалу в текущем канале (MCP elicitation / CLI-промпт); ответ ждётся синхронно
async платёж в pending-очередь (у кошелька или фасилитатора) + push принципалу; агенту сразу pending_approval, он продолжает остальную работу; TTL очереди = TTL Quote
deny headless-строгий: всё сверх порога — denied; для cron-агентов без канала к человеку

Confirmation — зафиксированное согласие

{
  "stipendex": "confirmation/0.1",
  "confirmation_id": "c_5e2b…",
  "quote_id": "q_8f3d1c…",
  "mandate_id": "m_7c1f…",
  "approved_total": { "value": "92.40", "currency": "USD" },
  "tolerance_ack": true,
  "channel": "elicitation",
  "approved_at": "2026-07-16T12:03:02Z",
  "sig": "<jws кошелька>",
  "principal_assertion": "<webauthn/passkey assertion | null>"
}

Что кошелёк отдаёт принципалу