Настройки со скоупами и ожидающая конфигурация

Настройка — это значение, которое должно быть задано в деплое, чтобы модуль, агент, инструмент или хук заработал: API-ключ, имя аккаунта, endpoint. Настройки объявляет их владелец; они хранятся в БД, при желании проецируются в переменные окружения процесса и собираются у человека через неблокирующий диалог в админке.

Это заменяет прежний подход, где каждый потребитель изобретал свой резолв *_env, а единственной пошаговой настройкой был мастер первого запуска для LLM (см. Поверхности-оверлеи). Онбординг остаётся как есть: это блокирующий мастер первого запуска только для LLM. Настройки — общий, всегда доступный и неблокирующий механизм внутри админки.

Понятия

Термин Значение
Объявление (SettingSpec) Что нужно владельцу: id, заголовок, kind, scope, обязательность, UI-подсказки.
Ключ Канонический id {owner}/{id} (напр. github/token, agent:greeter/greeting).
Владелец Модуль или шаблон агента, объявивший настройку.
Скоуп Кому принадлежит значение (см. ниже).
Значение Сохранённая строка для кортежа (key, scope, scope_id).
Ожидает (pending) Обязательная настройка без значения в нужном скоупе, либо рантайм-запрос модуля.

Скоупы и резолв

Значения живут на четырёх уровнях, от общего к частному:

Скоуп Принадлежит Кто может менять
global всему деплою админы с полным скоупом
principal личности админа/оператора (Principal.name) этот принципал / полные админы
agent шаблону агента админы, допущенные к этому шаблону
local_user каноническому конечному пользователю (id из виджета/логина) сам пользователь (самообслуживание)

Потребитель резолвит настройку от самого частного к общему:

local_user → principal → agent → global → объявленная env-переменная → отредактированный конфиг → default

scope объявления — это самый частный поддерживаемый уровень; более общие уровни используются как фолбэк. Резолв повторяет get_resolved хранилища состояния (user → session → agent).

Пример: github/token объявлен scope: global (один ключ на деплой). mycrm/password объявлен scope: local_user — каждый пользователь задаёт свой, а если не задал — используется глобальное значение (если есть).

Объявление настроек

Rust-модули переопределяют Module::settings:

fn settings(&self) -> Vec<agent_os_modules::SettingSpec> {
    vec![agent_os_modules::SettingSpec {
        id: "token".into(),
        title: "GitHub token".into(),
        description: "Personal access token with `repo` scope.".into(),
        kind: SettingKind::Secret,
        scope: SettingScope::Global,
        required: true,
        ..Default::default()
    }]
}

DLL-модули объявляют ту же форму в module.yaml:

id: github
version: 1.0.0
settings:
  - id: token
    title: GitHub token
    kind: secret
    scope: global
    required: true
    env: GITHUB_TOKEN
    trigger: on_login

Поля

Поле Значения Смысл
id string Уникален внутри владельца; ключ — {owner}/{id}.
title / description string Подписи в диалоге; description — markdown.
kind string secret bool int url select Виджет + валидация.
scope global principal agent local_user Самый частный поддерживаемый уровень.
required bool Влияет на подсчёт pending.
default string Значение, показываемое когда не задано (не хранится).
options list Для kind: select.
env string Имя процессной переменной, в которую проецируется значение.
trigger on_login on_use manual Когда диалог её показывает.
enforce advisory block block: потребитель падает со структурной ошибкой, пока не задано.
verify object Необязательная проверка креденшелов (см. ниже).

Модули без Module (declare_module!, DLL)

Модуль, собранный через agent_os_sdk::declare_module! (dual-transport форма, используется портативными/DLL-модулями вроде searchers), не реализует Module::settings(). Он объявляет настройки, контрибутя data-item в известный реестр agent_os_sdk::SETTINGS_DECLARATIONS ("settings.declarations"), которым владеет always-on kernel:

fn contribute(h: &mut dyn agent_os_sdk::RegistryHost) {
    h.stable(agent_os_tool_api::TOOL_ENGINES, /* … */);
    h.data_item(
        agent_os_sdk::SETTINGS_DECLARATIONS,
        &serde_json::json!({
            "owner": "searchers",
            "spec": {
                "id": "firecrawl_api_key",
                "kind": "secret",
                "scope": "global",
                "required": true,
                "env": "FIRECRAWL_API_KEY",
                "trigger": "on_login"
            }
        })
        .to_string(),
    );
}

Это транспорт-нейтрально: один и тот же contribute работает и linked, и в DLL. Хост сливает эти items с любыми из Module::settings().

Хранение

Значения лежат в отдельной DuckDB, logging.settings_db_path (по умолчанию logs/settings.db):

