O.G.A.S. English

Обмен с ядром

Как части платформы говорят друг с другом: сокет и строки JSON-RPC, обёртка сообщения, знакомство функции с ядром, методы ядра и события. Нужен тому, кто пишет функцию платформы или разбирается в её устройстве.

Введение

Договор обмена между частями платформы: сокет Unix внутри машины и соединение мастерской по 127.0.0.1. Наружу платформа ничего не открывает. Версия договора — 1. Каналы и пересылка — Устройство платформы, 3.3, 3.7, форматы файлов — Форматы файлов.

Методы, чьё имя начинается на core., обрабатывает только ядро. Остальные принадлежат той функции, которая объявила их при знакомстве.

Договор разделён по хозяевам. Настоящая часть содержит общее для всех: провод, обёртку сообщения, знакомство с ядром, методы ядра и события. Остальные части договора:

Таблица 1 — Введение

Файл Что в нём
Методы графа методы функции графа: процесс, программа, прогон, переменные, вопросы
Методы модулей договор с модулем и методы функции модулей
Методы мастерской и пульта данные мастерской, методы окна, пульт
Методы служб платформы настройки, локализация, наблюдаемость, источники, вход для программ, помощник
Коды ошибок коды ошибок: кто бросает и когда

1 Область действия

  1. функция ↔ ядро — Unix-сокет data/run/ogas.sock;
  2. функция модулей ↔ модуль — stdin / stdout модуля, которые ядро передаёт как байты;
  3. мастерская ↔ функция ui — TCP 127.0.0.1, дальше через ядро к функциям (Методы мастерской и пульта, раздел 2).

Функции не вызывают код друг друга — только методы через ядро. Модуль к сокету ядра не подключается.

2 Передача сообщений

JSON-RPC 2.0 задаёт вид сообщения, но не способ передачи и не границу между сообщениями. Границу платформа задаёт сама: одно сообщение — одна строка (JSON Lines).

Если строку не удалось разобрать, канал закрывается — но сначала, если ответ ещё можно отправить, уходит Parse error (-32700, id: null). Само ядро при этом продолжает работать.

Коды JSON-RPC -32700, -32600, -32601, -32602 и -32603 значат то же, что в спецификации. Ошибки по делу отдаются кодом -32000 и полем data.error_code (Коды ошибок).

Длина очередей ограничена настройкой core.rpc_queue. Запрос, пришедший в переполненную очередь, получает отказ backpressure; уведомления в такой момент могут склеиваться или отбрасываться. Вывод модуля в stdout не отбрасывается никогда: если он не успевает читаться, шаг завершается ошибкой transport_overflow. От stderr хранится только конец, ограниченной длины.

3 Общая обёртка сообщения

Запрос:

{
  "jsonrpc": "2.0",
  "id": "graph-17",
  "method": "core.process.spawn",
  "params": {
    "command_id": "cmd-17",
    "command": "python3",
    "args": ["main.py"],
    "cwd": "modules/echo"
  }
}

Успех:

{
  "jsonrpc": "2.0",
  "id": "graph-17",
  "result": {
    "process_handle": "proc-1",
    "pid": 1234,
    "state": "running"
  }
}

Ошибка:

{
  "jsonrpc": "2.0",
  "id": "graph-17",
  "error": {
    "code": -32000,
    "message": "process spawn failed",
    "data": {
      "error_code": "process_spawn_failed",
      "category": "runtime",
      "message_key": "error.process_spawn_failed",
      "retryable": false,
      "trace_id": "trace-1"
    }
  }
}

Отправитель вызова. Вызов, который ядро передаёт функции, несёт поле source рядом с params: id функции, чей вызов ядро передаёт, или core, если вызывает само ядро. Ставит его ядро; поле source, записанное самим вызывающим, до хозяина метода не доходит. Хозяин, которому важно, кто вызывает, проверяет source, а не параметры (Защита, раздел 4).

