@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
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` не сможет завершить подключение. Все ключи, токены и настройки хранятся только локально у пользователя.