CREATE TABLE settings (
    owner       TEXT NOT NULL,
    setting_id  TEXT NOT NULL,
    scope_kind  TEXT NOT NULL,   -- global | principal | agent | local_user
    scope_id    TEXT NOT NULL,   -- "" для global
    value       TEXT NOT NULL,
    secret      BOOLEAN NOT NULL,
    updated_at  TIMESTAMP NOT NULL,
    updated_by  TEXT NOT NULL,
    PRIMARY KEY (owner, setting_id, scope_kind, scope_id)
);

Проекция в окружение

Когда объявление задаёт env: NAME, значение проецируется в окружение запущенного процесса на старте и при каждой записи (через существующий механизм agent_os.Env). Поэтому существующие инструменты с *_env и скрипты env_var(name) видят новое значение сразу — без перезапуска.

Приоритет (от общего к частному): ручное значение .env / config/env.overrides.yaml побеждает проекцию из настроек (аварийный выход оператора). В админке такая настройка помечается как «перекрыто извне».

Enforcement и проверка

verify:
  kind: http            # none | http | script
  method: GET
  url: "https://api.example.com/me"
  headers: { Authorization: "Bearer {value}" }
  expect_status: 200

{value} подставляется в момент проверки. Результат пишется в verified_at / verify_error и показывается в диалоге. Без verify значение сохраняется без проверки.

Порты и HTTP API

Модули обращаются к настройкам через хост-порт agent_os.Settings (sensitive, capability agent_os.Settings):

declarations()                 -> все спеки (никогда не значения)
status(scope, scope_id)        -> объявления + resolved/pending
get(key, scope, scope_id)      -> резолвленное значение ("" если не задано)
set(key, value, scope, id, actor)
request(spec_json)             -> поднять рантайм-запрос

HTTP-поверхностью владеет фича-модуль settings:

Маршрут Доступ Назначение
GET /v1/settings admin объявления + статус вызывающего (фильтр по auth-скоупу)
GET /v1/settings/pending admin незаданные обязательные + рантайм-запросы
POST /v1/settings admin { key, value, scope, scope_id }
POST /v1/settings/request admin поднять рантайм-запрос
GET/POST /v1/settings/me user самообслуживание конечного пользователя (local_user)

Админ-UI

Модуль settings отдаёт /settings и /ui/settings.js и добавляет пункт Настройки в меню админки. После успешного входа страница логина опрашивает /v1/settings/pending и показывает закрываемый диалог; закрытие запоминается на сессию браузера, и подсказка возвращается при следующем входе. Навигацию ничего не блокирует: /settings доступна и напрямую.

Релевантность настроек

Страница и диалог показывают приоритетно то, что реально используется:

GET /v1/settings возвращает оба флага; GET /v1/settings/pending фильтрует по relevant != false, поэтому неиспользуемые интеграции не «надоедают» при входе. Соответствие kind → модуль берётся из реестра tool.engines; используемые виды инструментов — из tool-конфигов агентов.

Пример: токен GitHub

  1. Модуль github объявляет github/token (secret, global, required, env: GITHUB_TOKEN, trigger: on_login).
  2. На старте хост видит объявление без значения → pending.
  3. Админ входит; оболочка показывает диалог.
  4. Значение сохраняется в settings.db, проецируется в GITHUB_TOKEN в окружении процесса и пишется в аудит.
  5. Инструмент с token_env: GITHUB_TOKEN работает со следующего вызова, без перезапуска.
  6. Следующий вход подсказку не показывает.

Первые потребители

Модули google и yandex объявляют oauth_client_id / oauth_client_secret (глобальные секреты, проецируются в GOOGLE_OAUTH_CLIENT_SECRET / YANDEX_OAUTH_CLIENT_SECRET). Конфиги инструментов уже читают эти переменные, поэтому деплой, где они заданы в .env, подсказок не видит; свежий деплой спросит один раз при входе админа. Так как они резолвятся из окружения, при заданном ключе они не надоедают.

Модуль searchers объявляет firecrawl_api_key (через реестровый канал выше — он макрос-генерируемый), проецируется в FIRECRAWL_API_KEY с trigger: on_login — виден в диалоге после входа и на странице настроек, а значение проецируется в процесс без перезапуска.

Фазы реализации

  1. Global — модель, реестр, БД, /v1/settings*, админ-диалог и страница.
  2. Скоупы Principal и Agent, фильтрация по auth-скоупу.
  3. Скоуп Local_user (шифрование) и самообслуживание в виджете.
  4. Рантайм-запросы, enforce/verify, select, настройки, объявляемые агентами.

Связь с онбордингом

Онбординг (Поверхности-оверлеи) — блокирующий мастер первого запуска только для провайдера LLM. Настройки — отдельный, неблокирующий, общий механизм внутри админки. Они не перекрывают маршруты и не могут закрыть оператору доступ к конфигурации.