@iola_adm/iola-cli 0.1.31 → 0.1.33
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 +3 -0
- package/package.json +1 -1
- package/src/cli.js +328 -1
- package/wiki/Home.md +1 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +6 -0
- package/wiki//320/237/320/276/320/264/320/272/320/273/321/216/321/207/320/265/320/275/320/270/320/265-/320/223/320/276/321/201/321/203/321/201/320/273/321/203/320/263.md +101 -0
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ iola subagents list
|
|
|
75
75
|
iola trajectory last
|
|
76
76
|
iola review config
|
|
77
77
|
iola browser status
|
|
78
|
+
iola gosuslugi status
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
Локальная модель через Ollama:
|
|
@@ -102,6 +103,7 @@ iola version --check
|
|
|
102
103
|
- [Рабочая среда агента](https://github.com/adm-iola/iola-cli/wiki/Рабочая-среда-агента)
|
|
103
104
|
- [Платформа агента](https://github.com/adm-iola/iola-cli/wiki/Платформа-агента)
|
|
104
105
|
- [Браузерный агент](https://github.com/adm-iola/iola-cli/wiki/Браузерный-агент)
|
|
106
|
+
- [Подключение Госуслуг](https://github.com/adm-iola/iola-cli/wiki/Подключение-Госуслуг)
|
|
105
107
|
- [Расширения и локальные данные](https://github.com/adm-iola/iola-cli/wiki/Расширения-и-локальные-данные)
|
|
106
108
|
- [Архивы и мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Архивы-и-мастер-настройки)
|
|
107
109
|
- [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
|
|
@@ -119,6 +121,7 @@ iola version --check
|
|
|
119
121
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
|
120
122
|
- полноценный локальный MCP server по stdio/http: tools, resources и prompts;
|
|
121
123
|
- браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
|
|
124
|
+
- личное локальное подключение Госуслуг с явным согласием пользователя и хранением доступа только на его ПК;
|
|
122
125
|
- управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
|
|
123
126
|
- планы выполнения, traces, tasks, artifacts, snapshots и policy-профили;
|
|
124
127
|
- экспорт отчетов в Excel/Word-совместимые файлы;
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -30,6 +31,20 @@ const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "
|
|
|
30
31
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
31
32
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
32
33
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
34
|
+
const GOSUSLUGI_CONSENT_VERSION = "2026-05-26-personal-local-v1";
|
|
35
|
+
const GOSUSLUGI_CONSENT_TEXT = `Подключение личных Госуслуг
|
|
36
|
+
|
|
37
|
+
Вы подключаете личную учетную запись Госуслуг к локальному CLI-агенту iola-cli на этом компьютере.
|
|
38
|
+
|
|
39
|
+
Нажимая "Да", вы подтверждаете, что:
|
|
40
|
+
- используете собственную учетную запись Госуслуг;
|
|
41
|
+
- понимаете, что все действия, выполненные через CLI-агента после подключения, считаются действиями владельца этой учетной записи;
|
|
42
|
+
- разрешаете iola-cli локально сохранить данные доступа, необходимые для повторного входа или выполнения запросов от вашего имени;
|
|
43
|
+
- понимаете, что данные доступа хранятся только на этом компьютере в локальном хранилище пользователя и не передаются разработчикам CLI, администрации города или третьим лицам;
|
|
44
|
+
- обязуетесь не подключать чужие учетные записи и не передавать локальные файлы доступа другим лицам;
|
|
45
|
+
- понимаете, что перед юридически значимыми действиями, отправкой заявлений, оплатой, подписанием или изменением персональных данных CLI должен запросить отдельное подтверждение.
|
|
46
|
+
|
|
47
|
+
Продолжить подключение?`;
|
|
33
48
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
49
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
35
50
|
const USER_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
@@ -109,6 +124,19 @@ const DEFAULT_AI_CONFIG = {
|
|
|
109
124
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
110
125
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
111
126
|
},
|
|
127
|
+
gosuslugi: {
|
|
128
|
+
enabled: false,
|
|
129
|
+
mode: "personal-local",
|
|
130
|
+
authUrl: "",
|
|
131
|
+
tokenUrl: "",
|
|
132
|
+
userinfoUrl: "",
|
|
133
|
+
clientId: "",
|
|
134
|
+
clientSecret: "",
|
|
135
|
+
scope: "openid",
|
|
136
|
+
redirectHost: "127.0.0.1",
|
|
137
|
+
redirectPort: 18791,
|
|
138
|
+
redirectPath: "/gosuslugi/callback",
|
|
139
|
+
},
|
|
112
140
|
ai: {
|
|
113
141
|
activeProfile: "local",
|
|
114
142
|
provider: "ollama",
|
|
@@ -262,6 +290,7 @@ const COMMANDS = new Map([
|
|
|
262
290
|
["fork", forkSession],
|
|
263
291
|
["features", handleFeatures],
|
|
264
292
|
["settings", handleSettings],
|
|
293
|
+
["gosuslugi", handleGosuslugi],
|
|
265
294
|
["wiki", handleWiki],
|
|
266
295
|
["context", handleContext],
|
|
267
296
|
["skills", handleSkills],
|
|
@@ -388,6 +417,7 @@ Usage:
|
|
|
388
417
|
iola fork SESSION_ID [TEXT]
|
|
389
418
|
iola features list|enable|disable
|
|
390
419
|
iola settings list|get|validate|doctor|init
|
|
420
|
+
iola gosuslugi terms|consent|configure|status|login|logout|userinfo
|
|
391
421
|
iola wiki [open|links]
|
|
392
422
|
iola context list|show|init
|
|
393
423
|
iola skills list|show|paths|enable|disable|bundles|bundle|doctor
|
|
@@ -684,6 +714,11 @@ async function handleAgentLine(line, state) {
|
|
|
684
714
|
return false;
|
|
685
715
|
}
|
|
686
716
|
|
|
717
|
+
if (command === "gosuslugi") {
|
|
718
|
+
await handleGosuslugi(args);
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
687
722
|
if (command === "workspace") {
|
|
688
723
|
await handleWorkspace(args);
|
|
689
724
|
return false;
|
|
@@ -833,6 +868,7 @@ async function handleAgentLine(line, state) {
|
|
|
833
868
|
resume: ["resume", args],
|
|
834
869
|
fork: ["fork", args],
|
|
835
870
|
features: ["features", args],
|
|
871
|
+
gosuslugi: ["gosuslugi", args],
|
|
836
872
|
wiki: ["wiki", args],
|
|
837
873
|
context: ["context", args],
|
|
838
874
|
skills: ["skills", args],
|
|
@@ -891,6 +927,7 @@ function printAgentHelp() {
|
|
|
891
927
|
/sessions
|
|
892
928
|
/resume SESSION_ID
|
|
893
929
|
/features list
|
|
930
|
+
/gosuslugi status
|
|
894
931
|
/wiki
|
|
895
932
|
/context list
|
|
896
933
|
/skills list
|
|
@@ -1700,6 +1737,89 @@ async function handleSettings(args) {
|
|
|
1700
1737
|
throw new Error("Команды settings: list, get [KEY], validate, doctor, init.");
|
|
1701
1738
|
}
|
|
1702
1739
|
|
|
1740
|
+
async function handleGosuslugi(args) {
|
|
1741
|
+
const [action = "status", ...rest] = args;
|
|
1742
|
+
const options = parseOptions(rest);
|
|
1743
|
+
|
|
1744
|
+
if (action === "terms") {
|
|
1745
|
+
console.log(GOSUSLUGI_CONSENT_TEXT);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (action === "consent") {
|
|
1750
|
+
await acceptGosuslugiConsent(options);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
if (action === "status") {
|
|
1755
|
+
const config = await loadConfig();
|
|
1756
|
+
const secrets = await loadSecrets();
|
|
1757
|
+
const tokens = secrets.gosuslugi?.tokens || null;
|
|
1758
|
+
const consent = secrets.gosuslugiConsent || null;
|
|
1759
|
+
printKeyValue({
|
|
1760
|
+
mode: config.gosuslugi?.mode || "personal-local",
|
|
1761
|
+
enabled: config.gosuslugi?.enabled ? "yes" : "no",
|
|
1762
|
+
configured: isGosuslugiConfigured(config) ? "yes" : "no",
|
|
1763
|
+
consent: consent?.version === GOSUSLUGI_CONSENT_VERSION ? "accepted" : "not accepted",
|
|
1764
|
+
consentAt: consent?.acceptedAt || "-",
|
|
1765
|
+
clientId: config.gosuslugi?.clientId ? maskSecret(config.gosuslugi.clientId) : "-",
|
|
1766
|
+
authUrl: config.gosuslugi?.authUrl || "-",
|
|
1767
|
+
tokenUrl: config.gosuslugi?.tokenUrl || "-",
|
|
1768
|
+
userinfoUrl: config.gosuslugi?.userinfoUrl || "-",
|
|
1769
|
+
redirectUri: gosuslugiRedirectUri(config),
|
|
1770
|
+
connected: tokens?.access_token ? "yes" : "no",
|
|
1771
|
+
savedAt: secrets.gosuslugi?.savedAt || "-",
|
|
1772
|
+
expiresAt: secrets.gosuslugi?.expiresAt || "-",
|
|
1773
|
+
});
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (action === "configure") {
|
|
1778
|
+
const current = await loadConfig();
|
|
1779
|
+
const next = {
|
|
1780
|
+
...(current.gosuslugi || {}),
|
|
1781
|
+
enabled: true,
|
|
1782
|
+
mode: "personal-local",
|
|
1783
|
+
authUrl: options["auth-url"] || current.gosuslugi?.authUrl || "",
|
|
1784
|
+
tokenUrl: options["token-url"] || current.gosuslugi?.tokenUrl || "",
|
|
1785
|
+
userinfoUrl: options["userinfo-url"] || current.gosuslugi?.userinfoUrl || "",
|
|
1786
|
+
clientId: options["client-id"] || current.gosuslugi?.clientId || "",
|
|
1787
|
+
clientSecret: options["client-secret"] || current.gosuslugi?.clientSecret || "",
|
|
1788
|
+
scope: options.scope || current.gosuslugi?.scope || "openid",
|
|
1789
|
+
redirectHost: options["redirect-host"] || current.gosuslugi?.redirectHost || "127.0.0.1",
|
|
1790
|
+
redirectPort: Number(options["redirect-port"] || current.gosuslugi?.redirectPort || 18791),
|
|
1791
|
+
redirectPath: options["redirect-path"] || current.gosuslugi?.redirectPath || "/gosuslugi/callback",
|
|
1792
|
+
};
|
|
1793
|
+
await saveConfig({ gosuslugi: next });
|
|
1794
|
+
console.log("Настройки личного локального подключения Госуслуг сохранены.");
|
|
1795
|
+
console.log(`Redirect URI: ${gosuslugiRedirectUri({ gosuslugi: next })}`);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (action === "login") {
|
|
1800
|
+
const result = await gosuslugiLogin(options);
|
|
1801
|
+
printKeyValue(result);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if (action === "logout") {
|
|
1806
|
+
const secrets = await loadSecrets();
|
|
1807
|
+
delete secrets.gosuslugi;
|
|
1808
|
+
await saveSecrets(secrets);
|
|
1809
|
+
console.log("Локальное подключение Госуслуг удалено.");
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (action === "userinfo" || action === "me") {
|
|
1814
|
+
const result = await gosuslugiUserinfo(options);
|
|
1815
|
+
if (options.json) printJson(result);
|
|
1816
|
+
else printKeyValue(flattenObjectForPrint(result));
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
throw new Error("Команды gosuslugi: terms, consent, configure, status, login, logout, userinfo.");
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1703
1823
|
async function handleWiki(args) {
|
|
1704
1824
|
const [action = "links"] = args;
|
|
1705
1825
|
const base = "https://github.com/adm-iola/iola-cli/wiki";
|
|
@@ -1714,6 +1834,7 @@ async function handleWiki(args) {
|
|
|
1714
1834
|
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1715
1835
|
["Платформа агента", `${base}/Платформа-агента`],
|
|
1716
1836
|
["Браузерный агент", `${base}/Браузерный-агент`],
|
|
1837
|
+
["Подключение Госуслуг", `${base}/Подключение-Госуслуг`],
|
|
1717
1838
|
["Расширения и локальные данные", `${base}/Расширения-и-локальные-данные`],
|
|
1718
1839
|
["Архивы и мастер настройки", `${base}/Архивы-и-мастер-настройки`],
|
|
1719
1840
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
@@ -2640,6 +2761,204 @@ async function openUrl(url) {
|
|
|
2640
2761
|
await runCommand("xdg-open", [url], { inherit: false });
|
|
2641
2762
|
}
|
|
2642
2763
|
|
|
2764
|
+
async function gosuslugiLogin(options = {}) {
|
|
2765
|
+
const config = await loadConfig();
|
|
2766
|
+
if (!isGosuslugiConfigured(config)) {
|
|
2767
|
+
throw new Error("Личное подключение не настроено. Пример: iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid");
|
|
2768
|
+
}
|
|
2769
|
+
await ensureGosuslugiConsent(options);
|
|
2770
|
+
|
|
2771
|
+
const state = randomUrlSafe(24);
|
|
2772
|
+
const codeVerifier = randomUrlSafe(64);
|
|
2773
|
+
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
2774
|
+
const redirectUri = gosuslugiRedirectUri(config);
|
|
2775
|
+
const callback = waitForOAuthCallback(config.gosuslugi, state, Number(options.timeout || 180000));
|
|
2776
|
+
const authUrl = new URL(config.gosuslugi.authUrl);
|
|
2777
|
+
authUrl.searchParams.set("response_type", "code");
|
|
2778
|
+
authUrl.searchParams.set("client_id", config.gosuslugi.clientId);
|
|
2779
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
2780
|
+
authUrl.searchParams.set("scope", config.gosuslugi.scope || "openid");
|
|
2781
|
+
authUrl.searchParams.set("state", state);
|
|
2782
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
2783
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
2784
|
+
|
|
2785
|
+
console.log("Открываю экран входа Госуслуг в браузере для личного локального подключения.");
|
|
2786
|
+
console.log("После входа CLI примет callback на локальном адресе и сохранит данные доступа только на этом компьютере.");
|
|
2787
|
+
await openUrl(authUrl.toString());
|
|
2788
|
+
const params = await callback;
|
|
2789
|
+
if (params.error) throw new Error(`Госуслуги вернули ошибку: ${params.error} ${params.error_description || ""}`.trim());
|
|
2790
|
+
if (!params.code) throw new Error("Authorization code не получен.");
|
|
2791
|
+
|
|
2792
|
+
const tokens = await exchangeGosuslugiCode(config, {
|
|
2793
|
+
code: params.code,
|
|
2794
|
+
codeVerifier,
|
|
2795
|
+
redirectUri,
|
|
2796
|
+
});
|
|
2797
|
+
const secrets = await loadSecrets();
|
|
2798
|
+
const now = new Date();
|
|
2799
|
+
const expiresAt = tokens.expires_in ? new Date(now.getTime() + Number(tokens.expires_in) * 1000).toISOString() : "";
|
|
2800
|
+
secrets.gosuslugi = {
|
|
2801
|
+
savedAt: now.toISOString(),
|
|
2802
|
+
expiresAt,
|
|
2803
|
+
tokens,
|
|
2804
|
+
};
|
|
2805
|
+
await saveSecrets(secrets);
|
|
2806
|
+
return {
|
|
2807
|
+
connected: "yes",
|
|
2808
|
+
savedAt: secrets.gosuslugi.savedAt,
|
|
2809
|
+
expiresAt: expiresAt || "-",
|
|
2810
|
+
tokenType: tokens.token_type || "-",
|
|
2811
|
+
scope: tokens.scope || config.gosuslugi.scope || "-",
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
async function acceptGosuslugiConsent(options = {}) {
|
|
2816
|
+
console.log(GOSUSLUGI_CONSENT_TEXT);
|
|
2817
|
+
if (!options.yes) {
|
|
2818
|
+
const accepted = await confirm("Да, подключить личные Госуслуги к локальному iola-cli? [y/N] ");
|
|
2819
|
+
if (!accepted) {
|
|
2820
|
+
throw new Error("Подключение Госуслуг отменено пользователем.");
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
const secrets = await loadSecrets();
|
|
2824
|
+
secrets.gosuslugiConsent = {
|
|
2825
|
+
version: GOSUSLUGI_CONSENT_VERSION,
|
|
2826
|
+
acceptedAt: new Date().toISOString(),
|
|
2827
|
+
user: os.userInfo().username,
|
|
2828
|
+
host: os.hostname(),
|
|
2829
|
+
};
|
|
2830
|
+
await saveSecrets(secrets);
|
|
2831
|
+
console.log("Согласие сохранено локально.");
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
async function ensureGosuslugiConsent(options = {}) {
|
|
2835
|
+
const secrets = await loadSecrets();
|
|
2836
|
+
if (secrets.gosuslugiConsent?.version === GOSUSLUGI_CONSENT_VERSION) return;
|
|
2837
|
+
await acceptGosuslugiConsent(options);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
function waitForOAuthCallback(settings, expectedState, timeoutMs) {
|
|
2841
|
+
const host = settings.redirectHost || "127.0.0.1";
|
|
2842
|
+
const port = Number(settings.redirectPort || 18791);
|
|
2843
|
+
const callbackPath = settings.redirectPath || "/gosuslugi/callback";
|
|
2844
|
+
return new Promise((resolve, reject) => {
|
|
2845
|
+
const timer = setTimeout(() => {
|
|
2846
|
+
server.close(() => {});
|
|
2847
|
+
reject(new Error("Истекло время ожидания входа через Госуслуги."));
|
|
2848
|
+
}, timeoutMs);
|
|
2849
|
+
const server = createServer((req, res) => {
|
|
2850
|
+
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
2851
|
+
if (url.pathname !== callbackPath) {
|
|
2852
|
+
res.statusCode = 404;
|
|
2853
|
+
res.end("Not found");
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
2857
|
+
if (params.state !== expectedState) {
|
|
2858
|
+
res.statusCode = 400;
|
|
2859
|
+
res.end("Invalid state");
|
|
2860
|
+
clearTimeout(timer);
|
|
2861
|
+
server.close(() => {});
|
|
2862
|
+
reject(new Error("OAuth state не совпал. Вход отменен."));
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
2866
|
+
res.end("<!doctype html><meta charset=\"utf-8\"><title>iola</title><body>Вход выполнен. Можно закрыть это окно и вернуться в терминал.</body>");
|
|
2867
|
+
clearTimeout(timer);
|
|
2868
|
+
server.close(() => resolve(params));
|
|
2869
|
+
});
|
|
2870
|
+
server.once("error", (error) => {
|
|
2871
|
+
clearTimeout(timer);
|
|
2872
|
+
reject(error);
|
|
2873
|
+
});
|
|
2874
|
+
server.listen(port, host);
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
async function exchangeGosuslugiCode(config, { code, codeVerifier, redirectUri }) {
|
|
2879
|
+
const body = new URLSearchParams();
|
|
2880
|
+
body.set("grant_type", "authorization_code");
|
|
2881
|
+
body.set("code", code);
|
|
2882
|
+
body.set("redirect_uri", redirectUri);
|
|
2883
|
+
body.set("client_id", config.gosuslugi.clientId);
|
|
2884
|
+
body.set("code_verifier", codeVerifier);
|
|
2885
|
+
body.set("client_mode", config.gosuslugi.mode || "personal-local");
|
|
2886
|
+
if (config.gosuslugi.clientSecret) body.set("client_secret", config.gosuslugi.clientSecret);
|
|
2887
|
+
|
|
2888
|
+
const response = await fetch(config.gosuslugi.tokenUrl, {
|
|
2889
|
+
method: "POST",
|
|
2890
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
2891
|
+
body,
|
|
2892
|
+
});
|
|
2893
|
+
const text = await response.text();
|
|
2894
|
+
let payload = {};
|
|
2895
|
+
try {
|
|
2896
|
+
payload = text ? JSON.parse(text) : {};
|
|
2897
|
+
} catch {
|
|
2898
|
+
payload = { raw: text };
|
|
2899
|
+
}
|
|
2900
|
+
if (!response.ok) {
|
|
2901
|
+
throw new Error(`Token endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
2902
|
+
}
|
|
2903
|
+
return payload;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
async function gosuslugiUserinfo() {
|
|
2907
|
+
const config = await loadConfig();
|
|
2908
|
+
const secrets = await loadSecrets();
|
|
2909
|
+
const accessToken = secrets.gosuslugi?.tokens?.access_token;
|
|
2910
|
+
if (!accessToken) throw new Error("Госуслуги не подключены. Запустите: iola gosuslugi login");
|
|
2911
|
+
if (!config.gosuslugi?.userinfoUrl) throw new Error("userinfoUrl не настроен.");
|
|
2912
|
+
const response = await fetch(config.gosuslugi.userinfoUrl, {
|
|
2913
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
2914
|
+
});
|
|
2915
|
+
const text = await response.text();
|
|
2916
|
+
let payload = {};
|
|
2917
|
+
try {
|
|
2918
|
+
payload = text ? JSON.parse(text) : {};
|
|
2919
|
+
} catch {
|
|
2920
|
+
payload = { raw: text };
|
|
2921
|
+
}
|
|
2922
|
+
if (!response.ok) throw new Error(`Userinfo endpoint вернул ${response.status}: ${JSON.stringify(payload)}`);
|
|
2923
|
+
return payload;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function isGosuslugiConfigured(config) {
|
|
2927
|
+
return Boolean(config.gosuslugi?.authUrl && config.gosuslugi?.tokenUrl && config.gosuslugi?.clientId);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
function gosuslugiRedirectUri(config) {
|
|
2931
|
+
const settings = config.gosuslugi || DEFAULT_AI_CONFIG.gosuslugi;
|
|
2932
|
+
return `http://${settings.redirectHost || "127.0.0.1"}:${Number(settings.redirectPort || 18791)}${settings.redirectPath || "/gosuslugi/callback"}`;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
function randomUrlSafe(bytes) {
|
|
2936
|
+
return base64Url(randomBytes(bytes));
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
function base64Url(buffer) {
|
|
2940
|
+
return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
function maskSecret(value) {
|
|
2944
|
+
const text = String(value || "");
|
|
2945
|
+
if (text.length <= 8) return text ? "***" : "-";
|
|
2946
|
+
return `${text.slice(0, 4)}...${text.slice(-4)}`;
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
function flattenObjectForPrint(value, prefix = "") {
|
|
2950
|
+
const rows = {};
|
|
2951
|
+
for (const [key, item] of Object.entries(value || {})) {
|
|
2952
|
+
const name = prefix ? `${prefix}.${key}` : key;
|
|
2953
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
2954
|
+
Object.assign(rows, flattenObjectForPrint(item, name));
|
|
2955
|
+
} else {
|
|
2956
|
+
rows[name] = Array.isArray(item) ? item.join(", ") : item;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
return rows;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2643
2962
|
async function handlePermissions(args) {
|
|
2644
2963
|
const [action = "list", name] = args;
|
|
2645
2964
|
const config = await loadConfig();
|
|
@@ -6001,7 +6320,7 @@ function parseOptions(args) {
|
|
|
6001
6320
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
6002
6321
|
result.check = true;
|
|
6003
6322
|
result[arg.slice(2)] = true;
|
|
6004
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--debug-file") {
|
|
6323
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
6005
6324
|
result[arg.slice(2)] = args[index + 1];
|
|
6006
6325
|
index += 1;
|
|
6007
6326
|
} else {
|
|
@@ -7769,6 +8088,10 @@ function mergeConfig(base, override) {
|
|
|
7769
8088
|
...base.api,
|
|
7770
8089
|
...(override.api || {}),
|
|
7771
8090
|
},
|
|
8091
|
+
gosuslugi: {
|
|
8092
|
+
...base.gosuslugi,
|
|
8093
|
+
...(override.gosuslugi || {}),
|
|
8094
|
+
},
|
|
7772
8095
|
ai: {
|
|
7773
8096
|
...base.ai,
|
|
7774
8097
|
...(override.ai || {}),
|
|
@@ -7843,6 +8166,9 @@ function validateConfig(config) {
|
|
|
7843
8166
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
7844
8167
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
7845
8168
|
}
|
|
8169
|
+
if (config.gosuslugi?.enabled && !isGosuslugiConfigured(config)) {
|
|
8170
|
+
errors.push("gosuslugi включен, но authUrl/tokenUrl/clientId не заполнены");
|
|
8171
|
+
}
|
|
7846
8172
|
return errors;
|
|
7847
8173
|
}
|
|
7848
8174
|
|
|
@@ -7852,6 +8178,7 @@ function configSchema() {
|
|
|
7852
8178
|
required: ["api", "ai"],
|
|
7853
8179
|
properties: {
|
|
7854
8180
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
8181
|
+
gosuslugi: { requiredWhenEnabled: ["authUrl", "tokenUrl", "clientId"], optional: ["userinfoUrl", "clientSecret", "scope", "redirectHost", "redirectPort", "redirectPath"] },
|
|
7855
8182
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
7856
8183
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
7857
8184
|
toolsets: { available: Object.keys(TOOLSETS) },
|
package/wiki/Home.md
CHANGED
|
@@ -33,6 +33,7 @@ iola ask "найди школу 29"
|
|
|
33
33
|
- [Рабочая среда агента](Рабочая-среда-агента)
|
|
34
34
|
- [Платформа агента](Платформа-агента)
|
|
35
35
|
- [Браузерный агент](Браузерный-агент)
|
|
36
|
+
- [Подключение Госуслуг](Подключение-Госуслуг)
|
|
36
37
|
- [Расширения и локальные данные](Расширения-и-локальные-данные)
|
|
37
38
|
- [Архивы и мастер настройки](Архивы-и-мастер-настройки)
|
|
38
39
|
- [Daemon, RPC и cron](Daemon-RPC-и-cron)
|
|
@@ -92,6 +92,12 @@ iola context init
|
|
|
92
92
|
iola context list
|
|
93
93
|
iola settings list
|
|
94
94
|
iola settings validate
|
|
95
|
+
iola gosuslugi status
|
|
96
|
+
iola gosuslugi terms
|
|
97
|
+
iola gosuslugi consent
|
|
98
|
+
iola gosuslugi configure --auth-url URL --token-url URL --client-id ID --scope openid
|
|
99
|
+
iola gosuslugi login
|
|
100
|
+
iola gosuslugi userinfo --json
|
|
95
101
|
iola cron add "каждый день 09:00 -- quality"
|
|
96
102
|
iola cron tick
|
|
97
103
|
iola daemon status
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Личное подключение Госуслуг
|
|
2
|
+
|
|
3
|
+
`iola-cli` поддерживает локальное подключение личной учетной записи Госуслуг на компьютере пользователя.
|
|
4
|
+
|
|
5
|
+
Сценарий рассчитан именно на пользователя, который ставит CLI на свой ПК и подключает свою учетную запись. Ключи организации или администрации в публичный пакет не вшиваются.
|
|
6
|
+
|
|
7
|
+
## Согласие пользователя
|
|
8
|
+
|
|
9
|
+
Перед входом CLI показывает текст согласия:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Вы подключаете личную учетную запись Госуслуг к локальному CLI-агенту iola-cli на этом компьютере.
|
|
13
|
+
|
|
14
|
+
Нажимая "Да", вы подтверждаете, что:
|
|
15
|
+
- используете собственную учетную запись Госуслуг;
|
|
16
|
+
- понимаете, что все действия, выполненные через CLI-агента после подключения, считаются действиями владельца этой учетной записи;
|
|
17
|
+
- разрешаете iola-cli локально сохранить данные доступа, необходимые для повторного входа или выполнения запросов от вашего имени;
|
|
18
|
+
- понимаете, что данные доступа хранятся только на этом компьютере в локальном хранилище пользователя и не передаются разработчикам CLI, администрации города или третьим лицам;
|
|
19
|
+
- обязуетесь не подключать чужие учетные записи и не передавать локальные файлы доступа другим лицам;
|
|
20
|
+
- понимаете, что перед юридически значимыми действиями, отправкой заявлений, оплатой, подписанием или изменением персональных данных CLI должен запросить отдельное подтверждение.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Посмотреть текст:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
iola gosuslugi terms
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Принять заранее:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
iola gosuslugi consent
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Настройка
|
|
36
|
+
|
|
37
|
+
Для работы нужны параметры пользовательского OAuth/OIDC-подключения:
|
|
38
|
+
|
|
39
|
+
- authorization endpoint;
|
|
40
|
+
- token endpoint;
|
|
41
|
+
- client ID;
|
|
42
|
+
- разрешенный redirect URI;
|
|
43
|
+
- scope;
|
|
44
|
+
- optional client secret, если он выдан именно пользователю или локальному приложению;
|
|
45
|
+
- optional userinfo endpoint.
|
|
46
|
+
|
|
47
|
+
Команда настройки:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
iola gosuslugi configure \
|
|
51
|
+
--auth-url "https://..." \
|
|
52
|
+
--token-url "https://..." \
|
|
53
|
+
--userinfo-url "https://..." \
|
|
54
|
+
--client-id "CLIENT_ID" \
|
|
55
|
+
--scope "openid" \
|
|
56
|
+
--redirect-port 18791
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
CLI покажет redirect URI:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
http://127.0.0.1:18791/gosuslugi/callback
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Этот URI должен быть разрешен в настройках подключения.
|
|
66
|
+
|
|
67
|
+
## Вход
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
iola gosuslugi login
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Что происходит:
|
|
74
|
+
|
|
75
|
+
1. CLI запускает локальный callback server на `127.0.0.1`.
|
|
76
|
+
2. Показывается и сохраняется согласие пользователя.
|
|
77
|
+
3. Открывается экран входа Госуслуг.
|
|
78
|
+
4. Пользователь сам вводит логин, пароль, SMS/2FA.
|
|
79
|
+
5. Госуслуги возвращают `authorization code` на локальный callback.
|
|
80
|
+
6. CLI обменивает code на токены и сохраняет их локально.
|
|
81
|
+
|
|
82
|
+
Токены хранятся в `~/.iola/secrets.json` на компьютере пользователя.
|
|
83
|
+
|
|
84
|
+
## Проверка
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
iola gosuslugi status
|
|
88
|
+
iola gosuslugi userinfo --json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Выход
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
iola gosuslugi logout
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Команда удаляет локально сохраненные токены.
|
|
98
|
+
|
|
99
|
+
## Ограничения
|
|
100
|
+
|
|
101
|
+
Без параметров пользовательского подключения команда `login` не сможет завершить подключение. Все ключи, токены и настройки хранятся только локально у пользователя.
|