@agfpd/iapeer 0.2.97 → 0.2.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/01-overview.md +86 -0
- package/docs/02-quickstart.md +110 -0
- package/docs/03-peers-runtimes-identity.md +117 -0
- package/docs/04-lifecycle-and-daemon.md +101 -0
- package/docs/05-system-prompt.md +82 -0
- package/docs/06-storage.md +115 -0
- package/docs/07-install-and-update.md +87 -0
- package/docs/08-peer-management.md +119 -0
- package/docs/09-runtimes.md +103 -0
- package/docs/10-messaging.md +69 -0
- package/docs/11-extensions-and-memory.md +60 -0
- package/docs/12-cli-reference.md +96 -0
- package/docs/13-architecture-and-contract.md +190 -0
- package/docs/14-configuration-and-logs.md +91 -0
- package/docs/README.md +97 -0
- package/docs/ru/01-/320/276/320/261/320/267/320/276/321/200.md +86 -0
- package/docs/ru/02-/320/261/321/213/321/201/321/202/321/200/321/213/320/271-/321/201/321/202/320/260/321/200/321/202.md +110 -0
- package/docs/ru/03-/320/277/320/270/321/200/321/213-/321/200/320/260/320/275/321/202/320/260/320/271/320/274/321/213-/320/270/320/264/320/265/320/275/321/202/320/270/321/207/320/275/320/276/321/201/321/202/321/214.md +117 -0
- package/docs/ru/04-/320/266/320/270/320/267/320/275/320/265/320/275/320/275/321/213/320/271-/321/206/320/270/320/272/320/273-/320/270-/320/264/320/265/320/274/320/276/320/275.md +101 -0
- package/docs/ru/05-/321/201/320/270/321/201/321/202/320/265/320/274/320/275/321/213/320/271-/320/277/321/200/320/276/320/274/320/277/321/202.md +82 -0
- package/docs/ru/06-/321/205/321/200/320/260/320/275/320/265/320/275/320/270/320/265.md +115 -0
- package/docs/ru/07-/321/203/321/201/321/202/320/260/320/275/320/276/320/262/320/272/320/260-/320/270-/320/276/320/261/320/275/320/276/320/262/320/273/320/265/320/275/320/270/320/265.md +87 -0
- package/docs/ru/08-/321/203/320/277/321/200/320/260/320/262/320/273/320/265/320/275/320/270/320/265-/320/277/320/270/321/200/320/260/320/274/320/270.md +119 -0
- package/docs/ru/09-/321/200/320/260/320/275/321/202/320/260/320/271/320/274/321/213.md +103 -0
- package/docs/ru/10-/320/276/320/261/320/274/320/265/320/275-/321/201/320/276/320/276/320/261/321/211/320/265/320/275/320/270/321/217/320/274/320/270.md +69 -0
- package/docs/ru/11-/321/200/320/260/321/201/321/210/320/270/321/200/320/265/320/275/320/270/321/217-/320/270-/320/277/320/260/320/274/321/217/321/202/321/214.md +60 -0
- package/docs/ru/12-cli-reference.md +96 -0
- package/docs/ru/13-/320/260/321/200/321/205/320/270/321/202/320/265/320/272/321/202/321/203/321/200/320/260-/320/270-/320/272/320/276/320/275/321/202/321/200/320/260/320/272/321/202.md +190 -0
- package/docs/ru/14-/320/272/320/276/320/275/321/204/320/270/320/263/321/203/321/200/320/260/321/206/320/270/321/217-/320/270-/320/266/321/203/321/200/320/275/320/260/320/273/321/213.md +91 -0
- package/docs/ru/README.md +97 -0
- package/package.json +4 -2
- package/src/cli/index.ts +18 -5
- package/src/launch/adapters/claude.ts +16 -1
- package/src/onboard/index.ts +111 -14
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# 13 — Архитектура и контракт
|
|
2
|
+
|
|
3
|
+
**Русский** · [English](../13-architecture-and-contract.md)
|
|
4
|
+
|
|
5
|
+
Раздел для тех, кто разрабатывает под iapeer: рантайм-пакеты, capability-плагины, провайдеры памяти.
|
|
6
|
+
|
|
7
|
+
## Инварианты
|
|
8
|
+
|
|
9
|
+
На них держится поведение системы:
|
|
10
|
+
|
|
11
|
+
- **Один роутер.** В системе ровно один постоянно работающий демон. Все сообщения идут через него; второго маршрутизатора нет.
|
|
12
|
+
- **Идентичность на каждый запрос.** Демон не хранит сессию клиента — он определяет, кто к нему обратился, из заголовка `X-IAPeer-Identity` каждого MCP-запроса. Один слушатель обслуживает всех пиров.
|
|
13
|
+
- **Единственный писатель реестра.** `peers-profiles.json` пишется только через одного писателя под файловой блокировкой. Прямая запись файла запрещена на уровне ядра — это исключает гонки.
|
|
14
|
+
- **Живость определяется live-сессией пира.** Пир жив тогда и только тогда, когда у него есть живая сессия под супервизором. Это не отдельный флаг в состоянии, который можно рассинхронизировать, а наблюдаемый факт.
|
|
15
|
+
- **`доставлено` ⟺ легло в живую сессию.** Успех доставки означает, что сообщение принято живой сессией (демон дождался подтверждения). Не подтвердила — демон считает сессию мёртвой, а не лжёт об успехе.
|
|
16
|
+
- **Источник истины — локальный профиль.** Реестр — пересобираемая проекция профилей пиров. При расхождении прав профиль.
|
|
17
|
+
- **Бинарь установлен на хост.** Демон работает с установленного `~/.local/bin/iapeer`, а не из рабочего дерева исходников. Обновление — это замена бинаря и перезапуск демона на него.
|
|
18
|
+
|
|
19
|
+
## Рантайм-адаптеры на стороне ядра
|
|
20
|
+
|
|
21
|
+
Адаптер — слой ядра, который умеет запускать сессию рантайма. Все адаптеры реализуют единый интерфейс; различия рантаймов спрятаны за ним.
|
|
22
|
+
|
|
23
|
+
Ключевые свойства адаптера:
|
|
24
|
+
|
|
25
|
+
- **`kind`** — `tui` (интерактивная сессия, принимает ввод: claude, codex) или `router` (долгоживущий процесс без ввода: telegram, notifier).
|
|
26
|
+
- **`usesDoctrine`** — потребляет ли рантайм собранный системный промпт. Для TUI-агентов — да, для роутеров — нет (это не языковые модели).
|
|
27
|
+
- **`requiresIntelligence`** — гейт по природе пира. У `telegram` это `natural`: попытка завести Telegram-присутствие у не-человека отклоняется с явной ошибкой.
|
|
28
|
+
- **`buildArgv`** — как собрать команду запуска: для роутеров это `<рантайм>-runtime run` плюс дополнительные аргументы из `launch.env`.
|
|
29
|
+
|
|
30
|
+
Роутеры пропускают все TUI-фазы: у них нет загрузочных диалогов, маркеров готовности, подтверждения доставки и контрольных команд. Их живость держит launchd, а не пробуждение демоном.
|
|
31
|
+
|
|
32
|
+
Продакшен-механизм хостинга TUI-сессий — **pty-супервизор**: супервизор-демон владеет pty сессии и её pane-логом, через его сокет идёт доставка на ввод. tmux остаётся только внутренней dev-ветвью (shadow-branch), на пользовательском уровне не фигурирует.
|
|
33
|
+
|
|
34
|
+
## Контракт рантайм-пакета
|
|
35
|
+
|
|
36
|
+
Рантайм-пакет (`*-runtime`) — npm-пакет, добавляющий инфраструктурный рантайм. Примеры: `@agfpd/notifier-runtime`, `@agfpd/telegram-runtime`.
|
|
37
|
+
|
|
38
|
+
### Манифест runtime.json
|
|
39
|
+
|
|
40
|
+
Пакет при self-install пишет манифест в `~/.iapeer/runtimes/<рантайм>/runtime.json`. Ядро его читает.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"runtime": "notifier",
|
|
45
|
+
"version": "0.2.0",
|
|
46
|
+
"selfConfig": { "command": "notifier-runtime", "args": ["self-config"] },
|
|
47
|
+
"peers": [
|
|
48
|
+
{ "personality": "timer", "intelligence": "absent", "description": "Планировщик по расписанию" },
|
|
49
|
+
{ "personality": "watcher", "intelligence": "absent", "description": "Наблюдатель за событиями" }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Поле | Тип | Обязательность | Назначение |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `runtime` | строка | обязательно | ID рантайма, совпадает с именем папки; шаблон `^[a-z][a-z0-9]{0,31}$` |
|
|
57
|
+
| `version` | строка | опционально | Версия пакета. Используется для гейта при `update-runtime`; без неё обновление просто переустанавливает |
|
|
58
|
+
| `selfConfig` | дескриптор | опционально | Хук персональной настройки, вызывается ядром на каждого пира при провижне |
|
|
59
|
+
| `peers` | массив | опционально | Объявленный набор пиров. Есть — режим declared-set; нет — operator-add |
|
|
60
|
+
|
|
61
|
+
Элемент `peers[]`: `personality` (обязательно), `intelligence`, `description`, `path` (явная папка), `runtimeBin` (путь лаунчера) — последние четыре опциональны.
|
|
62
|
+
|
|
63
|
+
**Дескриптор** (`selfConfig` и подобные) — это `{ "command": "...", "args": [...] }` или просто строка-команда.
|
|
64
|
+
|
|
65
|
+
### Два режима развёртывания
|
|
66
|
+
|
|
67
|
+
- **declared-set** — манифест содержит `peers[]`. `iapeer install-runtime <рантайм>` разворачивает весь набор сразу (notifier разворачивает `timer` и `watcher`).
|
|
68
|
+
- **operator-add** — `peers[]` пуст или отсутствует. Пиров заводит оператор по одному: `iapeer create <имя> --runtime <рантайм>` (так подключают людей в telegram).
|
|
69
|
+
|
|
70
|
+
### Что ядро вызывает у пакета
|
|
71
|
+
|
|
72
|
+
Контракт минимален — ядро обращается к пакету ровно тремя способами:
|
|
73
|
+
|
|
74
|
+
1. **`npx <пакет>`** — при установке. Пакет идемпотентно раскладывает свой бинарь на `PATH` и пишет `runtime.json`.
|
|
75
|
+
2. **`<рантайм>-runtime run`** — при запуске сессии (через launchd-плист или launch). В окружении — `PEER_PERSONALITY`, `PEER_RUNTIME`, `PEER_IDENTITY`.
|
|
76
|
+
3. **Хук `selfConfig`** — при провижне каждого пира. Вызывается в рабочей папке пира; в окружении — `IAPEER_PEER_PERSONALITY`, `IAPEER_PEER_CWD`, `IAPEER_PEER_RUNTIME`, `IAPEER_PEER_INTELLIGENCE`. Код возврата 0 — настроено (идемпотентно), иначе — ошибка (плист пишется, но не загружается).
|
|
77
|
+
|
|
78
|
+
Команды вроде `prepare`, `interface`, `bot`, `doctor` — это внутренние команды самого пакета для оператора, а не контракт с ядром: ядро их не вызывает.
|
|
79
|
+
|
|
80
|
+
### Установка и обновление
|
|
81
|
+
|
|
82
|
+
- **`install-runtime <рантайм>`** определяет npm-пакет (по встроенному реестру или флагу `--package`), ставит его через `npx`, затем разворачивает объявленный набор пиров.
|
|
83
|
+
- **`update-runtime <рантайм>`** сверяет версию манифеста с npm; при отставании переустанавливает, заново провизионирует и перезапускает пиров рантайма штатными `stop`/`start`.
|
|
84
|
+
|
|
85
|
+
## Стандарт capability-плагина
|
|
86
|
+
|
|
87
|
+
Capability-плагин добавляет пиру способность, не будучи рантаймом. Отличие чёткое: рантайм — способ присутствия пира, плагин — способность поверх рантайма.
|
|
88
|
+
|
|
89
|
+
### Манифест iapeer.json
|
|
90
|
+
|
|
91
|
+
Плагин может (но не обязан) положить в свой корень `iapeer.json`:
|
|
92
|
+
|
|
93
|
+
| Поле | Тип | Назначение |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `setup` | дескриптор | Шаг установки плагина. Вызывается при `enable`, если объявлен и установка прошла |
|
|
96
|
+
| `requires` | — | Зависимости (рантаймы, другие плагины). Ядро их не разрешает — плагин проверяет их сам и отказывает, если зависимости нет |
|
|
97
|
+
|
|
98
|
+
Плагин без `iapeer.json` или без `setup` — это простой класс: ставится и включается, дополнительной инициализации нет.
|
|
99
|
+
|
|
100
|
+
### enable
|
|
101
|
+
|
|
102
|
+
`iapeer enable <плагин> [пир]` по шагам:
|
|
103
|
+
|
|
104
|
+
1. Находит пир (по имени или по текущей папке); отказывает, если у пира нет агентского рантайма.
|
|
105
|
+
2. На каждом агентском рантайме пира ставит плагин: для Claude — в область проекта внутри папки пира (`plugin install … --scope project`), для Codex — в общий конфиг хоста. Установка попутно включает плагин.
|
|
106
|
+
3. Если объявлен `setup` и установка удалась — вызывает его. В окружении: `IAPEER_PEER_PERSONALITY`, `IAPEER_PEER_CWD`; рабочая папка — корень плагина. Шаг best-effort: его сбой не откатывает уже сделанные установку и включение.
|
|
107
|
+
|
|
108
|
+
Идемпотентность: для Claude привязка идёт по реальному пути папки пира, поэтому повторный `enable` не задевает другие пиры и не дублирует установку.
|
|
109
|
+
|
|
110
|
+
### Критерии iapeer-совместимости
|
|
111
|
+
|
|
112
|
+
Плагин считается совместимым, если он:
|
|
113
|
+
|
|
114
|
+
1. полезен сам по себе;
|
|
115
|
+
2. объявляет, что предоставляет и что требует;
|
|
116
|
+
3. не владеет чужой ответственностью;
|
|
117
|
+
4. имеет идемпотентную подготовку;
|
|
118
|
+
5. имеет самодиагностику (`doctor`/`status`);
|
|
119
|
+
6. использует общий ABI идентичности (`PEER_*`), когда тот доступен;
|
|
120
|
+
7. не требует от пользователя вручную синхронизировать идентичность между плагинами;
|
|
121
|
+
8. одинаково понимает имя, рантайм и идентичность под Claude, Codex и будущими рантаймами.
|
|
122
|
+
|
|
123
|
+
## Контракт слота памяти
|
|
124
|
+
|
|
125
|
+
Память даёт провайдер, занимающий единственный на хост слот. Слот занят ровно одним провайдером или пуст.
|
|
126
|
+
|
|
127
|
+
### Манифест провайдера
|
|
128
|
+
|
|
129
|
+
Провайдер атомарно пишет `~/.iapeer/memory-provider.json`; ядро только читает. Удаляет файл — освобождает слот. Ядро слот никогда не пишет.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"provider": "iapeer-memory",
|
|
134
|
+
"package": "@agfpd/iapeer-memory",
|
|
135
|
+
"version": "0.2.7",
|
|
136
|
+
"registeredAt": "2026-06-11T17:46:41.449Z",
|
|
137
|
+
"heartbeat": "/Users/me/.iapeer/state/iapeer-memory/memoryd.heartbeat",
|
|
138
|
+
"provision": {
|
|
139
|
+
"command": "/Users/me/.local/bin/iapeer-memory",
|
|
140
|
+
"args": ["provision-peer", "--cwd", "{cwd}", "--runtime", "{runtime}",
|
|
141
|
+
"--personality", "{personality}", "--occasion", "{occasion}"]
|
|
142
|
+
},
|
|
143
|
+
"unprovision": {
|
|
144
|
+
"command": "/Users/me/.local/bin/iapeer-memory",
|
|
145
|
+
"args": ["unprovision-peer", "--cwd", "{cwd}", "--runtime", "{runtime}",
|
|
146
|
+
"--occasion", "{occasion}"]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Поле | Тип | Обязательность | Назначение |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| `provider` | строка | обязательно | Имя провайдера |
|
|
154
|
+
| `package` | строка | обязательно | npm-пакет |
|
|
155
|
+
| `version` | строка | обязательно | Версия пакета |
|
|
156
|
+
| `registeredAt` | ISO-дата | обязательно | Когда занят слот |
|
|
157
|
+
| `heartbeat` | путь | опционально | Файл сердцебиения демона провайдера; `iapeer status` показывает его свежесть |
|
|
158
|
+
| `provision` | дескриптор | опционально | Команда настройки памяти пира при рождении |
|
|
159
|
+
| `unprovision` | дескриптор | опционально | Команда сворачивания при удалении пира |
|
|
160
|
+
|
|
161
|
+
`command` обязан быть абсолютным путём; `args` — массив строк. Невалидный блок ядро трактует как отсутствующий, пустой слот — как нормальное состояние.
|
|
162
|
+
|
|
163
|
+
### Команды provision/unprovision
|
|
164
|
+
|
|
165
|
+
В аргументах подставляются четыре плейсхолдера (подстановка пер-аргумент, на уровне подстроки):
|
|
166
|
+
|
|
167
|
+
| Плейсхолдер | Значение |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `{cwd}` | рабочая папка пира |
|
|
170
|
+
| `{runtime}` | конкретный агентский рантайм (`claude` / `codex`) |
|
|
171
|
+
| `{personality}` | имя пира — обязан совпадать с `normalize(basename({cwd}))` (имя пира и папка соотносятся 1:1); несовпадение → `ensurePeerProfile` завершается ошибкой |
|
|
172
|
+
| `{occasion}` | момент вызова (см. ниже) |
|
|
173
|
+
|
|
174
|
+
**Моменты вызова, которые эмитит ядро:**
|
|
175
|
+
|
|
176
|
+
- **`birth`** — при рождении пира, по одной команде на каждый агентский рантайм. Идёт после предварительного доверия codex к папке, чтобы поверхности провайдера легли в уже доверенную папку.
|
|
177
|
+
- **`remove`** — при удалении пира, по команде на агентский рантайм. Идёт до очистки состояния пира — провайдер видит его в последнем согласованном виде.
|
|
178
|
+
|
|
179
|
+
**Окружение:** команда наследует всё окружение ядра; специальные `IAPEER_PEER_*` сюда ядро **не** добавляет — провайдер опирается на плейсхолдеры в аргументах. stdin игнорируется, stdout/stderr захватываются.
|
|
180
|
+
|
|
181
|
+
**Семантика выполнения:** код возврата 0 — успех; иначе — ошибка; превышение 120 секунд — таймаут; недоступный бинарь — отдельный исход. Всё best-effort: сбой команды не валит ни рождение, ни удаление пира, но громко логируется. Провайдер обязан быть идемпотентным — ядро может вызвать одну команду повторно.
|
|
182
|
+
|
|
183
|
+
### Рычаг родной памяти
|
|
184
|
+
|
|
185
|
+
Когда слот занят, держать параллельно родную память рантайма вредно. Ядро при рождении пира с занятым слотом отключает её:
|
|
186
|
+
|
|
187
|
+
- **Claude** — в `<папка>/.claude/settings.json` дописывает `"autoMemoryEnabled": false`;
|
|
188
|
+
- **Codex** — в `<папка>/.codex/config.toml` дописывает `[features] memories = false`.
|
|
189
|
+
|
|
190
|
+
Включить обратно — удалить ключ (восстановить умолчание рантайма). Вручную: `iapeer native-memory off|on`. Для Codex ядро при рождении заодно прописывает доверие к папке (`[projects."<реальный-путь>"] trust_level = "trusted"`), чтобы проектный конфиг читался с первой сессии; при удалении пира эта запись снимается.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# 14 — Конфигурация и журналы
|
|
2
|
+
|
|
3
|
+
**Русский** · [English](../14-configuration-and-logs.md)
|
|
4
|
+
|
|
5
|
+
Ядро работает на разумных значениях по умолчанию — отдельная настройка для запуска не нужна. Поведение тонко настраивается переменными окружения; журналы дают материал для разбора, когда что-то пошло не так.
|
|
6
|
+
|
|
7
|
+
## Переменные окружения
|
|
8
|
+
|
|
9
|
+
Все значения ниже — дефолты из текущего кода. Переменную задают в окружении демона или конкретного пира (через `launch.env`, см. [06 — Хранение](06-хранение.md)).
|
|
10
|
+
|
|
11
|
+
### Жизненный цикл
|
|
12
|
+
|
|
13
|
+
| Переменная | По умолчанию | Что задаёт |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `IAPEER_IDLE_SECS` | `3600` | Через сколько секунд простоя демон закрывает сессию агента (усыпление). |
|
|
16
|
+
| `IAPEER_BOOT_DEADLINE_SECS` | `240` | Сколько ждать загрузки сессии при подъёме, прежде чем считать запуск неудачным. |
|
|
17
|
+
| `IAPEER_READY_GATE_SECS` | `120` | Сколько ждать готовности агента к вводу после загрузки. |
|
|
18
|
+
| `IAPEER_CRASHLOOP_MAX` | `3` | Сколько смертей в окне допускается, прежде чем демон перестанет поднимать пир. |
|
|
19
|
+
| `IAPEER_CRASHLOOP_WINDOW_SECS` | `300` | Окно (секунды), на котором считаются смерти для защиты от цикла перезапусков. |
|
|
20
|
+
| `IAPEER_EPHEMERAL_QUIET_SECS` | `20` | Окно тишины, после которого свернётся эфемерный воркер, уже отправивший ответ. |
|
|
21
|
+
| `IAPEER_EPHEMERAL_UNARMED_IDLE_SECS` | `600` | Страховочный предел для эфемерного воркера, который молчит и не отправил ответ. |
|
|
22
|
+
|
|
23
|
+
Надзорный тик демона фиксирован — раз в минуту, не настраивается.
|
|
24
|
+
|
|
25
|
+
### Демон
|
|
26
|
+
|
|
27
|
+
| Переменная | По умолчанию | Что задаёт |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `IAPEER_PORT` | `8765` | TCP-порт роутера на `127.0.0.1`. |
|
|
30
|
+
| `IAPEER_DAEMON_SOCKET` | `~/.iapeer/state/iapeer/router.sock` | Путь локального сокета демона. |
|
|
31
|
+
| `IAPEER_BEARER_TOKEN` | — | Если задан, демон требует этот bearer-токен на входе; иначе проверка токена выключена. |
|
|
32
|
+
|
|
33
|
+
### Доставка сообщений
|
|
34
|
+
|
|
35
|
+
| Переменная | По умолчанию | Что задаёт |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `IAP_HOST_LIVENESS_GRACE_MS` | `4000` | Сколько ждать подтверждения, что сессия приняла сообщение, прежде чем считать её мёртвой. |
|
|
38
|
+
| `IAP_LIVENESS_GRACE_MS` | `3000` | Окно проверки живости занятой сессии (по сдвигу истории диалога). |
|
|
39
|
+
| `IAPEER_COMPOSER_QUEUE_TIMEOUT_MS` | `120000` | Сколько очередь композера ждёт, пока освободится поле ввода, прежде чем досылать принудительно. |
|
|
40
|
+
| `IAPEER_COMPOSER_QUEUE_POLL_MS` | `500` | Частота опроса поля ввода в очереди композера. |
|
|
41
|
+
|
|
42
|
+
### Пути и бинарники
|
|
43
|
+
|
|
44
|
+
| Переменная | По умолчанию | Что задаёт |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `IAPEER_ROOT` | `~/.iapeer` | Корень хранилища. Смена влечёт изоляцию сокетов в `<корень>/socks` (полезно для изолированных тестовых хостов). |
|
|
47
|
+
| `IAPEER_SOCK_DIR` | `/tmp` | Каталог сокетов host-wide. Переопределяет расположение host-wide. |
|
|
48
|
+
| `IAPEER_BIN_DIR` | `~/.local/bin` | Куда ставится бинарь `iapeer`. |
|
|
49
|
+
| `IAPEER_LAUNCHAGENTS_DIR` | `~/Library/LaunchAgents` | Каталог плистов launchd. |
|
|
50
|
+
| `IAPEER_CLAUDE_BIN` | `~/.local/bin/claude` | Путь к бинарю Claude. |
|
|
51
|
+
| `IAPEER_CODEX_BIN` | `codex` (из `PATH`) | Путь к бинарю Codex. |
|
|
52
|
+
| `NOTIFIER_RUNTIME_BIN` | `notifier-runtime` (из `PATH`) | Путь к лаунчеру notifier (для launchd с его минимальным `PATH`). |
|
|
53
|
+
| `TELEGRAM_RUNTIME_BIN` | `telegram-runtime` (из `PATH`) | Путь к лаунчеру Telegram. |
|
|
54
|
+
| `CODEX_HOME` | `~/.codex` | Переопределение каталога глобального конфига Codex. |
|
|
55
|
+
|
|
56
|
+
### Журналы
|
|
57
|
+
|
|
58
|
+
| Переменная | По умолчанию | Что задаёт |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `IAPEER_LIFECYCLE_LOG_MAX_BYTES` | `5 МиБ` | Размер `lifecycle.log`, при котором происходит ротация. |
|
|
61
|
+
| `IAPEER_LIFECYCLE_LOG_KEEP` | `5` | Сколько ротаций `lifecycle.log` хранить. |
|
|
62
|
+
| `IAPEER_DELIVERY_LOG_MAX_BYTES` | `5 МиБ` | Размер `delivery.log` для ротации. |
|
|
63
|
+
| `IAPEER_DELIVERY_LOG_KEEP` | `5` | Сколько ротаций `delivery.log` хранить. |
|
|
64
|
+
| `IAPEER_SUPERVISE_LOG_VERBOSE` | выкл. | Подробный лог надзорных тиков (по умолчанию пишутся только решения, не рутинные «ничего не делаю»). |
|
|
65
|
+
| `IAPEER_DAEMON_LOG` | выкл. | Расширенный лог демона. |
|
|
66
|
+
|
|
67
|
+
### Внутренние, dev и тестовые
|
|
68
|
+
|
|
69
|
+
`IAPEER_TEST_SANDBOX=1` включает защиту от записи в реальные пути: установка откажется писать в настоящий `~/.local/bin/iapeer` и потребует задать `IAPEER_BIN_DIR` на изолированный путь. Для разработки и автотестов, не для обычной работы.
|
|
70
|
+
|
|
71
|
+
Ещё несколько переменных система читает, но в обычной эксплуатации их не задают:
|
|
72
|
+
|
|
73
|
+
- `NOTIFIER_FALLBACK_TARGET` — запасной адресат для сигналов notifier; ядро резолвит его в плист пира при онбординге (без хардкода), вручную оператор его не выставляет.
|
|
74
|
+
- `IAP_TMUX_PASTE_SETTLE_MS`, `IAP_TMUX_SUBMIT_TIMEOUT_MS` — тайминги доставки во внутренней tmux dev-ветви; на проде используется pty-супервизор ([13 — Архитектура и контракт](13-архитектура-и-контракт.md)), вне разработки не нужны.
|
|
75
|
+
- `IAPEER_SHADOW_PATH` — `PATH` для shadow-fidelity прогона (внутренний инструмент разработки).
|
|
76
|
+
|
|
77
|
+
## Журналы и диагностика
|
|
78
|
+
|
|
79
|
+
Демон ведёт три долговременных журнала в `~/.iapeer/logs/iapeer/`:
|
|
80
|
+
|
|
81
|
+
- **`lifecycle.log`** — решения жизненного цикла: пробуждения, усыпления, перезапуски, итоги надзорных тиков. Первое место, куда смотреть, если пир ведёт себя не так.
|
|
82
|
+
- **`delivery.log`** — исход каждой доставки: кто, кому, в какой рантайм, результат, размеры, тема. Только метаданные — **тело сообщения не пишется**.
|
|
83
|
+
- **`exits.log`** — смерти сессий с посмертными данными: супервизор фиксирует причину (код выхода / сигнал) в момент смерти сессии.
|
|
84
|
+
|
|
85
|
+
### Посмертная диагностика
|
|
86
|
+
|
|
87
|
+
Причину смерти сессии (код выхода / сигнал) супервизор фиксирует в момент смерти — это то, что обычная проверка «жив ли процесс» не ловит. Отдельный сторож (canary) держит соединение с супервизором и при смерти сессии записывает в `exits.log` обстоятельства: память, swap, процессы по потреблению, системные отчёты диагностики. Это материал для ответа на вопрос «почему агент умер».
|
|
88
|
+
|
|
89
|
+
### Full Disk Access
|
|
90
|
+
|
|
91
|
+
Чтобы читать системные отчёты диагностики, бинарю `iapeer` нужен Full Disk Access (FDA) — разрешение macOS. `iapeer status` показывает, выдан ли FDA, и подсказывает, что делать, если нет. Без FDA пиры будут натыкаться на запросы TCC при доступе к защищённым папкам и контейнерам сторонних приложений.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# iapeer
|
|
2
|
+
|
|
3
|
+
**Русский** · [English](../README.md)
|
|
4
|
+
|
|
5
|
+
iapeer — ядро мультиагентной системы на одном хосте. Он держит реестр участников, единый демон-роутер для обмена сообщениями между ними и набор команд, которыми участниками управляют: создают, запускают, усыпляют, перезапускают, подключают к внешним каналам.
|
|
6
|
+
|
|
7
|
+
Участник системы называется **пиром**. Пиром может быть AI-агент (Claude, Codex), человек (через Telegram) или служебный процесс (планировщик, наблюдатель). iapeer не различает их по природе на уровне транспорта — все они адресуются одинаково и обмениваются сообщениями через один протокол.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
send_to_peer (от любого пира)
|
|
11
|
+
│
|
|
12
|
+
▼
|
|
13
|
+
┌────────────────────────┐
|
|
14
|
+
│ демон-роутер │ работает постоянно
|
|
15
|
+
│ поиск · доставка │ читает peers-profiles.json (реестр)
|
|
16
|
+
└────────────────────────┘
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
пир-получатель (если спит — поднимет)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Что это решает
|
|
23
|
+
|
|
24
|
+
Когда на машине живёт не один агент, а команда — у каждого своя роль, своя рабочая папка, свой рантайм, — возникают задачи, которых у одиночного агента нет:
|
|
25
|
+
|
|
26
|
+
- **Адресация.** Как агенту A отправить сообщение агенту B, не зная, запущен ли тот прямо сейчас.
|
|
27
|
+
- **Присутствие.** Держать ли каждого агента в памяти постоянно (дорого) или поднимать по требованию (нужен кто-то, кто поднимет).
|
|
28
|
+
- **Идентичность.** Один и тот же логический агент может присутствовать в разных рантаймах (сегодня Claude, завтра Codex) — система должна понимать, что это один и тот же пир.
|
|
29
|
+
- **Жизненный цикл.** Кто усыпит простаивающего агента, кто перезапустит зависшего, кто не даст упавшему агенту войти в цикл перезапусков.
|
|
30
|
+
|
|
31
|
+
iapeer берёт это на себя. Агенту достаточно вызвать `send_to_peer(<имя>, <текст>)` — остальное делает демон: найдёт получателя в реестре, проверит, жив ли он, доставит сообщение в живую сессию или поднимет спящего и доставит после пробуждения.
|
|
32
|
+
|
|
33
|
+
## Быстрый старт
|
|
34
|
+
|
|
35
|
+
Требования: **macOS** (управление процессами идёт через launchd), **[bun](https://bun.sh)** на `PATH` и хотя бы один агентский рантайм — Claude Code или Codex CLI.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 1. Установите ядро — бинарь, раскладку ~/.iapeer и плист демона
|
|
39
|
+
npx @agfpd/iapeer
|
|
40
|
+
|
|
41
|
+
# 2. Подготовьте хост — onboard стартует демон, регистрирует marketplace,
|
|
42
|
+
# ставит notifier, Telegram и память
|
|
43
|
+
iapeer onboard
|
|
44
|
+
|
|
45
|
+
# 3. Создайте первого агента
|
|
46
|
+
iapeer create assistant --runtime claude
|
|
47
|
+
|
|
48
|
+
# 4. Отправьте сообщение — демон поднимет сессию, если её нет
|
|
49
|
+
iapeer send assistant --message "привет, ты на связи?"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Подробный разбор каждого шага — в [02 — Быстрый старт](02-быстрый-старт.md).
|
|
53
|
+
|
|
54
|
+
## Документация
|
|
55
|
+
|
|
56
|
+
Разделы выстроены слоями: от «поставить и пользоваться» к «понять архитектуру и расширять».
|
|
57
|
+
|
|
58
|
+
**Введение**
|
|
59
|
+
- [01 — Обзор](01-обзор.md) — что такое iapeer и из чего он состоит
|
|
60
|
+
- [02 — Быстрый старт](02-быстрый-старт.md) — установка, онбординг, первый пир, первое сообщение
|
|
61
|
+
|
|
62
|
+
**Концепции**
|
|
63
|
+
- [03 — Пиры, рантаймы, идентичность](03-пиры-рантаймы-идентичность.md)
|
|
64
|
+
- [04 — Жизненный цикл и демон](04-жизненный-цикл-и-демон.md)
|
|
65
|
+
- [05 — Идентичность и системный промпт](05-системный-промпт.md)
|
|
66
|
+
- [06 — Хранение](06-хранение.md)
|
|
67
|
+
|
|
68
|
+
**Руководства**
|
|
69
|
+
- [07 — Установка и обновление](07-установка-и-обновление.md)
|
|
70
|
+
- [08 — Управление пирами](08-управление-пирами.md)
|
|
71
|
+
- [09 — Рантаймы](09-рантаймы.md)
|
|
72
|
+
- [10 — Обмен сообщениями](10-обмен-сообщениями.md)
|
|
73
|
+
- [11 — Расширения и память](11-расширения-и-память.md)
|
|
74
|
+
|
|
75
|
+
**Справочник**
|
|
76
|
+
- [12 — CLI](12-cli-reference.md) — все команды с флагами
|
|
77
|
+
- [13 — Архитектура и контракт](13-архитектура-и-контракт.md) — для разработчиков плагинов и рантайм-пакетов
|
|
78
|
+
- [14 — Конфигурация и журналы](14-конфигурация-и-журналы.md) — переменные окружения, журналы, диагностика
|
|
79
|
+
|
|
80
|
+
## Статус
|
|
81
|
+
|
|
82
|
+
Построено и работает: единый демон-роутер, протокол обмена сообщениями (IAP), реестр пиров, идентичность и её ABI, жизненный цикл (пробуждение, усыпление, перезапуск, надзор), рантайм-адаптеры для Claude, Codex, Telegram и notifier, установка и онбординг хоста, подключение Telegram, слот памяти и capability-плагины.
|
|
83
|
+
|
|
84
|
+
Запланировано поверх ядра: граф совместимости пакетов, доктор здоровья всей команды и интерактивный интерфейс координации. Пока этих надстроек нет, координационные операции живут в CLI `iapeer`.
|
|
85
|
+
|
|
86
|
+
## Планы
|
|
87
|
+
|
|
88
|
+
Более крупные направления, запланированы и ещё не сделаны:
|
|
89
|
+
|
|
90
|
+
- **Веб-интерфейс** — работа с командой через браузер.
|
|
91
|
+
- **Поддержка Linux** — сегодня iapeer работает на macOS; Linux в планах.
|
|
92
|
+
- **Дополнительные агентские рантаймы** — `opencode` и `antigravity`, два новых CLI-рантайма наряду с Claude Code и Codex.
|
|
93
|
+
- **Рантайм Discord** — рантайм присутствия человека для Discord, наряду с Telegram.
|
|
94
|
+
- **Гейт разрешений** — более тонкий контроль над тем, что агенту позволено делать.
|
|
95
|
+
- **Апрув разрешений через Telegram** — подтверждать запросы разрешений агента прямо из чата; сегодня разрешения выданы автоматически флагами.
|
|
96
|
+
|
|
97
|
+
Платформа сейчас — только macOS/launchd. Поддержка Linux (systemd) и Windows не реализована.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.99",
|
|
4
4
|
"description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "agfpd",
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"src",
|
|
29
29
|
"bin",
|
|
30
|
+
"docs",
|
|
30
31
|
"tsconfig.json",
|
|
31
|
-
"!src/**/*.test.ts"
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"!docs/internals/**"
|
|
32
34
|
],
|
|
33
35
|
"exports": {
|
|
34
36
|
".": "./src/index.ts",
|
package/src/cli/index.ts
CHANGED
|
@@ -63,8 +63,8 @@ import {
|
|
|
63
63
|
} from '../identity/profileStandard.ts'
|
|
64
64
|
import { runAlwaysOn } from '../launch/launchdRun.ts'
|
|
65
65
|
import { ensureDaemonStarted, installDaemonPlist, startConfiguredDaemon } from '../daemon/main.ts'
|
|
66
|
-
import { MARKETPLACE_NAME, onboardHost, tccFullDiskAccessNote } from '../onboard/index.ts'
|
|
67
|
-
import { readMemoryProvider } from '../status/index.ts'
|
|
66
|
+
import { MARKETPLACE_NAME, onboardHost, runtimeAuthNote, tccFullDiskAccessNote } from '../onboard/index.ts'
|
|
67
|
+
import { probeFullDiskAccess, readMemoryProvider } from '../status/index.ts'
|
|
68
68
|
import { appendLifecycleEvent } from '../lifecycle/eventlog.ts'
|
|
69
69
|
import { pluginLogsDir } from '../storage/index.ts'
|
|
70
70
|
|
|
@@ -1096,9 +1096,22 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1096
1096
|
})
|
|
1097
1097
|
const memLabel = mem.provider ? `${mem.provider.provider} ${mem.provider.version}` : 'none'
|
|
1098
1098
|
out(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${memLabel})\n`)
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1099
|
+
// Runtime auth readiness (clean-host prerequisite): a peer runs the runtime's
|
|
1100
|
+
// interactive TUI; the launcher auto-clears first-run modals (theme/trust) but
|
|
1101
|
+
// CANNOT complete a login OAuth flow. Warn for every INSTALLED runtime (marketplace
|
|
1102
|
+
// state != runtime-missing) that has no positive auth evidence — before it ships a
|
|
1103
|
+
// peer that would fail to wake on the login screen.
|
|
1104
|
+
for (const m of r.marketplaces) {
|
|
1105
|
+
if (m.state === 'runtime-missing') continue
|
|
1106
|
+
const authNote = runtimeAuthNote(m.runtime, env)
|
|
1107
|
+
if (authNote) errOut(authNote + '\n')
|
|
1108
|
+
}
|
|
1109
|
+
// macOS TCC advisory (manual grant — no flag/setting/script can): PROBE-DRIVEN, not
|
|
1110
|
+
// memory-gated — a missing grant silently breaks file I/O (EPERM, no prompt/hang) on
|
|
1111
|
+
// any TCC-protected path (iCloud vault, Documents/Desktop/Downloads peer cwd). Silent
|
|
1112
|
+
// when granted / non-macOS / undeterminable.
|
|
1113
|
+
const { iapeerBinPath } = await import('../install/index.ts')
|
|
1114
|
+
const tcc = tccFullDiskAccessNote({ fda: probeFullDiskAccess(env), binPath: iapeerBinPath(env) })
|
|
1102
1115
|
if (tcc) out(tcc)
|
|
1103
1116
|
return daemonFailed || r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
|
|
1104
1117
|
}
|
|
@@ -49,6 +49,12 @@ const CLAUDE_BOOT_DIALOG_MARKERS = [
|
|
|
49
49
|
'I am using this for local development',
|
|
50
50
|
'Resume from summary',
|
|
51
51
|
'Resuming the full session',
|
|
52
|
+
// First-run theme picker ("Let's get started. Choose the text style…") — appears
|
|
53
|
+
// on a CLEAN host (fresh config dir) BEFORE the input prompt, verified live on
|
|
54
|
+
// claude 2.1.181 in a pty. NOT removed by --dangerously-skip-permissions; if not
|
|
55
|
+
// cleared the peer never reaches ready. (There is NO separate "accept dangerous
|
|
56
|
+
// permissions" screen in 2.1.181 — verified absent.)
|
|
57
|
+
'Choose the text style',
|
|
52
58
|
] as const
|
|
53
59
|
|
|
54
60
|
function anyBootDialog(pane: string): boolean {
|
|
@@ -185,7 +191,16 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
185
191
|
if (
|
|
186
192
|
pane.includes('trust this folder') ||
|
|
187
193
|
pane.includes('Allow external CLAUDE.md file imports?') ||
|
|
188
|
-
pane.includes('I am using this for local development')
|
|
194
|
+
pane.includes('I am using this for local development') ||
|
|
195
|
+
// First-run theme picker (clean host): the default-highlighted row is always a
|
|
196
|
+
// valid theme (any choice is harmless for a headless peer), so a bare Enter
|
|
197
|
+
// accepts it. Without this the picker blocks the boot forever on a fresh host.
|
|
198
|
+
// NOTE: the LOGIN method picker that can follow on an UNAUTHENTICATED host is
|
|
199
|
+
// deliberately NOT auto-answered — selecting subscription opens a browser OAuth
|
|
200
|
+
// flow no headless peer can complete. Login is an onboard PREREQUISITE
|
|
201
|
+
// (ANTHROPIC_API_KEY or a one-time `claude login`); a never-authed host fails
|
|
202
|
+
// the wake LOUD at the boot deadline rather than auto-entering a dead OAuth.
|
|
203
|
+
pane.includes('Choose the text style')
|
|
189
204
|
) {
|
|
190
205
|
return ['Enter']
|
|
191
206
|
}
|
package/src/onboard/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { spawnSync } from 'child_process'
|
|
13
13
|
import { homedir } from 'os'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
-
import { accessSync, constants as FS } from 'fs'
|
|
15
|
+
import { accessSync, constants as FS, existsSync, readFileSync } from 'fs'
|
|
16
16
|
|
|
17
17
|
/** OUR marketplace — GitHub owner/repo, the source both runtimes' `marketplace add`
|
|
18
18
|
* takes; and the registered NAME both runtimes' `marketplace list` shows. */
|
|
@@ -216,23 +216,120 @@ export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
|
|
|
216
216
|
/**
|
|
217
217
|
* macOS Full Disk Access (TCC) advisory printed at the END of onboard. The runtime permission layer
|
|
218
218
|
* (`--add-dir` / `additionalDirectories` for claude, the sandbox flags for codex) governs what the
|
|
219
|
-
* RUNTIME allows; macOS TCC is a SEPARATE OS layer that NO flag
|
|
220
|
-
* user-granted in System Settings, by design
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
219
|
+
* RUNTIME allows; macOS TCC is a SEPARATE OS layer that NO flag, settings key, config, env var or
|
|
220
|
+
* script can grant — it is user-granted in System Settings, by design (the TCC db is SIP-protected).
|
|
221
|
+
*
|
|
222
|
+
* PROBE-DRIVEN, not memory-gated (was: gated on a memory provider). The advisory now fires off the
|
|
223
|
+
* REAL FDA state (`fda` from probeFullDiskAccess), because a missing grant is a latent fault no proxy
|
|
224
|
+
* captures: the DEFAULT memory vault is an iCloud Obsidian dir AND a peer work dir can itself sit in a
|
|
225
|
+
* protected location (Documents/Desktop/Downloads) with NO memory provider at all. So: grant present
|
|
226
|
+
* → silent; grant ABSENT on macOS → warn, every time.
|
|
227
|
+
*
|
|
228
|
+
* Failure mode is SILENT, not a hang: per Apple (DTS/Quinn, devforums 678819) a launchd-spawned CLI
|
|
229
|
+
* with no GUI front gets NO TCC dialog — the access is denied with EPERM. A peer therefore does NOT
|
|
230
|
+
* block; its file I/O on a protected path just silently fails. `~/.iapeer` (under $HOME) is NOT
|
|
231
|
+
* TCC-protected and needs nothing.
|
|
232
|
+
*
|
|
233
|
+
* Returns null when granted / undeterminable / non-macOS (`fda` true or null). Pure (no I/O) —
|
|
234
|
+
* the FDA probe and binPath are injected by the caller, so this stays unit-testable.
|
|
225
235
|
*/
|
|
226
|
-
export function tccFullDiskAccessNote(opts: {
|
|
227
|
-
if (
|
|
236
|
+
export function tccFullDiskAccessNote(opts: { fda: boolean | null; binPath: string }): string | null {
|
|
237
|
+
if (opts.fda !== false) return null // granted, non-macOS, or undeterminable → nothing to nag
|
|
228
238
|
return (
|
|
229
239
|
[
|
|
230
|
-
'macOS Full Disk Access (manual — TCC cannot be set by any flag
|
|
231
|
-
'
|
|
232
|
-
'
|
|
233
|
-
'
|
|
234
|
-
|
|
240
|
+
'macOS Full Disk Access NOT granted (manual — TCC cannot be set by any flag, settings key or script):',
|
|
241
|
+
' Without it, file I/O on TCC-protected paths SILENTLY FAILS with EPERM (no prompt, no hang) —',
|
|
242
|
+
' a peer reading/writing an iCloud Obsidian vault (iCloud Drive / Desktop / Documents / Downloads)',
|
|
243
|
+
' just gets denied. `~/.iapeer` itself is under $HOME and needs nothing.',
|
|
244
|
+
` Grant it to ${opts.binPath} (and to Terminal too, if you attach) in:`,
|
|
235
245
|
' System Settings → Privacy & Security → Full Disk Access',
|
|
236
246
|
].join('\n') + '\n'
|
|
237
247
|
)
|
|
238
248
|
}
|
|
249
|
+
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
// Runtime auth readiness — the one CLEAN-HOST modal a launcher CANNOT auto-clear
|
|
252
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
253
|
+
//
|
|
254
|
+
// A headless peer runs the runtime's INTERACTIVE TUI (a pty), not `-p`. On a clean
|
|
255
|
+
// host the first-run flow blocks on modals; the launcher auto-answers the ones it
|
|
256
|
+
// recognizes (theme picker, folder-trust, …) — but the LOGIN screen cannot be
|
|
257
|
+
// auto-answered (selecting subscription opens a browser OAuth flow no headless peer
|
|
258
|
+
// can complete). So login is a PREREQUISITE: the host must be authenticated BEFORE
|
|
259
|
+
// the first peer launches, or that peer's wake fails loud at the boot deadline.
|
|
260
|
+
//
|
|
261
|
+
// These probes are CONSERVATIVE: they return ready ONLY on positive evidence, so a
|
|
262
|
+
// missing signal nags (one harmless advisory line) rather than letting an
|
|
263
|
+
// unauthenticated host silently ship a peer that will fail to wake.
|
|
264
|
+
|
|
265
|
+
/** Resolve claude's config dir (CLAUDE_CONFIG_DIR override, else ~/.claude). */
|
|
266
|
+
function claudeConfigDir(env: NodeJS.ProcessEnv): string {
|
|
267
|
+
const override = env.CLAUDE_CONFIG_DIR?.trim()
|
|
268
|
+
if (override) return override
|
|
269
|
+
return join(env.HOME?.trim() || homedir(), '.claude')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* True when claude is authenticated for a headless launch (won't block on the login
|
|
274
|
+
* picker). Positive evidence, in order: an auth env var; a completed onboarding +
|
|
275
|
+
* account in `~/.claude.json` (the subscription `claude login` path); or an
|
|
276
|
+
* `apiKeyHelper` in settings.json. Anything else → not ready (nag). Reads are
|
|
277
|
+
* env-rooted (HOME/CLAUDE_CONFIG_DIR) so a hermetic test isolates them.
|
|
278
|
+
*/
|
|
279
|
+
export function claudeAuthReady(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
280
|
+
if (env.ANTHROPIC_API_KEY?.trim() || env.ANTHROPIC_AUTH_TOKEN?.trim() || env.CLAUDE_CODE_OAUTH_TOKEN?.trim()) {
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
283
|
+
// `~/.claude.json` lives at HOME root (not inside the config dir) in the default
|
|
284
|
+
// layout the fleet uses; hasCompletedOnboarding + an account/key marker proves the
|
|
285
|
+
// first-run flow (theme + login) is done.
|
|
286
|
+
const statePath = join(env.HOME?.trim() || homedir(), '.claude.json')
|
|
287
|
+
try {
|
|
288
|
+
const j = JSON.parse(readFileSync(statePath, 'utf8')) as Record<string, unknown>
|
|
289
|
+
if (j.hasCompletedOnboarding === true && (j.oauthAccount != null || j.customApiKeyResponses != null)) return true
|
|
290
|
+
} catch {
|
|
291
|
+
/* absent/unreadable → fall through to settings */
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const s = JSON.parse(readFileSync(join(claudeConfigDir(env), 'settings.json'), 'utf8')) as Record<string, unknown>
|
|
295
|
+
if (typeof s.apiKeyHelper === 'string' && s.apiKeyHelper.trim()) return true
|
|
296
|
+
} catch {
|
|
297
|
+
/* no settings / no helper */
|
|
298
|
+
}
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* True when codex is authenticated (won't block / error at startup). Positive
|
|
304
|
+
* evidence: `OPENAI_API_KEY` env, or an `auth.json` under $CODEX_HOME/~/.codex.
|
|
305
|
+
* Env-rooted for hermetic tests.
|
|
306
|
+
*/
|
|
307
|
+
export function codexAuthReady(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
308
|
+
if (env.OPENAI_API_KEY?.trim()) return true
|
|
309
|
+
const codexHome = env.CODEX_HOME?.trim()
|
|
310
|
+
const authPath = codexHome
|
|
311
|
+
? join(codexHome, 'auth.json')
|
|
312
|
+
: join(env.HOME?.trim() || homedir(), '.codex', 'auth.json')
|
|
313
|
+
return existsSync(authPath)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Advisory line for a runtime that is NOT authenticated (so the operator fixes it
|
|
318
|
+
* BEFORE launching peers). Returns null when the runtime is ready. Pure dispatch
|
|
319
|
+
* over the per-runtime probes above.
|
|
320
|
+
*/
|
|
321
|
+
export function runtimeAuthNote(runtime: OnboardRuntime, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
322
|
+
if (runtime === 'claude') {
|
|
323
|
+
if (claudeAuthReady(env)) return null
|
|
324
|
+
return (
|
|
325
|
+
'claude: NOT authenticated — the first peer would block on the login screen (a launcher cannot\n' +
|
|
326
|
+
' complete the browser OAuth flow). Before launching peers, either run `claude` once in your\n' +
|
|
327
|
+
' terminal to sign in (subscription), or set ANTHROPIC_API_KEY in the daemon environment.'
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
if (codexAuthReady(env)) return null
|
|
331
|
+
return (
|
|
332
|
+
'codex: NOT authenticated — the first codex peer cannot reach the model. Before launching codex\n' +
|
|
333
|
+
' peers, run `codex login` once (or set OPENAI_API_KEY in the daemon environment).'
|
|
334
|
+
)
|
|
335
|
+
}
|