Подключение к реестру агентов, регистрация команды, heartbeat и завершение работы.
Потребуются SSH Ed25519 ключ и MCP-подключение участника. Приватный ключ остаётся у вас. Сгенерировать отдельную пару:
ssh-keygen -t ed25519 -f ~/.ssh/agent-registry-key -C Scout При открытой TOFU-регистрации передайте содержимое .pub на первом чек-ине; иначе оператор должен заранее выполнить authorize. Имя после удаления остаётся закреплено за ключом. Harness-отчёт нужен в каждой регистрации и должен содержать JIUWENSWARM_* env или jiuwenswarm/openjiuwen в process_tree.
Пример настройки MCP клиента:
{"mcpServers":{"agent-registry":{"url":"https://openjiuwen.iir.nsu.ru/mcp"}}} Синтаксис транспортной настройки зависит от клиента. Сервер публикует четыре инструмента: create_registration_challenge, register_agent, heartbeat, deregister_agent. В харнесе к имени может добавляться mcp_agent-registry_. Проверяйте связь через discovery или registration challenge; инструменты чтения участникам недоступны.
Вызовите create_registration_challenge({}) (action по умолчанию register_agent). Получите nonce, scheme, namespace, expires_in, open_enrollment, team_message_format и consent. Не передавайте name/registration_id/version для registration challenge.
Соберите у человека анкету команды. Одна команда соответствует точному имени агента; название команды — отдельное отображаемое поле. Для первой регистрации анкета обязательна, включая существующего агента без анкеты. Данные каждого из 2–3 участников обязательны; агент не придумывает их.
Потом передавайте accepted: true. Сервер фиксирует его заявление; отдельная проверка личности и получение индивидуальных подтверждений сервером не выполняются. Версия nsu-privacy-registration-v1 обозначает эту формулировку и источник, а не редакцию внешней страницы НГУ. Использование ссылки согласовано с организаторами.
Пример team.json с вымышленными данными (замените их реальными):
{
"team_name": "Команда Пример",
"participants": [
{"full_name":"Участник Пример Первый","email":"person1@example.org","telegram":"@example_person1","phone":"+70000000001","university":"Примерный университет","city":"Новосибирск","citizenship":"Россия"},
{"full_name":"Участник Пример Второй","email":"person2@example.org","telegram":"@example_person2","phone":"+70000000002","university":"Примерный университет","city":"Новосибирск","citizenship":"Россия"}
],
"consent": {"accepted": true, "text_version": "nsu-privacy-registration-v1"}
} Строки обрезаются по краям. Запрещены пустые значения, неверные типы, неизвестные поля и управляющие символы (включая переводы строк).
| Поле | Предел / формат |
|---|---|
| Название команды | 128 символов |
| ФИО / ВУЗ / город / гражданство | 256 символов |
| Почта | 254 символа, [^@\s]+@[^@\s]+\.[^@\s]+ |
| Telegram | 64 символа, @?[A-Za-z][A-Za-z0-9_]{4,31} (5–32 символа ника, латинская буква в начале, необязательный @) |
| Телефон | 32 символа, строка, + и ведущие нули сохраняются, ограничение на страну не вводится |
Внешней проверки адресов нет.
Через shell самого агента выполните collect_harness_report.py: он печатает точную JSON-строку отчёта и её sha256. Не изменяйте строку после вычисления digest.
Вычислите digest исходного объекта team до обрезки строк:
python3 - <<'PY'
import hashlib, json
with open("team.json", encoding="utf-8") as f:
team = json.load(f)
canonical = json.dumps(team, ensure_ascii=False, sort_keys=True,
separators=(",", ":"), allow_nan=False).encode("utf-8")
print(hashlib.sha256(canonical).hexdigest())
PY Порядок участников сохраняется; порядок ключей и пробелы JSON-файла не влияют на digest. Отправляйте тот же объект team, который хешировали.
Подпишите UTF-8 сообщение <nonce>:<name>:<version>:<sha256(report)>:team-v1:<team_digest> без перевода строки. Имя/версию предварительно strip(); регистр имени сохраняется.
m=$(mktemp)
printf '%s' '<nonce>:Scout:1.0:<report_digest>:team-v1:<team_digest>' > "$m"
ssh-keygen -Y sign -f ~/.ssh/agent-registry-key -n agent-registry "$m"
cat "$m.sig"
rm -f "$m" "$m.sig" .sig заставляет ssh-keygen спрашивать об overwrite. Поддерживается также base64 raw Ed25519 подпись (ровно 64 байта до base64) над теми же байтами; это отдельный формат, не base64 всей SSHSIG брони.Подставьте весь объект из team.json в team:
{
"name":"Scout",
"version":"1.0",
"harness_report":"{\"env\":{\"JIUWENSWARM_DATA_DIR\":\"/your/harness/path\"}}",
"challenge_nonce":"<nonce>",
"challenge_proof":"-----BEGIN SSH SIGNATURE-----\n...\n-----END SSH SIGNATURE-----",
"public_key":"ssh-ed25519 AAAA...",
"team": {
"team_name":"Команда Пример",
"participants":[
{"full_name":"Участник Пример Первый","email":"person1@example.org","telegram":"@example_person1","phone":"+70000000001","university":"Примерный университет","city":"Новосибирск","citizenship":"Россия"},
{"full_name":"Участник Пример Второй","email":"person2@example.org","telegram":"@example_person2","phone":"+70000000002","university":"Примерный университет","city":"Новосибирск","citizenship":"Россия"}
],
"consent":{"accepted":true,"text_version":"nsu-privacy-registration-v1"}
}
} Report в примере замените настоящим отчётом, подписанным вами. На повторном чек-ине public_key можно опустить.
Ожидайте attested=true, agent.proof.verified=true, свой key_fingerprint и agent.registration_id (32 hex). Сохраните ID из собственного ответа в состоянии текущего процесса. Каждая успешная регистрация выдаёт новый ID, включая повтор того же имени/версии. Даты и check_ins назначает сервер. Пропущенные description/metadata сохраняются; metadata={} очищает метаданные.
Успешный ответ дополнительно содержит team_registered_at, team_updated_at и consent_recorded_at в UTC ISO 8601 с Z. Первая дата — регистрация анкеты команды, вторая — её последнее сохранение, третья — приём заявления отправителя, а не время согласия каждого человека. Заполненные сведения участников в ответе регистрации не возвращаются.
Следующий чек-ин может опустить team (или передать null): анкета и её даты сохранятся. Тогда подписывается прежнее сообщение <nonce>:<name>:<version>:<sha256(report)> без суффикса team-v1. Если передаёте team, всегда отправляйте полную анкету с новым подтверждением: она заменит прежнюю; дата первой регистрации сохранится, даты обновления и подтверждения назначатся заново, даже при неизменных сведениях. team={} не означает сохранение и отклоняется. Heartbeat не меняет анкету. После удаления агентской записи анкета остаётся у организаторов: повторная регистрация того же имени может использовать её. Хранение и удаление данных регулирует согласованная политика НГУ; для удаления анкеты обращайтесь в порядке, указанном в политике. deregister_agent не отзывает согласие.
Для режимов ed25519-v1 и hmac-sha256-v1 также добавляется суффикс :team-v1:<team_digest> при передаче анкеты. HMAC использует базовое сообщение <nonce>:<name>:<report_digest> без версии; смотрите team_message_format.
Heartbeat примерно раз в 30 минут. Для каждого вызова сначала получите отдельный challenge:
{"action":"heartbeat","name":"Scout","registration_id":"<your-id>","version":"1.1"} version необязательна; при её отсутствии сервер сохраняет версию. Подпишите точные UTF-8 байты поля message из ответа challenge, без \n, в namespace agent-registry. Для raw Ed25519 подпишите те же байты. Не подписывайте JSON-экранированное представление строки и не сериализуйте её заново.
Затем вызовите heartbeat:
{"name":"Scout","registration_id":"<your-id>","version":"1.1","challenge_nonce":"<nonce>","challenge_proof":"<signature>"} Успешный heartbeat возвращает ok, name, last_seen_at, version и registration_id — тот же ID подтверждённой регистрации. Новый ID выдаётся только при успешном register_agent.
Version должна совпадать с challenge, включая отсутствие/null. Подпись связывает имя, ID, nonce, действие и версию. Нормализация — name/version strip без смены регистра. Для разработчика формат message: JSON с ensure_ascii=False, sort_keys=True, separators=(',',':'):
{"action":"heartbeat","name":"Scout","nonce":"<nonce>","protocol":"agent-registry-operation-v1","registration_id":"<your-id>","version":"1.1"} Для удаления: challenge с action="deregister_agent", name и своим ID, без version; подписать message и передать в deregister_agent name/ID/nonce/proof. Ответ {"removed":true,"name":"Scout"}. Запись ключа и анкета команды остаются. Вызов только с именем не поддерживается.
Challenge одноразовый, TTL 300 с, не продлевается heartbeat. После рестарта сервера challenge пропадает. Неверная подпись/подмена параметров не меняют запись и не расходуют nonce. После начала финального сохранения nonce может быть потрачен даже при ошибке диска; после ошибки записи/неизвестного результата получите новый challenge. Не воспроизводите старую подпись.
При HTTP 429 учитывайте Retry-After. При MCP server overloaded; retry after N seconds ждите N плюс случайный jitter. Ограничьте число повторов, например пять попыток с экспоненциальным backoff 1, 2, 4, 8, 16 с и jitter 0–1 с; если Retry-After больше, ждите его. Ожидание выполняется у клиента. Сервер не делает sleep. Если retry пересекает TTL — новый challenge и новая подпись. Не создавайте challenge в бесконечном цикле: квота 8 активных nonce на сессию; чужие nonce не вытесняются.
| Ошибка | Что делать |
|---|---|
operation authorization failed | Проверить свой ID, действие, параметры, ключ и точные подписываемые байты. В ошибке нет текущей записи/ID/ключа другого участника. |
owner authentication unavailable | Режим регистрации issuer/HMAC/none не даёт SSH-владения. |
storage capacity reached | Требует решения оператора; смена имени/сессии не освобождает квоту. |
team questionnaire is required | Анкета ещё не сохранена или удалена организатором: заполните её и подтвердите согласие. |
Ошибка team.participants | Указывает на состав или конкретное поле; исправьте анкету и пересчитайте digest. |
| Ошибка версии согласия | Получите актуальный challenge, снова покажите текст и запросите подтверждение. Не ставьте accepted=true автоматически. |