command_id — защита от повторной доставки. Это не транспортный id JSON-RPC.

После аварии гарантия сохраняется только у тех команд, результат которых уже записан на диск. Гарантии выполнения внешнего действия ровно один раз платформа не даёт.

4 Знакомство функции с ядром (handshake)

Ядро передаёт функции одноразовый токен запуска в переменной OGAS_LAUNCH_TOKEN. Функция подключается к сокету из OGAS_SOCKET и первым сообщением отправляет:

{
  "jsonrpc": "2.0",
  "id": "graph-1",
  "method": "function.handshake",
  "params": {
    "launch_token": "...",
    "id": "graph",
    "implementation_version": "1.0.0",
    "contracts": { "core": 1, "graph": 1, "surface": 1 },
    "commands": ["process.run.start", "process.run.stop"],
    "queries": ["process.snapshot", "function.health"],
    "events": ["run.changed"],
    "surfaces": ["graph.editor"],
    "capabilities": []
  }
}

Успех:

{
  "jsonrpc": "2.0",
  "id": "graph-1",
  "result": {
    "status": "ready",
    "function_id": "graph",
    "granted_capabilities": []
  }
}

Когда знакомство отклоняется:

capabilities — необязательный список прав; незнакомые имена не читаются. Право, которое функция просит, должно быть перечислено в её записи реестра, иначе — capability_denied. Ответ содержит granted_capabilities. Право process нужно для core.process.spawn, core.process.stdin.write, core.process.stop и core.process.kill; без него эти методы отвечают capability_denied.

