felishub-gateway 0.0.0-stage → 1.2.1
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/LICENSE +21 -0
- package/README.md +440 -2
- package/package.json +22 -4
- package/src/agent.js +226 -0
- package/src/approvals.js +52 -0
- package/src/cli.js +81 -0
- package/src/commands.js +257 -0
- package/src/config.js +233 -0
- package/src/events.js +98 -0
- package/src/felishub.js +83 -0
- package/src/files.js +79 -0
- package/src/format.js +86 -0
- package/src/identity.js +70 -0
- package/src/index.js +819 -0
- package/src/mail.js +38 -0
- package/src/queue.js +51 -0
- package/src/scheduler.js +276 -0
- package/src/skills.js +58 -0
- package/src/state.js +72 -0
- package/src/tunnel.js +537 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FelisHub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,441 @@
|
|
|
1
|
-
#
|
|
1
|
+
# felishub-gateway
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Шлюз агента FelisHub. Держит сессии Agent SDK по разговорам, очереди ходов, решения по рискованным действиям и вопросы, расписания, бюджеты, журнал событий, файлы и почту между агентами. Шлюз сам подключается к FelisHub исходящим соединением и ничего не слушает: сообщения, решения и настройки FelisHub присылает по этому соединению от имени людей. Структуру работы — название задачи, объяснение решения, предложения, замечания, файлы-результаты — агент передаёт встроенными инструментами `felishub`. Telegram шлюз больше не использует.
|
|
4
|
+
|
|
5
|
+
Код шлюза живёт только здесь. Агенты отличаются профилем и своим каталогом: CLAUDE.md, скиллы, `bin/`, хуки.
|
|
6
|
+
|
|
7
|
+
## Подключение к FelisHub
|
|
8
|
+
|
|
9
|
+
Шлюз открывает одно соединение WebSocket к FelisHub (`wss://<адрес FelisHub>/agent/tunnel`) и держит его сам. Входящий порт, VPN и токены в окружении не нужны: хватает исходящего 443. Агент входит в FelisHub ключом Ed25519 из файла ключа, ключ выдаёт одноразовый код из окна «Подключить агента».
|
|
10
|
+
|
|
11
|
+
### Новый агент
|
|
12
|
+
|
|
13
|
+
1. В каталоге `gateway/` агента: `npm i felishub-gateway@1.2.1 @anthropic-ai/claude-agent-sdk@<версия> --save-exact`. Нужен Node.js 22.4 или новее.
|
|
14
|
+
2. `config.json` с `agentName` (остальное — по умолчанию, см. ниже).
|
|
15
|
+
3. Вход в Claude: `ANTHROPIC_API_KEY` (ключ Claude Console) или `CLAUDE_CODE_OAUTH_TOKEN` (токен подписки) в окружении службы.
|
|
16
|
+
4. В FelisHub: «Команда» → «Подключить агента», скопировать команду. Человек выполняет её сам в терминале сервера, в каталоге `gateway/`:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx felishub-gateway connect https://hub.example.com K7QM-4XPD-R2TA
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Команда создаёт ключ, проходит проверку FelisHub и печатает, где лежит ключ. Код одноразовый и действует 15 минут; в коде не важны регистр и дефисы.
|
|
23
|
+
5. Вернуться в мастер, закончить его — и запустить шлюз: `node src/index.js` (или `npx felishub-gateway` — шлюз без профиля). Запущенный раньше шлюз подхватит ключ сам в течение 5 секунд.
|
|
24
|
+
|
|
25
|
+
### Файл ключа
|
|
26
|
+
|
|
27
|
+
- Путь — `FELISHUB_KEY_FILE`, иначе `~/.config/felishub/<agentName>.json`. Ключ — вне каталога агента: внутри него агент прочитал бы ключ своим `Read` или приложил бы его файлом, поэтому команда и шлюз такой путь не принимают.
|
|
28
|
+
- Каталог ключа — отдельный, только для ключей шлюзов: любое упоминание этого каталога шлюз считает необратимым (см. ниже). Каталог, внутри которого лежит каталог агента, например домашняя папка, команда и шлюз не принимают.
|
|
29
|
+
- В файле — адрес FelisHub, пространство, имя агента и закрытый ключ. Файл пишется с правами `0600`, папка создаётся с `0700`. FelisHub хранит только открытый ключ.
|
|
30
|
+
- Ключ привязан к одному FelisHub и одному пространству. Повторный `connect` с новым кодом того же FelisHub перезаписывает ключ: это замена ключа или переезд в другое пространство. Ключ другого FelisHub команда без `--replace` не перезапишет, а запущенный шлюз на другой FelisHub не переходит до перезапуска.
|
|
31
|
+
- Шлюз перечитывает файл при каждом подключении и проверяет его при каждом пинге FelisHub. Новый ключ того же пространства вступает в силу, когда в FelisHub нажмут «Заменить ключ». Ключ другого пространства — сразу: шлюз переподключается туда, а разговоры и расписания прежнего пространства откладывает (см. «Безопасность»).
|
|
32
|
+
|
|
33
|
+
Строки для `.claude/settings.json` агента, чтобы агент не трогал ключ:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"permissions": {
|
|
38
|
+
"deny": ["Read(~/.config/felishub/**)", "Edit(~/.config/felishub/**)", "Write(~/.config/felishub/**)"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Если ключ лежит в другой папке (`FELISHUB_KEY_FILE`), запретите её так же.
|
|
44
|
+
|
|
45
|
+
`connect` не запускается из хода агента: внутри хода в окружении есть `OPS_AGENT_CONVERSATION`, и команда отказывает. Любой вызов Bash, Read, правки файла, Glob, Grep, WebFetch или MCP-инструмента агента, где упомянуты `felishub-gateway`, `felishub-key`, `cli.js … connect`, `.config` и `felishub` в одной команде, маска после `.config` или сразу под домашней папкой (`~/.c*`, `$HOME/.*`, `/home/<пользователь>/.c*`), «папка/файл» ключа, каталог ключа — полным путём или через `~`, `$HOME`, `${HOME}`, — или маска, под которую попадает файл ключа или его каталог (`/srv/k*/key.json`, `/srv/*`), шлюз всегда отдаёт людям как необратимое решение, что бы ни было в `irreversiblePatterns` и в правилах `allow`. `//` и `/./` в пути эту проверку не обходят.
|
|
46
|
+
|
|
47
|
+
### Связь с FelisHub
|
|
48
|
+
|
|
49
|
+
Шлюз переподключается сам. Что он пишет в журнал и что делать:
|
|
50
|
+
|
|
51
|
+
| Строка журнала | Что значит |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `tunnel: connected to hub.example.com as devops (workspace «Ромашка»), events after 1234` | на связи |
|
|
54
|
+
| `hub not connected` в строке запуска и подсказка «шлюз не подключён к FelisHub…» | файла ключа нет: выполните `connect`; шлюз проверяет файл каждые 5 секунд |
|
|
55
|
+
| `tunnel: waiting for FelisHub: «Подключить» or «Заменить ключ» (4000)` | ключ ждёт человека в мастере или в окне «Переподключить шлюз»; повтор каждые 5 секунд |
|
|
56
|
+
| `tunnel: refused (4003): …` | FelisHub отказал по причине из строки (агент отключён или выключен, шлюз устарел); повтор каждые 30 секунд |
|
|
57
|
+
| `tunnel: key not accepted (4004), waiting for a new key in …` | ключ не принят или заменён: шлюз не подключается, пока в файле не появится другой ключ |
|
|
58
|
+
| `tunnel: replaced by another process with the same key (4009), stopped until restart` | тот же ключ у другого процесса: остановите лишний шлюз и перезапустите этот |
|
|
59
|
+
| `tunnel: closed (1006), reconnecting in 7s` | обрыв связи: повтор от 1 до 30 секунд, после перезапуска FelisHub (1012) — через 1–5 секунд, при лимите попыток (4029) — через 30–60 секунд |
|
|
60
|
+
| `tunnel: hub silent for 55s, reconnecting` | соединение молча пропало, шлюз открывает новое |
|
|
61
|
+
|
|
62
|
+
Расписания работают и без связи с FelisHub: события копятся в журнале и доходят после подключения.
|
|
63
|
+
|
|
64
|
+
### Служба systemd
|
|
65
|
+
|
|
66
|
+
```ini
|
|
67
|
+
[Unit]
|
|
68
|
+
Description=FelisHub agent gateway (Agent SDK, outbound tunnel)
|
|
69
|
+
After=network-online.target
|
|
70
|
+
Wants=network-online.target
|
|
71
|
+
|
|
72
|
+
[Service]
|
|
73
|
+
User=agent
|
|
74
|
+
WorkingDirectory=/opt/my-agent/gateway
|
|
75
|
+
EnvironmentFile=/home/agent/.config/felishub/my-agent.env
|
|
76
|
+
ExecStart=/usr/bin/node /opt/my-agent/gateway/node_modules/felishub-gateway/src/cli.js
|
|
77
|
+
Restart=on-failure
|
|
78
|
+
|
|
79
|
+
[Install]
|
|
80
|
+
WantedBy=multi-user.target
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
В `my-agent.env` — вход в Claude (`ANTHROPIC_API_KEY` или `CLAUDE_CODE_OAUTH_TOKEN`) и `FELISHUB_KEY_FILE`, если ключ шлюза лежит не в `~/.config/felishub/<agentName>.json`. Файл должен быть доступен только пользователю службы (`chmod 600`). Агент с профилем запускается своим `src/index.js` вместо `cli.js`.
|
|
84
|
+
|
|
85
|
+
### Профиль агента
|
|
86
|
+
|
|
87
|
+
В каталоге `gateway/` агента:
|
|
88
|
+
|
|
89
|
+
- `package.json` и `package-lock.json` — зависимости `felishub-gateway` и `@anthropic-ai/claude-agent-sdk` из npm с точными версиями (`--save-exact`), их ставит `npm ci`. Версию SDK агент закрепляет сам: вместе с SDK приезжает CLI Claude Code, и агенты обновляют его независимо.
|
|
90
|
+
- `src/index.js` — профиль агента, вызов `startGateway(...)`.
|
|
91
|
+
- `config.json`, `state.json`, `schedules.json`. Шлюз ищет `config.json` в текущем каталоге или по пути из `OPS_GATEWAY_CONFIG`. Запуск: `node src/index.js` из `gateway/` агента.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { startGateway } from "felishub-gateway";
|
|
95
|
+
|
|
96
|
+
startGateway({
|
|
97
|
+
defaults: {
|
|
98
|
+
maxTurns: 240,
|
|
99
|
+
irreversiblePatterns: ["dns\\.py[\"']?\\s+(--json\\s+)?(renew|autorenew|ns|lock)\\b"],
|
|
100
|
+
},
|
|
101
|
+
instructions: ["Строка, которая добавится в системный промпт этого агента."],
|
|
102
|
+
turnOptions: ({ cfg, conversation, actor, prompt, model }) => ({
|
|
103
|
+
env: { MY_AGENT_VAR: "…" },
|
|
104
|
+
hooks: { PostToolUse: [{ matcher: "Read", hooks: [async () => ({})] }] },
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `defaults` — умолчания конфигурации этого агента поверх общих из `src/config.js`. Всё, что задано в `config.json`, их перекрывает.
|
|
110
|
+
- `instructions` — строки системного промпта после общих правил шлюза.
|
|
111
|
+
- `turnOptions` — вызывается перед каждым ходом. Получает `conversation` (`id`, `kind`, `title`, `model`, `budgetUsd`, `maxTurns`, у разговора коллеги ещё `peer`), `actor` (`id`, `name`, `role`), текст хода и модель. Возвращает переменные окружения сессии (`env`) и хуки Agent SDK (`hooks`). Хуки `PreToolUse` профиля идут после хука шлюза, см. «Разрешения».
|
|
112
|
+
|
|
113
|
+
В окружении сессии агента есть `OPS_AGENT_CONVERSATION` (id разговора), `OPS_AGENT_USER` (имя человека или агента, начавшего ход) и `OPS_AGENT_ROLE` (его роль). `FELISHUB_KEY_FILE` в окружение сессии не попадает: агент не узнаёт, где лежит ключ.
|
|
114
|
+
|
|
115
|
+
Проверка `.dev-unlock` общая: если файл лежит в корне агента, шлюз не стартует.
|
|
116
|
+
|
|
117
|
+
## config.json
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"agentName": "devops",
|
|
122
|
+
"agentDir": "..",
|
|
123
|
+
"timezone": "Europe/Moscow",
|
|
124
|
+
"model": null,
|
|
125
|
+
"maxConcurrentTurns": 1,
|
|
126
|
+
"maxTurns": 80,
|
|
127
|
+
"turnTimeoutSec": 1800,
|
|
128
|
+
"permissionTimeoutSec": 900,
|
|
129
|
+
"questionTimeoutSec": 1800,
|
|
130
|
+
"sessionIdleResetMin": 180,
|
|
131
|
+
"dailyBudgetUsd": null,
|
|
132
|
+
"irreversiblePatterns": [],
|
|
133
|
+
"mailMaxHops": 4,
|
|
134
|
+
"mailDailyLimit": 30
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Ключ | По умолчанию | Что это |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `agentName` | — | Обязателен. Имя агента для коллег и FelisHub: `a-z`, `0-9`, `_`, `-`, до 32 символов |
|
|
141
|
+
| `agentDir` | `..` | Каталог агента относительно `config.json`, там же `logs/events.jsonl` |
|
|
142
|
+
| `timezone` | `Europe/Moscow` | Часовой пояс расписаний, дневных бюджетов и времени в заголовке хода |
|
|
143
|
+
| `model` | `null` | Модель по умолчанию; `null` — модель Claude Code по умолчанию |
|
|
144
|
+
| `maxConcurrentTurns` | 1 | Сколько разговоров агент ведёт одновременно |
|
|
145
|
+
| `maxTurns` | 80 | Лимит шагов одного хода |
|
|
146
|
+
| `turnTimeoutSec` | 1800 | Ход дольше этого останавливается, исход `timeout` |
|
|
147
|
+
| `permissionTimeoutSec` | 900 | Сколько решение ждёт человека, потом отклоняется |
|
|
148
|
+
| `questionTimeoutSec` | 1800 | Сколько вопрос ждёт ответа |
|
|
149
|
+
| `sessionIdleResetMin` | 180 | Разговор без ходов дольше этого начинает новую сессию; 0 выключает |
|
|
150
|
+
| `dailyBudgetUsd` | `null` | Дневной бюджет разговора по умолчанию; `null` или 0 — без лимита |
|
|
151
|
+
| `irreversiblePatterns` | `[]` | Регулярные выражения. Для Bash проверяется команда, для правок файлов (`Write`, `Edit`, `MultiEdit`, `NotebookEdit`) — только путь, `<инструмент> {"file_path":…}`, для остальных инструментов — `<инструмент> <JSON ввода>`. Проверка идёт перед каждым вызовом Bash, Read, правки файла, Glob, Grep, WebFetch и MCP-инструментов, в том числе разрешённым правилом `allow` или хуком, см. «Разрешения». Совпало — решение уровня `irreversible`. Ключ шлюза и `connect` — `irreversible` всегда |
|
|
152
|
+
| `mailMaxHops` | 4 | Сколько раз подряд агенты пересылают друг другу запросы одной цепочки |
|
|
153
|
+
| `mailDailyLimit` | 30 | Запросов одному коллеге в сутки и столько же входящих от него |
|
|
154
|
+
|
|
155
|
+
Ключей `api` и `peers` нет: шлюз сам подключается к FelisHub, а коллег задают люди в FelisHub.
|
|
156
|
+
|
|
157
|
+
Окружение:
|
|
158
|
+
|
|
159
|
+
- `FELISHUB_KEY_FILE` — путь к файлу ключа вне каталога агента, в отдельном каталоге только для ключей; по умолчанию `~/.config/felishub/<agentName>.json`.
|
|
160
|
+
- `OPS_GATEWAY_CONFIG` — путь к `config.json`, если он не в текущем каталоге.
|
|
161
|
+
- Вход в Claude: `ANTHROPIC_API_KEY` (ключ Claude Console) или `CLAUDE_CODE_OAUTH_TOKEN` (токен подписки). Шлюз передаёт сессиям своё окружение, поэтому после смены ключа шлюз перезапускают.
|
|
162
|
+
|
|
163
|
+
## Переход с 0.4
|
|
164
|
+
|
|
165
|
+
Шлюз 1.0 и новее не запускается с конфигурацией от Telegram-версии и печатает, какие ключи удалить:
|
|
166
|
+
|
|
167
|
+
- из `config.json`: `chatId`, `ownerId`, `topics`, `approvals`, `ownerCanApprove`, `ownerCanWriteAnywhere`, `showToolCalls`, `modelShortcuts`, `ownerOnlyPatterns`, `peers.<имя>.topic`;
|
|
168
|
+
- из `defaults` профиля — те же ключи. `ownerOnlyPatterns` переименуй в `irreversiblePatterns`.
|
|
169
|
+
|
|
170
|
+
`TELEGRAM_BOT_TOKEN` больше не нужен. Разговоры начинаются заново: привязки тем не переносятся, сессии тем и расписания тем Telegram из `schedules.json` отбрасываются, старые события без поля `conversation` не отдаются через API. Из `state.json` без реестра разговоров переносятся только расходы. Нумерация событий продолжается.
|
|
171
|
+
|
|
172
|
+
Что проверить в профиле агента: `turnOptions` получает `conversation` и `actor` вместо `topic` и `threadId`; вместо `OPS_AGENT_TOPIC` в окружении `OPS_AGENT_CONVERSATION`, это строка вида `dm-1a2b3c4d`, а не число; `OPS_AGENT_ROLE` — роль человека в FelisHub (например «Администратор»), а не `admin`/`operator`/`viewer`. Правила форматирования в CLAUDE.md агента (Telegram HTML) замени на Markdown.
|
|
173
|
+
|
|
174
|
+
## Переход с 1.0
|
|
175
|
+
|
|
176
|
+
Шлюз 1.1 запускается с конфигурацией 1.0 как есть: новых ключей нет. Все изменения — дополнения: API FelisHub v4 новые поля и события пропускает, API v5 требует шлюз версии 1.1.0 или новее.
|
|
177
|
+
|
|
178
|
+
Что сделать при выкатке:
|
|
179
|
+
|
|
180
|
+
- `dailyBudgetUsd: null` в `config.json`: лимиты денег считает API FelisHub.
|
|
181
|
+
- `inbox/` в `.gitignore` репозитория агента: туда шлюз кладёт файлы от людей.
|
|
182
|
+
- Хуки агента не должны запрещать инструменты `mcp__felishub__*`: они ничего не меняют в системах.
|
|
183
|
+
- Поля `title`, `icon` и `example` во frontmatter умений (`.claude/skills/*/SKILL.md`) необязательны: без них приложение покажет `name` и `description`.
|
|
184
|
+
|
|
185
|
+
## Переход с 1.1.0 и 1.1.1
|
|
186
|
+
|
|
187
|
+
Шлюз 1.1.2 ставится вместо 1.1.0 или 1.1.1 без изменений в конфигурации. Протокол между шлюзом и агентом использует имя FelisHub: MCP-сервер `felishub`, инструменты `mcp__felishub__*`, метка `[felishub]` в первой строке сообщения. Старые имена шлюз не понимает и не выдаёт, а API FelisHub не подключает шлюз ниже 1.1.2.
|
|
188
|
+
|
|
189
|
+
Что сделать при выкатке:
|
|
190
|
+
|
|
191
|
+
- В CLAUDE.md, умениях и скриптах агента проверь метку `[felishub]` и имена инструментов `mcp__felishub__*`.
|
|
192
|
+
- В хуках и правилах разрешений используй имена инструментов `mcp__felishub__*`.
|
|
193
|
+
|
|
194
|
+
## Переход с 1.1
|
|
195
|
+
|
|
196
|
+
Шлюз 1.2 не запускается с конфигурацией и окружением 1.1 и пишет, что убрать:
|
|
197
|
+
|
|
198
|
+
- из `config.json` — `api` и `peers`;
|
|
199
|
+
- из `gateway.env` — `GATEWAY_API_TOKEN` и все `PEER_TOKEN_*`.
|
|
200
|
+
|
|
201
|
+
Что сделать при выкатке:
|
|
202
|
+
|
|
203
|
+
1. Остановить шлюз 1.1, поставить 1.2 (`npm ci` после `bin/release.sh`).
|
|
204
|
+
2. Убрать `api`, `peers` и токены, добавить `FELISHUB_KEY_FILE` в `gateway.env`, если ключ должен лежать не в `~/.config/felishub/`.
|
|
205
|
+
3. Выполнить `connect` с кодом из окна «Подключить агента» и пройти мастер.
|
|
206
|
+
4. Запустить шлюз. Коллег теперь задают в FelisHub: шаг «Агенты-коллеги» мастера или «Работает с агентами» → «Изменить» в профиле агента.
|
|
207
|
+
|
|
208
|
+
FelisHub не подключает шлюз ниже 1.2.0. Журнал событий, разговоры и расписания переходят как есть; события до подключения FelisHub не загружает.
|
|
209
|
+
|
|
210
|
+
## Переход с 1.2
|
|
211
|
+
|
|
212
|
+
Шлюз 1.2.1 ставится вместо 1.2.0 без изменений в `config.json` и окружении, если ключ шлюза лежит в отдельном каталоге (см. «Файл ключа»). Что меняется для агентов:
|
|
213
|
+
|
|
214
|
+
- Правила `allow` из `.claude/settings.json` агента начинают работать, см. «Разрешения». В 1.2.0 Claude Code молча их отбрасывал. Перед выкаткой перечитайте список: всё, что в нём есть, пойдёт без карточки.
|
|
215
|
+
- Если правило `allow` держится на хуке агента, который сам решает «разрешить», «спросить» или «запретить», проверьте, что хук не падает: при сбое с кодом, отличным от 2, и при выходе по таймауту вызов пройдёт без карточки. Раньше в этом случае приходила карточка.
|
|
216
|
+
- «Необратимое» проверяется перед каждым вызовом Bash, Read, Write, Edit, MultiEdit, NotebookEdit, Glob, Grep, WebFetch и MCP-инструментов. Разрешение из хука и чтение в папке агента, которое Claude Code разрешает сам, больше его не обходят. Новые карточки — у команд и чтений, которые совпали с `irreversiblePatterns` или упоминают `felishub-gateway`, например путь в `gateway/node_modules`. Agent, Task*, Skill, AskUserQuestion и другие внутренние инструменты Claude Code хук не проверяет, как и в 1.2.0: каждый вызов, который делает субагент, проходит ту же проверку.
|
|
217
|
+
- Для правок файлов `irreversiblePatterns` сравнивается только с путём: запись в журнал, где упомянуто что-то из списка, больше не становится карточкой «Необратимое».
|
|
218
|
+
- Правила `deny` и `ask` шлюз передаёт Claude Code вместе с `allow`: если Claude Code не примет `settings.json` целиком, запреты всё равно действуют, а вот хуки из файла — нет, см. «Разрешения».
|
|
219
|
+
- Защита ключа шлюза шире: маски, `//` и `/./` в пути, `.config` и `felishub` в одной команде, путь и каталог из `FELISHUB_KEY_FILE`. Держите ключ в отдельном каталоге: всё, что упоминает этот каталог, теперь приходит карточкой `irreversible`. Если каталог ключа содержит каталог агента, шлюз не запускается и просит перенести ключ.
|
|
220
|
+
- Подсказка «шлюз не подключён» называет папку, где выполнить `connect`, а без `config.json` команда и шлюз объясняют, где их запускать, вместо ошибки `ENOENT`.
|
|
221
|
+
- Если шлюз не смог войти в Claude, подсказка в разговоре просит проверить ключ в окружении шлюза и перезапустить шлюз.
|
|
222
|
+
|
|
223
|
+
## Разговоры и акторы
|
|
224
|
+
|
|
225
|
+
Разговор — отдельная сессия Agent SDK со своей очередью, бюджетом и журналом. Id: `^[a-z0-9][a-z0-9_-]{0,63}$`.
|
|
226
|
+
|
|
227
|
+
- `dm` — разговор, созданный API FelisHub через `PUT /api/conversations/:id`: личный чат с агентом (`dm-…`) или ветка задачи (`th-…`). Шлюз хранит его в `state.json`, удаляет — `DELETE /api/conversations/:id`.
|
|
228
|
+
- `peer` — запросы от коллеги, id `peer-<имя>`. Шлюз ведёт его сам для каждого коллеги из списка, который присылает FelisHub (`PUT /api/peers`), `PUT` разговора для него запрещён.
|
|
229
|
+
- `system` — служебный: в нём события паузы. Тоже зарезервирован.
|
|
230
|
+
|
|
231
|
+
Каждая запись в API называет актора — человека, от имени которого действует FelisHub: `actor: { id, name, role }`, все три поля — строки (`role` может быть пустой). Шлюз доверяет API: права проверяет FelisHub, шлюз записывает, кто что сделал. Роль попадает в заголовок хода как контекст для модели, а не как разрешение.
|
|
232
|
+
|
|
233
|
+
## Команды FelisHub
|
|
234
|
+
|
|
235
|
+
FelisHub шлёт команды по соединению шлюза кадрами `request` с методом и путём из таблиц ниже, шлюз отвечает кадром `response` с кодом и телом — теми же, что у HTTP API шлюза 1.1. Тело — JSON до 64 КБ (файл идёт отдельными двоичными кадрами, до 20 МБ), ошибки — `{ "error": "…" }` по-русски. `via` в записях — `web`, `desktop`, `mobile` или `telegram`. Неизвестный путь — 404 «нет такого метода API».
|
|
236
|
+
|
|
237
|
+
Перед каждым ответом шлюз присылает своё состояние, если оно изменилось, поэтому после ответа на команду у FelisHub уже свежее состояние. Ещё шлюз присылает его через 250 мс после событий, меняющих состояние, и раз в 30 секунд, если что-то изменилось.
|
|
238
|
+
|
|
239
|
+
### Состояние
|
|
240
|
+
|
|
241
|
+
`GET /api/status` (то же приходит кадром `status`):
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"agent": "devops", "version": "1.2.1", "startedAt": "…", "timezone": "Europe/Moscow", "model": null, "observedModel": "claude-opus-4-1",
|
|
246
|
+
"paused": false, "pausedBy": null,
|
|
247
|
+
"maxConcurrentTurns": 1, "dailyBudgetUsd": 5, "spendTodayUsd": 0.42,
|
|
248
|
+
"permissionTimeoutSec": 900, "questionTimeoutSec": 1800,
|
|
249
|
+
"irreversiblePatterns": ["dns\\.py[\"']?\\s+(--json\\s+)?(renew|autorenew|ns|lock)\\b"],
|
|
250
|
+
"mailMaxHops": 4, "mailDailyLimit": 30, "mailToday": { "seo": { "sent": 3, "received": 1 } },
|
|
251
|
+
"skills": [{ "name": "healthcheck", "title": "Проверить серверы", "description": "Снять состояние серверов…", "icon": "server", "example": "Как дела на серверах?" }],
|
|
252
|
+
"conversations": [{
|
|
253
|
+
"id": "dm-1a2b3c4d", "kind": "dm", "title": "DevOps · Анна", "model": null, "budgetUsd": null, "maxTurns": null,
|
|
254
|
+
"spendTodayUsd": 0.42,
|
|
255
|
+
"running": { "turnId": "9f3c2a1b", "startedAt": "…", "actorName": "Анна" },
|
|
256
|
+
"queued": 0,
|
|
257
|
+
"session": { "id": "5d1e9c0a", "turns": 3, "updatedAt": "…" },
|
|
258
|
+
"pending": { "id": "c0ffee12", "kind": "permission", "turnId": "9f3c2a1b", "tier": "irreversible", "tool": "Bash", "input": "bin/dns.py renew …", "createdAt": "…", "expiresAt": "…" },
|
|
259
|
+
"lastEventAt": "…"
|
|
260
|
+
}],
|
|
261
|
+
"schedules": [{ "id": "ab12", "conversation": "dm-1a2b3c4d", "kind": "cron", "cron": "0 9 * * 1-5", "prompt": "…", "model": null, "enabled": true, "nextRun": "…", "lastRun": "…", "runs": 4, "createdAt": "…", "createdBy": { "id": "1", "name": "Анна", "role": "Администратор" } }]
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- `pausedBy` — `{ by, byId, at }`, пока агент на паузе.
|
|
266
|
+
- `observedModel` — модель из последнего `system/init` Agent SDK: какую модель SDK на самом деле запустил. До первого хода после установки — `null`.
|
|
267
|
+
- `permissionTimeoutSec`, `questionTimeoutSec`, `irreversiblePatterns`, `mailMaxHops`, `mailDailyLimit` — значения из конфигурации, только для показа. `mailToday` — по каждому коллеге запросы ему (`sent`) и от него (`received`) за сегодня в `timezone`: те самые счётчики, по которым срабатывает `mailDailyLimit`.
|
|
268
|
+
- `skills` — умения из `<agentDir>/.claude/skills/*/SKILL.md`, по имени каталога. Из frontmatter: `name` (иначе имя каталога), `description`, необязательные `title` (иначе `name`), `icon` и `example` (иначе `null`). Шлюз перечитывает их не чаще раза в минуту.
|
|
269
|
+
- `model`, `budgetUsd`, `maxTurns` разговора — его собственные значения; `null` значит умолчание агента (`model`, `dailyBudgetUsd`, `maxTurns` из конфигурации).
|
|
270
|
+
- `queued` — ходы в очереди без текущего. `pending` у разрешения несёт `input` так же, как событие `permission`, и `inputTruncated: true`, если вход обрезан. `pending` у вопроса — `{ id, kind: "question", turnId, questions, createdAt, expiresAt }`, где `questions` — `[{ question, header, multiSelect, options: [label] }]`.
|
|
271
|
+
|
|
272
|
+
### Разговоры
|
|
273
|
+
|
|
274
|
+
- `PUT /api/conversations/:id` `{ kind: "dm", title, model?, budgetUsd?, maxTurns? }` → `201 { ok, created: true }` или `200 { ok, created: false }`. Замена целиком: не переданные `model`, `budgetUsd`, `maxTurns` становятся `null`. `title` — до 200 символов, переводы строк схлопываются.
|
|
275
|
+
- `DELETE /api/conversations/:id` `{ actor }` → `{ ok }`: удаляет разговор и его сессию; история событий и расход за день остаются. 409, пока ход идёт или ждёт в очереди; `system` и `peer-…` удалить нельзя (400). Следующий `PUT` с тем же id создаёт разговор заново, сессия начнётся с нуля.
|
|
276
|
+
- `POST /api/conversations/:id/messages` `{ actor, text, model?, via, allowOverBudget? }` → `202 { ok, queued, turnId }`, `queued` — сколько ходов этого разговора впереди, `turnId` — id будущего хода: его несут эхо `user` и все события хода. `text` — от 1 до 8000 символов, `model` — модель только для этого хода. 404 — нет разговора, 409 — «агент на паузе» или дневной бюджет разговора исчерпан (если не `allowOverBudget: true`). Сообщение никогда не отвечает на ожидающее решение или вопрос: оно встаёт в очередь новым ходом.
|
|
277
|
+
- `POST /api/conversations/:id/cancel` `{ actor }` → `{ ok, wasRunning, wasQueued }`: отклоняет ожидающее решение (`via: "cancel"`), останавливает текущий ход и снимает с очереди все ходы разговора, которые ещё не начались. У каждого снятого хода сразу пишется `turn-end` с исходом `cancelled`, `durationMs: 0` и без `turn-start`; когда до него доходит очередь, шлюз его пропускает. `wasRunning` — шёл ли ход, `wasQueued` — сняты ли ходы из очереди.
|
|
278
|
+
- `POST /api/conversations/:id/reset` `{ actor }` → `{ ok }`: новый контекст — следующая задача начнёт новую сессию. 409, пока идёт ход.
|
|
279
|
+
|
|
280
|
+
### Файлы
|
|
281
|
+
|
|
282
|
+
- `PUT /api/conversations/:id/files/:name` — файл до 20 МБ (двоичные кадры, размер объявлен заранее) → `{ ok, path, size }`. Файл ложится в `<agentDir>/inbox/<id разговора>/<name>`, тот же путь заменяется; `path` — относительный путь, его API называет в тексте сообщения агенту. `name` — до 200 символов в кодировке URL, без `/`, `\` и управляющих символов, не начинается с точки. 404 — нет разговора, 413 — больше 20 МБ (шлюз отвечает сразу, не дожидаясь конца файла), 400 — пришло не столько байт, сколько объявлено. Если `inbox` ведёт за пределы каталога агента (ссылкой), запись отклоняется.
|
|
283
|
+
- `GET /api/files/:fileId` — артефакт, который агент отдал инструментом `felishub.attach` (событие `artifact`): ответ `{ name, mime, size }`, потом тело файла двоичными кадрами по 64 КБ. Если FelisHub отменил запрос, шлюз перестаёт читать файл. 404 — нет такого `fileId` или файл с тех пор удалён, переехал за пределы каталога агента или вырос больше 20 МБ. Шлюз помнит артефакты 14 дней.
|
|
284
|
+
|
|
285
|
+
### Решения и вопросы
|
|
286
|
+
|
|
287
|
+
- `POST /api/approvals/:id` `{ actor, decision: "allow" | "deny", comment?, via }` → `{ ok }`. Комментарий к отказу модель получает вместе с отказом. Решение уже принято, истекло или его нет — 404 `{ error, decidedBy? }`: `decidedBy` — имя того, кто решил первым.
|
|
288
|
+
- `POST /api/questions/:id` `{ actor, answers: { "<текст вопроса>": "<ответ>" }, via }` → `{ ok }`. Отвечает на весь набор вопросов сразу; ответ — вариант или свой текст до 2000 символов. Нет ответа на какой-то вопрос или лишний ключ — 400. Уже отвечено — 404, как у решений.
|
|
289
|
+
|
|
290
|
+
Уровень решения шлюз считает сам: `irreversible`, если команда совпала с `irreversiblePatterns`, иначе `normal`. Кто может решать необратимое, решает FelisHub. Без ответа решение отклоняется через `permissionTimeoutSec`, вопрос — через `questionTimeoutSec` (`via: "timeout"`).
|
|
291
|
+
|
|
292
|
+
Шаблоны проверяются и по исходному тексту, и по нормализованному, чтобы кавычки не прятали команду (`bin/dns.py ren''ew` — тоже `irreversible`). Для Bash исходный текст — команда, для остальных инструментов — `<инструмент> <JSON ввода>`, а если в вводе есть строка `command`, второй вариант — тот же JSON с нормализованной `command`. Нормализация снимает кавычки и экранирование, как shell при разборе слов:
|
|
293
|
+
|
|
294
|
+
- `'…'` — содержимое как есть, кавычки убираются;
|
|
295
|
+
- `"…"` и `$"…"` — кавычки убираются, `\"`, `\\`, `\$`, `` \` `` дают сам символ, `\` с переводом строки исчезает, остальные `\` остаются;
|
|
296
|
+
- `$'…'` — кавычки убираются, `\xHH`, `\uHHHH`, `\NNN` (восьмеричное), `\n`, `\t`, `\r` декодируются, `\<символ>` даёт символ;
|
|
297
|
+
- вне кавычек `\<символ>` даёт символ, `\` с переводом строки исчезает;
|
|
298
|
+
- незакрытая кавычка тянется до конца строки.
|
|
299
|
+
|
|
300
|
+
Подстановки (`$VAR`, `$(…)`) не раскрываются. Тот же нормализатор повторяет API FelisHub для своих правил.
|
|
301
|
+
|
|
302
|
+
### Расписания
|
|
303
|
+
|
|
304
|
+
- `POST /api/schedules` `{ actor, conversation, kind: "cron" | "once", cron? | at?, prompt, model? }` → `201 { ok, schedule }`. `cron` — 5 полей (минута час день месяц день-недели, в `timezone`), `at` — ISO 8601 не раньше чем через 30 секунд. 404 — нет разговора, 409 — агент на паузе.
|
|
305
|
+
- `PATCH /api/schedules/:id` `{ actor, enabled?, cron?, at?, prompt?, model? }` → `{ ok, schedule }`. `cron` меняется только у `cron`, `at` — только у `once`; при ошибке расписание не меняется. Если меняются `prompt` или `model`, `createdBy` становится `actor`: запуски идут от имени того, кто написал текст.
|
|
306
|
+
- `DELETE /api/schedules/:id` `{ actor }` → `{ ok }`.
|
|
307
|
+
|
|
308
|
+
Запуск идёт от имени создателя (`actor` из `POST`) с `via: "schedule"`, в отдельной сессии `job:<id>`. Рядом никого нет, поэтому решения и вопросы в нём отклоняются сразу, без событий. Запуск пропускается, если агент на паузе или дневной бюджет разговора исчерпан (тогда в разговоре событие `error`). Пропущенный больше чем на час запуск (шлюз не работал) не догоняется.
|
|
309
|
+
|
|
310
|
+
### Пауза
|
|
311
|
+
|
|
312
|
+
`POST /api/pause` `{ actor, paused }` → `{ ok, paused }`. Пауза останавливает текущие ходы (исход `paused`), отклоняет ожидающие решения и вопросы (`via: "pause"`), отвечает 409 на новые сообщения и расписания и пропускает запуски расписаний. Ходы, которые уже стояли в очереди, ждут возобновления. Запросы коллег на паузе сразу получают отказ. Пауза переживает перезапуск шлюза. Повторная пауза или возобновление без изменения состояния — без события.
|
|
313
|
+
|
|
314
|
+
### Коллеги и почта
|
|
315
|
+
|
|
316
|
+
- `PUT /api/peers` `{ peers: [{ name, about }] }` → `{ ok }`: коллеги агента от FelisHub, до 50, `name` — `a-z`, `0-9`, `_`, `-`, до 32 символов, не своё имя, `about` — одна строка до 300 символов. Список заменяется целиком и сохраняется в `state.json`, поэтому разговоры `peer-…` и их расписания переживают перезапуск, даже когда FelisHub недоступен.
|
|
317
|
+
- `POST /api/mail` `{ id, from, type, text, replyConversation, hops, re? }` → `202 { ok }`: письмо коллеги, которое переслал FelisHub. `from` ставит FelisHub; если его нет среди коллег — 403 «агент … не в коллегах — FelisHub не разрешал вам переписываться». Повтор `id` — `200 { ok, duplicate: true }`, `hops` больше `mailMaxHops` — 409, сверх `mailDailyLimit` — 429.
|
|
318
|
+
|
|
319
|
+
### Доставка событий
|
|
320
|
+
|
|
321
|
+
Шлюз сам присылает события кадрами `events`, начиная с номера, который FelisHub назвал при подключении (`welcome.after`): по 200 событий и не больше 1 МБ в кадре, не больше 4 неподтверждённых кадров. FelisHub подтверждает каждый кадр (`ack`), после обрыва шлюз начинает снова с его номера, поэтому события доходят хотя бы раз и без пропусков. Если у FelisHub номер больше, чем в журнале шлюза (новый сервер, удалённый `events.jsonl`), шлюз продолжает нумерацию с него. Событие больше 1 МБ не отправляется, шлюз пишет об этом в журнал.
|
|
322
|
+
|
|
323
|
+
## События
|
|
324
|
+
|
|
325
|
+
`<каталог агента>/logs/events.jsonl`, одна JSON-строка на событие: `{ id, ts, conversation, kind, ...поля }`. `id` растёт и продолжается после перезапуска.
|
|
326
|
+
|
|
327
|
+
Все события хода несут `turnId` — 8 шестнадцатеричных символов. Событие, которое ставит ход в очередь (`user`, `mail-in`), несёт `turnId` будущего хода.
|
|
328
|
+
|
|
329
|
+
| `kind` | Поля |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `user` | `turnId`, `actorId`, `from`, `via`, `text`, `model?` |
|
|
332
|
+
| `job` | `turnId`, `jobId`, `text` |
|
|
333
|
+
| `turn-start` | `turnId`, `from`, `actorId`, `via` (`web` · `desktop` · `mobile` · `telegram` · `schedule` · `agent`), `model` |
|
|
334
|
+
| `agent` | `turnId`, `text` — Markdown, `usage` |
|
|
335
|
+
| `tool` | `turnId`, `toolUseId`, `tool`, `input` (команда, путь файла или JSON ввода, до 2000 символов), `target?`, `usage`, `diff?` |
|
|
336
|
+
| `tool-result` | `turnId`, `toolUseId`, `isError`, `durationMs`, `output` (до 2000 символов) |
|
|
337
|
+
| `permission` | `turnId`, `approvalId`, `toolUseId?`, `tool`, `input` (у `Bash` — команда целиком, у остальных инструментов — весь вход JSON-объектом, до 64 КБ), `inputTruncated?` (`true`, если вход длиннее 64 КБ и обрезан), `tier` (`normal` · `irreversible`), `expiresAt`, `explain?` |
|
|
338
|
+
| `permission-result` | `turnId`, `approvalId`, `decision`, `by`, `byId`, `via` (`web` · `desktop` · `mobile` · `telegram` · `timeout` · `cancel` · `pause`), `comment?` |
|
|
339
|
+
| `question` | `turnId`, `approvalId`, `questions`, `expiresAt` |
|
|
340
|
+
| `question-result` | `turnId`, `approvalId`, `answers`, `by`, `byId`, `via` |
|
|
341
|
+
| `title` | `turnId`, `title` (до 80), `short?` (до 16) |
|
|
342
|
+
| `task-suggestion` | `turnId`, `title`, `reason?` |
|
|
343
|
+
| `proposal` | `turnId`, `proposalId` (8 шестнадцатеричных), `text`, `title?` |
|
|
344
|
+
| `remark` | `turnId`, `text` |
|
|
345
|
+
| `artifact` | `turnId`, `fileId` (uuid), `name`, `size`, `mime`, `title?` |
|
|
346
|
+
| `turn-end` | `turnId`, `costUsd`, `turns`, `isError`, `limitReached`, `durationMs`, `outcome` (`done` · `failed` · `cancelled` · `timeout` · `limit` · `paused`), `by`, `byId` (кто остановил или отменил ход, иначе `null`) |
|
|
347
|
+
| `error` | `turnId?`, `text` — объяснение для человека |
|
|
348
|
+
| `session-reset` | `by`, `byId`, `reason` (`manual` · `idle`); у `idle` — `turnId`, `by` и `byId` равны `null` |
|
|
349
|
+
| `mail-out` | `turnId?` (нет у отказа на запрос коллеги, пришедший на паузе или при исчерпанном бюджете), `mailId`, `to`, `mailType`, `re`, `text`, `hops`, `failed?`, `error?`, `refusal?` |
|
|
350
|
+
| `mail-in` | `turnId?`, `mailId`, `from`, `mailType`, `re`, `text`, `hops` |
|
|
351
|
+
| `agent-paused`, `agent-resumed` | `by`, `byId`; разговор `system` |
|
|
352
|
+
| `schedule-changed` | `scheduleId`, `action` (`created` · `updated` · `deleted`), `by`, `byId`; разговор расписания |
|
|
353
|
+
|
|
354
|
+
- `usage` — `{ input, output, cacheRead, cacheWrite }`: токены хода накопительно к моменту события, вместе с субагентами (по `usage` сообщений ассистента Agent SDK; сообщения с одним id считаются один раз). По ним API оценивает стоимость идущего хода, настоящая — `costUsd` в `turn-end`.
|
|
355
|
+
- `tool` и `tool-result` связаны `toolUseId` — id блока `tool_use` в Agent SDK; `permission` несёт его же, если SDK его передал. Инструменты `mcp__felishub__*` событий `tool` и `tool-result` не дают: у каждого своё событие.
|
|
356
|
+
- `target` — хост, на котором выполнен шаг: строковое поле `profile` во вводе инструмента (у `mcp__ssh-mcp__*` — профиль сервера), до 64 символов. Нет `profile` — нет и `target`.
|
|
357
|
+
- `diff` — у `Edit`, `MultiEdit` и `Write`: `{ added, removed, lines }`. Счётчики — по полному тексту правки, `lines` — строки с префиксом `+`, `-` или пробел (до трёх строк контекста вокруг изменения), не больше 40 строк и 4000 символов. У `Write` все строки добавлены, у `MultiEdit` правки идут подряд.
|
|
358
|
+
- `tool-result` приходит из блоков `tool_result` сообщений Agent SDK (без субагентов). `output` — текст результата; у инструментов с командой (`input.command`) — последние 2000 символов, у остальных — первые. `durationMs` — от вызова или от решения человека, если оно было, до результата. Отказ человека — тоже результат с `isError: true`.
|
|
359
|
+
- `explain` у `permission` — `{ summary, changes, reason, check, tags, costUsd }` из `felishub.explain` этого хода (см. ниже): `changes` и `tags` — массивы строк (пустые, если агент их не дал), `check` — `{ summary?, lines: [{ kind: "context" | "minus" | "plus", text }] }` или `null`, `costUsd` — число или `null`. Это слова агента, не проверка шлюза.
|
|
360
|
+
- `refusal` у `mail-out` — `hops` или `daily`: шлюз сам не отправил запрос коллеге по `mailMaxHops` или `mailDailyLimit`. У такого события `failed: true` и свой `mailId`, но письмо никуда не уходило.
|
|
361
|
+
|
|
362
|
+
## Инструменты felishub
|
|
363
|
+
|
|
364
|
+
В каждом ходе модели доступен встроенный MCP-сервер `felishub`. Его инструменты разрешены без решения человека и ничего не меняют в системах: они передают приложению структуру работы. Строки текста схлопываются в одну, пустые значения отклоняются, необязательное поле, которое агент не передал, в событии отсутствует.
|
|
365
|
+
|
|
366
|
+
| Инструмент | Ввод | Что делает шлюз |
|
|
367
|
+
|---|---|---|
|
|
368
|
+
| `mcp__felishub__title` | `title` (до 80), `short?` (до 16) | событие `title` — название задачи; API не применяет его, если человек переименовал задачу |
|
|
369
|
+
| `mcp__felishub__suggest_task` | `title` (до 80), `reason?` (до 300) | событие `task-suggestion`: быстрый вопрос оказался работой, приложение предложит вынести его в задачу |
|
|
370
|
+
| `mcp__felishub__explain` | `summary` (до 300), `changes?` (до 10 строк), `reason` (до 500), `check?` `{ summary?, lines: [{ kind, text }] }` (до 20 строк), `tags?` (до 5), `costUsd?` | хранит объяснение до следующего `permission` этого хода и прикладывает к нему полем `explain`; без запроса разрешения до конца хода объяснение пропадает |
|
|
371
|
+
| `mcp__felishub__propose` | `text` (до 4000), `title?` (до 80) | событие `proposal` со своим `proposalId`: предложение вместо действия, в запусках по расписанию — вместо всего, что требует решения |
|
|
372
|
+
| `mcp__felishub__remark` | `text` (до 500) | событие `remark`: работа сделана с оговорками |
|
|
373
|
+
| `mcp__felishub__attach` | `path` (до 1000), `title?` (до 200) | событие `artifact` и запись для `GET /api/files/:fileId`. Только обычный файл до 20 МБ внутри каталога агента и не в каталоге шлюза (там `config.json`, `state.json` и `gateway.env`); ссылки раскрываются до проверки. Иначе модель получает отказ и события нет |
|
|
374
|
+
|
|
375
|
+
## Системный промпт
|
|
376
|
+
|
|
377
|
+
Шлюз добавляет к пресету `claude_code`: сообщения приходят из FelisHub (веб, компьютер, телефон); отвечать в GitHub Flavored Markdown (заголовки, списки, таблицы, блоки кода, без HTML); рискованные действия становятся карточками решений, которые человек нажимает в приложении; вопросы — через AskUserQuestion; инструменты `felishub`: название новой задачи, объяснение перед действием с решением, предложение вынести вопрос в задачу, замечание к итогу, предложение вместо действия в запуске по расписанию, файл-результат через `attach`; файлы от людей лежат в `inbox/<id разговора>/`; журнал сессии — `journal/YYYY-MM-DD-<id разговора>.md`. Дальше правила коллег и разговора коллеги, затем `instructions` профиля.
|
|
378
|
+
|
|
379
|
+
Первая строка каждого хода — метаданные шлюза:
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
[felishub] от: Анна (Администратор), разговор «DevOps · Анна» #dm-1a2b3c4d, 01.10.2026, 14:20:05
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
У запуска по расписанию к ней добавляется номер расписания и правило «рядом никого нет»: всё, что требует решения, агент предлагает через `felishub.propose`. Ход по почте коллег начинается строкой `[agent] от: <коллега> (агент), запрос #<id>, <время>` или `[agent] ответ от <коллега> на запрос #<id> («…»), <время>`. Такие метки в начале строк текста сообщения шлюз заменяет на «(ложная метка)», в том числе за невидимыми символами и в широких скобках `[]`. Название разговора в метаданных и в системном промпте шлюз очищает от кавычек «», квадратных скобок, невидимых символов и переводов строк: название задают люди и агенты, и оно не должно выдавать себя за метаданные.
|
|
386
|
+
|
|
387
|
+
## Почта между агентами
|
|
388
|
+
|
|
389
|
+
Если у агента есть коллеги (их задаёт FelisHub), в ходах модели доступен инструмент `mcp__agents__ask` (`agent`, `text`) без решения человека: у получателя свои решения. Почта идёт только через FelisHub и только между коллегами одного пространства.
|
|
390
|
+
|
|
391
|
+
1. Агент A вызывает инструмент. Шлюз проверяет лимиты (`mailMaxHops`, `mailDailyLimit`): если лимит исчерпан, модель получает отказ, а в журнал пишется `mail-out` с `failed: true` и `refusal`. Иначе шлюз отправляет FelisHub запрос `POST /mail` с телом `{ id, to, type: "ask", text, replyConversation, hops }`: 3 попытки с паузами 1 и 3 секунды, 15 секунд на попытку, отказ FelisHub с кодом 4xx не повторяется. Ход A не ждёт ответа. Модель получает сведения, а не указания, и их можно показать людям как есть: «Запрос #<id> передан агенту <имя>; ответ придёт отдельным сообщением в этот разговор.» или «Запрос агенту <имя> не доставлен: <причина>. Связи с агентом сейчас нет, повтор в этом ходе ничего не даст.»
|
|
392
|
+
2. FelisHub проверяет, что B — коллега A в том же пространстве и на связи, ставит `from` сам и пересылает письмо шлюзу B (`POST /api/mail`). Шлюз B отвечает 202 и ставит ход в разговор `peer-A`. Если B на паузе или бюджет разговора `peer-A` исчерпан, A сразу получает отказ.
|
|
393
|
+
3. Итоговый текст хода B (или отказ, или описание сбоя), не длиннее 20000 символов, уходит A тем же путём как `{ type: "answer", re, replyConversation, hops }`.
|
|
394
|
+
4. Шлюз A принимает один ответ на свой запрос `re` и только от того агента, которому его отправил, остальные отбрасывает. Ответ запускает ход в разговоре, откуда ушёл запрос.
|
|
395
|
+
|
|
396
|
+
## Разрешения
|
|
397
|
+
|
|
398
|
+
Шлюз запускает Claude Code в папке агента с `settingSources: ["project"]` и `permissionMode: "default"`. Из `.claude/settings.json` агента работают правила `deny` и `ask`, хуки и MCP-серверы из `.mcp.json` (при `enableAllProjectMcpServers`). `settings.local.json` и настройки из `~/.claude` шлюз не подключает.
|
|
399
|
+
|
|
400
|
+
Правила `allow` из настроек проекта Claude Code применяет только в папке, которой доверяют, а шлюз доверие не ставит. Поэтому перед каждым ходом шлюз сам читает `permissions.allow`, `permissions.deny` и `permissions.ask` из `<agentDir>/.claude/settings.json` и передаёт строки из них в Agent SDK одним набором, как настройки запуска (`settings`). Правила совпадают так же, как в Claude Code: `./`, `/`, `~`, имена MCP-инструментов; неправильное правило ничего не разрешает и не задевает соседние. Правка файла действует со следующего хода, без перезапуска. Если файл не читается как JSON, ход заканчивается ошибкой «.claude/settings.json агента не читается: …» с подсказкой исправить файл; сессия разговора при этом не сбрасывается.
|
|
401
|
+
|
|
402
|
+
Если файл читается как JSON, но Claude Code его не принимает — например, `permissions.defaultMode` с неизвестным режимом или `model` не строкой, — Claude Code пропускает весь файл: хуки, `enableAllProjectMcpServers` и остальные настройки из него не действуют. Правила `allow`, `deny` и `ask` шлюз при этом всё равно применяет, а хуки агента молча выключены. После правки `settings.json` проверьте, что хуки агента срабатывают: попросите агента о действии, которое хук должен запретить, и убедитесь, что пришёл отказ хука.
|
|
403
|
+
|
|
404
|
+
Как решается вызов:
|
|
405
|
+
|
|
406
|
+
1. `deny` из правила или хука — отказ без карточки.
|
|
407
|
+
2. Первым в `PreToolUse` идёт хук шлюза. Он проверяет Bash, Read, Write, Edit, MultiEdit, NotebookEdit, Glob, Grep, WebFetch и MCP-инструменты, кроме `mcp__felishub__*` и `mcp__agents__ask`. Если вызов совпал с `irreversiblePatterns` или с защитой ключа шлюза, хук отвечает «спросить», и приходит карточка `irreversible` — даже когда вызов разрешает правило `allow`, хук профиля или агента или сам Claude Code (чтение внутри папки агента). Agent, Task*, Skill, AskUserQuestion и другие внутренние инструменты Claude Code хук пропускает: каждый вызов, который делает субагент, проходит этот же хук.
|
|
408
|
+
3. «Спросить» из хука агента или правила `ask` важнее `allow` — карточка `normal`.
|
|
409
|
+
4. `allow` из правила или хука и чтение внутри папки агента, которое Claude Code разрешает сам, — вызов идёт без карточки.
|
|
410
|
+
5. Всё остальное — карточка `normal`.
|
|
411
|
+
|
|
412
|
+
Правило `allow` не добавляет агенту инструментов, которые Claude Code по умолчанию прячет: `Glob(./**)` и `Grep(./**)` не включают Glob и Grep, агент ищет через `find` и `grep` в Bash.
|
|
413
|
+
|
|
414
|
+
Запись инструментом Write Claude Code сверяет с правилами `Edit(…)`. Поэтому запрет записи пишите как `Edit(путь)`, `Write(путь)` — по желанию рядом. Один `deny Write(…)` не перекрывает `allow Edit(…)`: Write пройдёт по разрешению без карточки.
|
|
415
|
+
|
|
416
|
+
Хук агента, который упал с кодом, отличным от 2, или не уложился в свой `timeout`, Claude Code считает промолчавшим: если вызов разрешён правилом `allow`, он пройдёт без карточки. Не разрешайте через `allow` то, что держится только на хуке, и не разрешайте команды, которые читают произвольные файлы (`cat`, `git diff --no-index`): защита ключа ловит путь по тексту команды, а не любой способ до него добраться.
|
|
417
|
+
|
|
418
|
+
## Безопасность
|
|
419
|
+
|
|
420
|
+
- Шлюз только подключается к FelisHub и ничего не слушает. `wss://` обязателен; `ws://` — только для `localhost`, `127.0.0.1` и `[::1]`.
|
|
421
|
+
- После подключения по сети не ходит ни один многоразовый секрет: каждое подключение — подпись Ed25519 над одноразовым числом FelisHub и адресом FelisHub.
|
|
422
|
+
- Что может тот, у кого ключ: держать сессию этого агента — получать сообщения людей, карточки решений и вопросы, загруженные файлы; присылать события в места этого агента; писать его коллегам. Решать, отвечать за людей, ставить паузу и трогать чужих агентов, чужие передачи задач и другие пространства ключ не может. Каждый новый адрес подключения попадает в журнал FelisHub и в подсказку «В сети · с …». Украденный ключ заменяют через «Переподключить шлюз».
|
|
423
|
+
- Никогда не запускайте два шлюза с одним ключом. Новое подключение вытесняет старое, вытесненный шлюз пишет «этот шлюз вытеснен другим процессом с тем же ключом», больше не подключается и не запускает расписания, пока его не перезапустят.
|
|
424
|
+
- При переезде в другое пространство разговоры и расписания прежнего откладываются в `state.workspace-<номер>.json` и `schedules.workspace-<номер>.json` рядом с `config.json`; при возврате они возвращаются. Журнал событий остаётся общим, нумерация продолжается; каждое событие помечено пространством, в котором оно произошло, и FelisHub получает только события своего пространства.
|
|
425
|
+
- Сервер агента — граница доверия: кто вошёл на него пользователем шлюза, может всё, что может шлюз. Держите ключ вне каталога агента и запретите агенту читать его (строки выше), а `connect` выполняйте только сами.
|
|
426
|
+
- Файлы: человек кладёт файл только в `inbox/<разговор>/` каталога агента, агент отдаёт только файлы из каталога агента вне каталога шлюза. Ссылки раскрываются до проверки, временный файл загрузки создаётся без перехода по ссылкам.
|
|
427
|
+
- Всё, что пришло из инструментов `felishub`, — слова агента: приложение помечает их так и никогда не показывает вместо команды.
|
|
428
|
+
- Команды агента видят окружение шлюза, в том числе `ANTHROPIC_API_KEY` и `CLAUDE_CODE_OAUTH_TOKEN`, а их вывод уходит в FelisHub. Шлюз такие команды сам не ловит: запретите их в `.claude/settings.json` агента (`deny`: `Bash(env:*)`, `Bash(printenv:*)`), а остальные способы (`set`, `export -p`, `ps e`, `/proc/*/environ`) добавьте в `irreversiblePatterns` или хуки агента.
|
|
429
|
+
|
|
430
|
+
## Выпуск версии
|
|
431
|
+
|
|
432
|
+
1. Правишь код здесь, поднимаешь `version` в `package.json`, прогоняешь `npm test`.
|
|
433
|
+
2. Агент со шлюзом из npm: после публикации версии впиши её в `gateway/package.json` агента и выполни `npm install` в `gateway/`.
|
|
434
|
+
3. Агент со шлюзом из архива в `vendor/`: `bin/release.sh <каталог gateway агента>…` прогоняет тесты, собирает tgz, убирает из каждого агента старый `agent-gateway` (зависимость, `node_modules/agent-gateway`, `vendor/agent-gateway-*.tgz`) и прежние `vendor/felishub-gateway-*.tgz`, сохраняет заменяемые файлы в резервную копию в `_dev/backups/` рядом с каталогом шлюза, кладёт новый tgz в `vendor/` и ставит его — `package.json` и `package-lock.json` агента меняются только в этой зависимости.
|
|
435
|
+
4. В репозитории агента коммитишь `gateway/package.json`, `gateway/package-lock.json` и `gateway/vendor/`, если он есть, на сервере агента — `npm ci` в `gateway/` и перезапуск шлюза.
|
|
436
|
+
|
|
437
|
+
Агент, которого не обновили, остаётся на своей версии.
|
|
438
|
+
|
|
439
|
+
## Тесты
|
|
440
|
+
|
|
441
|
+
`npm test`. Для тестов `node_modules` не нужны: SDK в них не загружается.
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felishub-gateway",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Шлюз агента FelisHub: сессии Agent SDK по разговорам, решения и вопросы, расписания, бюджеты — сам подключается к FelisHub исходящим соединением",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.4"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"felishub-gateway": "src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": "./src/index.js",
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.js"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@anthropic-ai/claude-agent-sdk": ">=0.3.263",
|
|
22
|
+
"zod": "^4.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|