locales — необязательный список папок со словарями, пути относительные. Ядро их запоминает и отдаёт по core.locales, а сами файлы не читает. Один сегмент пути может быть *: такая папка означает каждую папку этого места внутри папки установки, где есть остаток пути, — ядро находит их заново при каждом core.locales и пропускает скрытые (имя начинается с точки). Так функция модулей объявляет modules/*/locales, и тексты модуля, положенного в папку руками, созданного или добавленного импортом, видны без перезапуска и без нового объявления.

5 Методы ядра

Все они, кроме знакомства, вызываются функцией на уже принятом соединении.

5.1 core.status

Снимок здоровья ядра и функций (не модулей). Обязательных параметров нет:

{
  "core": {
    "pid": 12,
    "started_at": "2026-08-30T12:00:00Z",
    "data_dir": "/home/user/Concept/core/data",
    "functions_ready": 6,
    "functions_failed": 0
  },
  "metrics": {
    "rpc_requests": 12,
    "rpc_errors": 0,
    "health_failures": 0,
    "function_restarts": 0,
    "document_writes": 3,
    "processes_spawned": 1
  },
  "functions": [
    {
      "id": "graph",
      "interactive": false,
      "state": "ready",
      "pid": 34,
      "restart_count": 0,
      "last_health_at": "2026-08-30T12:00:05Z",
      "last_error": null,
      "last_error_code": null,
      "last_error_data": null
    }
  ]
}

interactive — признак из описания функции: ей нужна мастерская. По нему другие части отличают функции окна, не зная их имён. state: starting | ready | failed | restarting. О последнем отказе функции — last_error (сообщение для журнала), last_error_code (код, его текст для человека — error.<код> в словаре ядра) и last_error_data (что код называет: при function_dependency_unavailable — { "dependency" }, недостающая зависимость). Функции, которой нет в реестре или которая в нём выключена, в списке нет. Журнал подробностей: data/logs/core.jsonl.

5.2 core.health

Запрос без обязательных параметров. Ответ:

{ "status": "ok", "contract": 1, "functions": 6, "processes": 2 }

5.3 function.health

Ядро вызывает этот метод у функции, а не наоборот. Ответ — { "status": "ok" } или ошибка. Если ответа нет дольше core.health_timeout_ms, функция считается отказавшей (failed).

5.4 core.process.spawn

Параметры:

Таблица 2 — core.process.spawn

Поле Тип Обязательно Смысл
command_id строка да чтобы узнать повторную доставку
command строка да имя исполняемого файла или путь от папки установки или папки данных
command_base строка нет от чего считается путь command: install (по умолчанию) или data — папка данных
args список строк нет аргументы командной строки
cwd строка нет рабочая папка; по умолчанию папка установки
cwd_base строка нет от чего считается cwd: install (по умолчанию) или data — папка данных; при data папка создаётся, если её нет
env объект «имя → значение» нет дополнительные переменные окружения
limits объект нет пределы группы программ: group — имя группы (маленькие латинские буквы, цифры, _), memory_mb — память всей группы в мегабайтах, cpu_cores — сколько ядер процессора она может занять; 0 — без предела

Ядро сначала очищает окружение, потом добавляет PATH, LANG, LC_ALL и переменные из env. Переменных Python ядро не подставляет — их передаёт функция с правом process. Путь с косой чертой приводится к однозначному виду от своей папки и за её пределы выйти не может. В папке данных папки на пути проверяются по ссылкам, как у любого пути там; сама программа может быть ссылкой наружу — интерпретатор окружения процесса ссылается на Python, из которого сделан, — и должна вести к файлу (Защита, T-03). Ответ: process_handle, pid, state. Хозяин этого process_handle — функция, по чьему соединению пришёл запрос. Нет права process — capability_denied.

Сведений о Python ядро не содержит: интерпретатор и аргументы выбирает функция модулей.

Пределы. С limits ядро ставит программу в группу: пределы держит сам Linux — systemd пользователя, группа экземпляра своя, и все программы одного имени группы делят её память и процессоры. Программу, которая вывела группу за предел памяти, Linux останавливает, и process.io.closed говорит об этом полем limit: "memory". Если systemd пользователя не отвечает, запуск с пределами получает отказ process_limits_unavailable, а без пределов не выполняется: предел, который не держится, не обещается. Работает ли systemd, говорит строка process_limits команды doctor.

5.5 core.process.stdin.write

Параметры: process_handle и line — целая строка JSON, обязательно с \n на конце. Что внутри line, ядро не разбирает. Если ответ на запись потерялся, повторять её модулю, для которого повтор не безобиден, нельзя.

5.6 core.process.stop

Параметры: command_id (обязательно), process_handle. Штатная остановка: SIGTERM через pidfd, ожидание core.process_stop_grace_ms, затем SIGKILL через тот же pidfd, если OS-процесс ещё жив. Повторить остановку безопасно: для уже завершившегося OS-процесса приходит ответ { "state": "exited", "signal_sent": false }. То же у core.process.kill. Нет права process — capability_denied.

5.7 core.process.kill

Параметры: command_id (обязательно) и process_handle. Немедленный SIGKILL через pidfd — после истечения срока ожидания или по аварийной команде.

5.8 core.document.read

Параметры: path от OGAS_DATA_DIR, без ... Ответ — содержимое документа. Если файла нет, читается соседний {name}.bak, когда он есть: значит, запись оборвалась после переименования текущего файла в резервную копию. Если и его нет — document_not_found. Повреждённый текущий файл резервной копией без сообщения не подменяется.

5.9 core.document.list

Параметры: prefix от OGAS_DATA_DIR, без ..; sizes: true — посчитать размер каждой записи. Ответ — { "entries": [{ "name", "path", "directory", "modified_ms", "bytes" }] }: modified_ms — время изменения в миллисекундах от эпохи, bytes — только при sizes, для папки вместе с содержимым, ссылки считаются сами по себе. Если папки нет, список пустой. Смысл самих файлов ядро не разбирает.

5.10 core.document.write

Параметры: command_id (обязательно), path, document. Ядро проверяет путь, UTF-8 и размер (не больше core.document_max_bytes), а если у документа есть format на верхнем уровне — что он выглядит как <имя>/<номер> (document_invalid). Какие версии понятны, решает хозяин документа при чтении (Форматы файлов, раздел 15); ключи настроек с секретными именами отклоняет функция настроек, а ядро проверяет только обёртку. Запись идёт так: временный файл в той же папке, fsync файла, прежний файл переименовывается в .bak (не копируется), временный встаёт на его место, fsync папки. Сбой при записи не уничтожает прежнее поколение: на диске остаётся либо целый текущий файл, либо целая резервная копия.

Документы процесса разрешено писать с отступами для читаемости. Размер считается в байтах записанного UTF-8.

5.11 core.document.remove

Параметры: command_id (обязательно), path. Удаляет файл или папку целиком внутри папки данных. Символьная ссылка удаляется как ссылка, её цель остаётся на месте. Путь обязан состоять хотя бы из двух частей (runs/<run_id>, а не runs), чтобы один вызов не снёс целый раздел; иначе — document_remove_refused. Ответ — { "path", "removed" }; если удалять было нечего, приходит removed: false, а не ошибка.

5.12 core.locales

Отдаёт папки словарей: свою core/kernel/locales и те, которые функции назвали при знакомстве в поле locales; папка с сегментом * раскрыта в папки, которые есть в этот момент (раздел 4). Параметров у запроса нет. Ответ: { "dirs": ["core/kernel/locales", "functions/graph/locales", ...] } — пути считаются от OGAS_INSTALL_DIR, без .. и не от корня. Функция i18n читает только эти папки и свою собственную.

5.13 core.settings.schema

Схема ключей core.* — содержимое файла core/kernel/settings.schema.json (ogas.settings-schema/1). Параметров нет. Спрашивает её функция настроек при запуске; ядро само читает core.* по той же схеме (Настройки, раздел 3).

5.14 core.event.publish

Параметры: command_id (обязательно), event_type, payload. Ядро даёт событию номер event_id и рассылает уведомление event всем функциям. Ответ: { "event_id", "source" }.

5.15 core.event.subscribe

Параметры: last_event_id — последний полученный подписчиком номер или null. Номера растут не подряд: номер получает и событие, посланное одной функции, а видит его только она, — поэтому шаг номера больше единицы потерей не является. Если этот номер ещё есть в памяти последних событий, ядро досылает пропущенное; если нет, отвечает { "resync_required": true }, и подписчик запрашивает состояние заново. Событий process.io.* в этой памяти нет: поток модуля подпиской не восстанавливается.

5.16 core.shutdown

Параметры: command_id (обязательно). Останавливает экземпляр: новые запуски запрещаются, ядро публикует system.stopping и завершает OS-процессы. Ответ — { "status": "stopping" }.

5.17 core.space

Запрос: сколько места занимает платформа. Параметры: fresh — true, если нужен свежий счёт, а не сохранённый на короткое время. Ответ: { used_bytes, budget_bytes, free_bytes, keep_free_bytes, room_bytes, places: [{ path, bytes }] }. Считаются папка данных и папки, которые части назвали ядру (core.space.places); вложенная папка не считается второй раз, файл с несколькими именами (пакеты окружений — ссылки в один кэш) — тоже. budget_bytes — бюджет core.disk_budget_gib, null — без бюджета; free_bytes — сколько ещё вмещает самый тесный диск этих папок; room_bytes — сколько можно занять, не выйдя ни за бюджет, ни за core.disk_free_min_gib. Оба ключа ядро перечитывает при каждом вопросе, поэтому они действуют без перезапуска.

5.18 core.space.check

Запрос: поместится ли новое. Параметры: bytes — размер нового, 0 — размер неизвестен, fresh. Ответ { fits: true, room_bytes }; не помещается — отказ space_budget_exceeded (выйдет за бюджет) или space_disk_full (выйдет за место, оставленное свободным), в данных ошибки needed_bytes и room_bytes. Новое неизвестного размера помещается, пока остаётся хоть сколько-то места. Спрашивают те, кто занимает место: загрузка из источника, установка окружения, начало прогона.

5.19 core.space.places

Команда: папки, в которых часть хранит своё вне папки данных. Параметры: command_id (обязательно), places — список: полный путь или { path, backup }, где backup: true — папку берёт резервная копия; каждый вызов называет все папки части заново. Ядро держит названное и в data/places.json (ogas.places/1), чтобы копию можно было сделать и при остановленной платформе.

6 События о OS-процессе модуля

Уведомление ядра (без id):

{
  "jsonrpc": "2.0",
  "method": "event",
  "params": {
    "event_id": 42,
    "event_type": "process.io.chunk",
    "event_version": 1,
    "time": "2026-08-30T12:00:01.250Z",
    "source": "core",
    "trace_id": null,
    "payload": {
      "process_handle": "proc-1",
      "stream": "stdout",
      "sequence": 0,
      "chunk": "{\"jsonrpc\":\"2.0\",\"id\":\"module-3\",\"result\":{}}\n"
    }
  }
}

Когда закрыты оба потока:

{
  "jsonrpc": "2.0",
  "method": "event",
  "params": {
    "event_id": 43,
    "event_type": "process.io.closed",
    "event_version": 1,
    "time": "2026-08-30T12:00:01.300Z",
    "source": "core",
    "trace_id": null,
    "payload": {
      "process_handle": "proc-1",
      "exit_code": 0,
      "termination": "exited"
    }
  }
}

Поля события, как у всякого события ядра, лежат в payload (раздел 7). termination принимает значения exited (вышел сам) и killed (завершён сигналом); limit: "memory" — его остановил предел памяти его группы (limits, раздел 5.4). OS-процесс, который не запустился, событий не порождает: у него нет process_handle, и отказ приходит ответом на core.process.spawn (process_spawn_failed). После process.io.closed ядро забывает этот OS-процесс, и core.health.processes считает только живые. У stderr свой счётчик sequence. Байты, которые не складываются в UTF-8, в stderr заменяются знаком U+FFFD.

process.io.chunk и process.io.closed ядро отправляет только той функции, которой принадлежит process_handle, и в общую память последних событий не пишет. Остальные события (function.ready, process.started, system.stopping и опубликованные через core.event.publish) получают все подключённые функции.

7 События

Каждое событие ядра: event_id, event_type, event_version, time, source (core или id функции, опубликовавшей событие), trace_id (из payload.trace_id, иначе null), payload. У каждого типа события есть текст для человека event.<тип> в словаре его хозяина — ядра или функции, объявившей событие при знакомстве: им событие называет журнал.

Обязательный набор: system.ready, system.stopping, system.activate, function.ready, function.failed, function.restarted, function.handshake_failed, function.handshake_timeout, process.started, process.io.chunk, process.io.closed, run.changed, node.changed, problem.opened, problem.resolved, logs.removed, sources.download.changed, profile.changed, profile.sending.changed, profile.checked, settings.changed, settings.schema.registered, settings.value_invalid, i18n.key_missing, function.work_failed. (resync_required — не событие, а поле ответа core.event.subscribe, раздел 5.)

system.ready — { functions }: экземпляр поднялся; functions — функции, которые ядро запустило. Функция, подключившаяся позже, получает его при догоняющей подписке (раздел 5).

function.handshake_failed — { error_code, message, function_id? }: ядро отказало соединению в знакомстве. function_id есть, только если соединение назвалось функцией из реестра: слова постороннего соединения на шину не попадают.

function.handshake_timeout — { function_id, error_code }: запущенная функция не познакомилась с ядром за core.function_start_timeout_ms; за ним следует function.failed.

system.activate — { reason: "second_launch" }: мастерскую запустили ещё раз, пока экземпляр работает. Функция окна открывает окно, если его нет (Устройство платформы, 3.6.3).

function.restarted — { function_id, cause, attempt, max, delay_ms, level: "warn" }: ядро перезапускает функцию; cause — почему: function_exited (вышла сама) или health_timeout (не ответила на проверку работоспособности, и ядро её остановило). Предупреждение, а не проблема: исчерпанный бюджет перезапусков даёт function.failed с error_code: function_restart_exhausted.

function.failed — { function_id, error_code, dependency? }: функция не работает и ядро её больше не запускает — исчерпаны перезапуски (function_restart_exhausted), функция вышла или замолчала, а перезапуск ей не положен (function_exited, health_timeout), её канал оборвался (function_lost), не запустилась, не познакомилась вовремя, другой версии договора или осталась без обязательной зависимости (function_dependency_unavailable, тогда dependency называет её). Функция наблюдения открывает по нему проблему с именем функции и причиной.

function.work_failed — { work, error_code, message, … }: функция не завершила работу, ответа на которую никто не ожидает (фоновую работу): выгрузку модулей после прогона, уборку старых прогонов, запись контрольной точки, срабатывание таймера или события по часам, остановку служб и модулей. work — что это была за работа, простыми словами для того, кто разбирает журнал; остальные поля говорят, о чём она была (process_id, run_id, module_id). Опубликовать его может любая функция, объявлять его отдельно ей не нужно. По error_code наблюдение открывает проблему, как по любому событию с кодом (Форматы файлов, раздел 13). Событие, которое ядро не приняло, функция говорит в свой stderr, а ядро держит stderr функций в своём журнале (Устройство платформы, 3.8.2), так что сбой не теряется бесследно.

node.changed — шаг прогона: { run_id, process_id, node_id, execution_id, status }, где status — queued — шаг пришёл к модулю и ждёт своей очереди: места у модуля заняты другими шагами (parallelism манифеста); running — модуль взял шаг; completed, failed или cancelled — шаг оборван остановкой прогона или его группы. Шаг, которому место нашлось сразу, queued не публикует. Публикуется для узлов модулей и для узлов, на которых ветка стоит, — вопроса, ожидания сигнала и паузы (Устройство платформы, 3.8.2): у таких status — waiting, когда ветка встала, затем completed (ветка пошла дальше) или cancelled (ожидание снято остановкой группы или тем, что взят другой пакет круга), а execution_id пуст — попытки шага у ожидания нет. Событие об ожидании публикуется после записи контрольной точки, в которой оно уже есть.

События живой программы (Программа: граф и запуски, разделы 2, 9):

События документов. Каждая команда, которая записывает или удаляет документ, оставляет событие: функция наблюдения пишет его в журнал, и порядок изменений восстанавливается по журналу (Правила разработки, 2.7.3):

8 Совместимость

Если меняется смысл обязательного поля или метода — растёт версия договора contract. Добавление необязательного поля версию не меняет. Незнакомые необязательные поля не читаются.

Договор не обещает ни JSON-RPC 1.0, ни нескольких запросов одним массивом, ни числовых id, ни работы поверх HTTP.

9 Авторизация

Входа по имени и паролю в платформе нет: на машине работает один человек. Функцию к ядру допускают четыре условия: права 0600 на сокете, одноразовый токен запуска, совпадение пользователя и номера процесса на другом конце сокета (SO_PEERCRED) с тем OS-процессом, который ядро запускало, и совпадение объявленного id с записью реестра. Модуль методов ядра не вызывает вообще. process_handle принадлежит только той функции, которая его получила. Окно мастерской пускают адрес 127.0.0.1, токен сессии функции ui и закрытый список методов ядра (Защита, 3.10). Программу человека пускает вход для программ: открытая настройка, адрес 127.0.0.1, ключ входа и закрытый список методов (Методы служб платформы, раздел 5).

10 Ограничения договора