@dev-sergeev/datalab-kit 0.1.0 → 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 — 2026-10-01
4
+
5
+ - Возвращён штатный диалог `@gotgenes/pi-permission-system`, включая разрешение на сессию и отказ с причиной. Собственный диалог Datalab Kit удалён.
6
+ - Удалены собственные команды `/yolo on|off|status`; YOLO остаётся в штатных настройках permission-system.
7
+ - Автоматическое разрешение чтения и todo, пользовательские правила и интеграция Plan mode сохранены.
8
+ - Сборка очищает устаревшие модули диалога и YOLO при обновлении с 0.1.1.
9
+ - Добавлены проверки штатных вариантов диалога, однократного и сессионного разрешения, смены сессии и штатного YOLO.
10
+
11
+ ## 0.1.1 — 2026-10-01
12
+
13
+ - Служебный todo и безопасные читающие цепочки с шаблонами файлов больше не требуют подтверждения по правилам набора.
14
+ - Добавлены сессионные команды `/yolo on`, `/yolo off`, `/yolo status`; постоянная настройка YOLO сохранена. Явные запреты и ограничения Plan mode остаются активными.
15
+ - Исправлен разбор имён файлов после `<`, которые могли скрыть изменяющую команду или опасную опцию.
16
+ - Исправлена атрибуция эффектов опасных опций и приоритет пользовательских правил в составных командах.
17
+ - Пользовательские ограничения путей проверяются для файлов, раскрытых shell-шаблонами, включая симлинки и команды после `cd`.
18
+ - Добавлены интеграционные матрицы читающих и изменяющих команд, обновление todo, проверки симлинков, режимов Plan/YOLO и смены сессии.
19
+
20
+ ## 0.1.0 — 2026-09-30
21
+
22
+ - Первый публичный выпуск набора из восьми дополнений для Pi coding agent.
23
+ - Значения по умолчанию разрешают чтение и требуют подтверждения изменений; пользовательские настройки имеют приоритет.
24
+ - Plan mode контролирует допустимость операций без дополнительных подтверждений набора.
25
+ - Общий предел параллельности субагентов; GigaChat обновляется независимо от версии набора.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Datalab Kit
2
2
 
3
- Готовый набор дополнений для **Pi coding agent**. Пакет предназначен для публикации как `@dev-sergeev/datalab-kit`.
3
+ Готовый набор дополнений для **Pi coding agent**, опубликованный как `@dev-sergeev/datalab-kit`. История выпусков — в [CHANGELOG](CHANGELOG.md).
4
4
 
5
5
  ## Установка
6
6
 
@@ -17,7 +17,7 @@ pi
17
17
  npm ci --ignore-scripts --legacy-peer-deps
18
18
  npm run build
19
19
  npm pack
20
- pi install ./dev-sergeev-datalab-kit-0.1.0.tgz
20
+ pi install ./dev-sergeev-datalab-kit-0.1.2.tgz
21
21
  ```
22
22
 
23
23
  Целевые системы — Linux и macOS; локальные интеграционные проверки выполнены на Linux. Проверка macOS включена в CI. Windows пока не заявляется.
@@ -42,17 +42,25 @@ pi install ./dev-sergeev-datalab-kit-0.1.0.tgz
42
42
  ## Значения по умолчанию
43
43
 
44
44
  - Чтение файлов, просмотр каталогов и поиск разрешены в любых папках.
45
+ - Служебный `todo` создаёт и обновляет список задач сессии без подтверждения.
46
+ - Читающие цепочки вроде `grep foo *.ts | head`, регулярные выражения, комментарии, чтение через `<`, объединение stderr со stdout и вывод в `/dev/null` не требуют подтверждения. У команд с изменяющими опциями шаблоны файлов разрешаются с фиксированным префиксом пути или после `--`, когда это исключает внедрение опций из имён файлов.
45
47
  - Изменения через `write`, `edit`, `npm install`, `git commit` требуют подтверждения.
46
48
  - `rm`, `hadoop`, `hdfs`, неизвестные команды и любой удалённый `curl` требуют подтверждения.
47
49
  - `curl` к `localhost`, `127.0.0.1` и `::1` разрешён только для GET/HEAD. Для автоматически разрешённых запросов отключаются `.curlrc` и прокси; перенаправления, вывод в файл и изменяющие запросы требуют подтверждения.
48
50
  - Составная команда проверяется целиком. `cat a > b` и `find . -delete` требуют подтверждения. Неизвестный синтаксис также требует подтверждения.
49
- - Диалог выделяет предупреждение, сначала выбирает **«Отмена»**. Enter отменяет; ↓ и Enter разрешают один запуск. Разрешений на всю сессию нет. В RPC доступны те же два варианта; обработку клавиш определяет клиент.
51
+ - Подтверждения показывает штатный диалог `@gotgenes/pi-permission-system`: разрешение одного запуска или на сессию, отказ и отказ с причиной. Настройки клавиш и повторного нажатия управляются самим расширением; собственного диалога Datalab Kit нет.
50
52
  - Одновременно работает один субагент. Инструмент `subagent` принимает одну задачу за вызов.
51
53
 
52
- Уже заданные пользователем настройки имеют приоритет, включая разрешения и число субагентов. Если пользователь явно настроил `doublePressToConfirm` или `permissionDialogKeys`, используется его исходный диалог permission-system. Значения набора действуют в памяти; существующие конфиги расширений не перезаписываются. Настройки разрешений остаются в штатных `extensions/pi-permission-system/config.json` внутри каталога агента и `.pi/extensions/pi-permission-system/config.json` внутри проекта. Субагенты используют `<agentDir>/subagents.json` и `<project>/.pi/subagents.json`.
54
+ Уже заданные пользователем настройки имеют приоритет, включая разрешения, настройки штатного диалога и число субагентов. Значения набора действуют в памяти; существующие конфиги расширений не перезаписываются. Настройки разрешений остаются в штатных `extensions/pi-permission-system/config.json` внутри каталога агента и `.pi/extensions/pi-permission-system/config.json` внутри проекта. Субагенты используют `<agentDir>/subagents.json` и `<project>/.pi/subagents.json`.
53
55
 
54
56
  В **Plan mode** допустимые инструменты и команды проверяет pi-plan-mode без дополнительных подтверждений по правилам набора. Явные пользовательские `ask` и `deny` продолжают действовать; некорректная конфигурация разрешений также не обходится. После выхода из Plan mode или перехода к реализации правила набора снова действуют. Это поведение сохраняется при восстановлении сессии и смене ветки. Plan mode допускает некоторые тесты и проверки, которые могут создавать артефакты.
55
57
 
58
+ ## YOLO
59
+
60
+ YOLO управляется штатным `@gotgenes/pi-permission-system`: откройте `/permission-system` → **YOLO mode** или задайте `"yoloMode": true` в конфигурации расширения. Собственные сессионные команды `/yolo on|off|status`, добавленные в 0.1.1, удалены в 0.1.2.
61
+
62
+ YOLO автоматически разрешает `ask`. Явные `deny` и ограничения Plan mode сохраняются.
63
+
56
64
  ## Обновление и ограничение Pi 0.85.1
57
65
 
58
66
  После первого запуска Pi:
@@ -6,10 +6,17 @@ export declare const kitPermission: {
6
6
  readonly grep: "allow";
7
7
  readonly find: "allow";
8
8
  readonly path_read: "allow";
9
- readonly path_write: "ask";
9
+ readonly path_write: {
10
+ readonly '*': "ask";
11
+ readonly '/dev/null': "allow";
12
+ };
10
13
  readonly external_directory_read: "allow";
11
- readonly external_directory_write: "ask";
14
+ readonly external_directory_write: {
15
+ readonly '*': "ask";
16
+ readonly '/dev/null': "allow";
17
+ };
12
18
  readonly bash: "ask";
19
+ readonly todo: "allow";
13
20
  readonly subagent: "allow";
14
21
  readonly get_subagent_result: "allow";
15
22
  readonly steer_subagent: "allow";
package/dist/defaults.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** A lower policy layer; explicit global/project/agent rules override it. */
2
2
  export const kitPermission = {
3
3
  '*': 'ask', read: 'allow', ls: 'allow', grep: 'allow', find: 'allow',
4
- path_read: 'allow', path_write: 'ask',
5
- external_directory_read: 'allow', external_directory_write: 'ask',
6
- bash: 'ask', subagent: 'allow', get_subagent_result: 'allow', steer_subagent: 'allow',
4
+ path_read: 'allow', path_write: { '*': 'ask', '/dev/null': 'allow' },
5
+ external_directory_read: 'allow', external_directory_write: { '*': 'ask', '/dev/null': 'allow' },
6
+ bash: 'ask', todo: 'allow', subagent: 'allow', get_subagent_result: 'allow', steer_subagent: 'allow',
7
7
  };
@@ -1,5 +1,11 @@
1
+ export declare function isKnownReadCommand(name: string): boolean;
1
2
  export declare function isReadOnlyWords(words: readonly string[]): boolean;
2
3
  export declare function isReadOnlyCommand(command: string): boolean;
4
+ /** Only syntax whose effects can be decided by the individual command gates. */
5
+ export declare function inspectionCommands(command: string): string[] | undefined;
6
+ /** Expand accepted shell globs for path checks without executing the command. */
7
+ export declare function shellGlobTokens(command: string): string[];
8
+ export declare function expandShellGlob(pattern: string, cwd: string): string[];
3
9
  export declare function isReadOnlyCommandInput(input: unknown): boolean;
4
10
  /** Local read requests must not inherit a proxy, curlrc mutation or redirect. */
5
11
  export declare function normalizeLocalReadCurl(command: string): string | undefined;
@@ -1,12 +1,27 @@
1
- /** Conservative shell parsing: unfamiliar syntax is confirmed, never executed here. */
1
+ import { globSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ /** Parse inspection syntax without executing expansions or subprocesses. */
2
4
  function shellCommands(input) {
5
+ if (input.includes('\u0000'))
6
+ return undefined;
3
7
  const commands = [];
4
8
  let words = [];
5
9
  let word = '';
10
+ let glob = false;
6
11
  let started = false;
12
+ let inputTarget = false;
7
13
  let quote;
8
- const pushWord = () => { if (started)
9
- words.push(word); word = ''; started = false; };
14
+ const pushWord = () => {
15
+ if (started) {
16
+ if (inputTarget)
17
+ inputTarget = false;
18
+ else
19
+ words.push({ text: word, glob });
20
+ }
21
+ word = '';
22
+ glob = false;
23
+ started = false;
24
+ };
10
25
  const pushCommand = () => { pushWord(); if (words.length)
11
26
  commands.push(words); words = []; };
12
27
  for (let index = 0; index < input.length; index++) {
@@ -16,11 +31,27 @@ function shellCommands(input) {
16
31
  quote = undefined;
17
32
  continue;
18
33
  }
19
- if (quote === '"' && (char === '$' || char === '`' || char === '\\'))
34
+ if (quote === '"' && (char === '$' || char === '`'))
20
35
  return undefined;
36
+ if (quote === '"' && char === '\\') {
37
+ const next = input[++index];
38
+ if (next === undefined)
39
+ return undefined;
40
+ if (next !== '\n')
41
+ word += ['$', '`', '"', '\\'].includes(next) ? next : '\\' + next;
42
+ continue;
43
+ }
21
44
  word += char;
22
45
  continue;
23
46
  }
47
+ if (!started) {
48
+ // Descriptor duplication and discarding output touch no regular file.
49
+ const redirect = input.slice(index).match(/^(?:[012])?(?:[<>]&[012]|>[ \t]*\/dev\/null)(?=$|[\s|;&])/);
50
+ if (redirect) {
51
+ index += redirect[0].length - 1;
52
+ continue;
53
+ }
54
+ }
24
55
  if (char === "'" || char === '"') {
25
56
  quote = char;
26
57
  started = true;
@@ -28,15 +59,40 @@ function shellCommands(input) {
28
59
  }
29
60
  if (char === '\\') {
30
61
  const next = input[++index];
31
- if (next === undefined || next === '\n')
62
+ if (next === undefined)
32
63
  return undefined;
33
- word += next;
34
- started = true;
64
+ if (next !== '\n') {
65
+ word += next;
66
+ started = true;
67
+ }
35
68
  continue;
36
69
  }
37
- if ('<>$`(){}#*?['.includes(char) || char === '\u0000')
70
+ if (char === '#' && !started) {
71
+ if (inputTarget)
72
+ return undefined;
73
+ while (index < input.length && input[index] !== '\n')
74
+ index++;
75
+ pushCommand();
76
+ continue;
77
+ }
78
+ if (char === '<') {
79
+ // A single file input redirect only reads; heredocs/process substitutions
80
+ // can execute code and are deliberately left to confirmation.
81
+ if (input[index + 1] === '<' || input[index + 1] === '(')
82
+ return undefined;
83
+ pushWord();
84
+ if (inputTarget)
85
+ return undefined;
86
+ inputTarget = true;
87
+ continue;
88
+ }
89
+ if ('>$`(){}'.includes(char) || char === '\u0000')
38
90
  return undefined;
91
+ if ('*?['.includes(char))
92
+ glob = true;
39
93
  if (char === '&') {
94
+ if (inputTarget && !started)
95
+ return undefined;
40
96
  if (input[index + 1] !== '&')
41
97
  return undefined;
42
98
  index++;
@@ -44,6 +100,8 @@ function shellCommands(input) {
44
100
  continue;
45
101
  }
46
102
  if (char === '|' || char === ';' || char === '\n') {
103
+ if (inputTarget && !started)
104
+ return undefined;
47
105
  pushCommand();
48
106
  continue;
49
107
  }
@@ -57,12 +115,17 @@ function shellCommands(input) {
57
115
  if (quote)
58
116
  return undefined;
59
117
  pushCommand();
60
- return commands.length ? commands : undefined;
118
+ if (inputTarget)
119
+ return undefined;
120
+ return commands;
61
121
  }
62
122
  const readers = new Set(['read', 'cat', 'ls', 'grep', 'egrep', 'fgrep', 'rg', 'find', 'fd',
63
123
  'head', 'tail', 'wc', 'stat', 'pwd', 'basename', 'dirname', 'realpath', 'which',
64
124
  'diff', 'cmp', 'comm', 'du', 'df', 'md5sum', 'sha1sum', 'sha256sum', 'cksum',
65
125
  'whoami', 'id', 'uname', 'uptime', 'ps', 'printenv', 'echo', 'cd', 'sort']);
126
+ export function isKnownReadCommand(name) {
127
+ return readers.has(name) || name === 'git' || name === 'curl';
128
+ }
66
129
  export function isReadOnlyWords(words) {
67
130
  const [name, ...args] = words;
68
131
  if (!name)
@@ -72,25 +135,78 @@ export function isReadOnlyWords(words) {
72
135
  && args.includes('--noproxy') && isLocalReadCurl(args);
73
136
  if (name === 'git') {
74
137
  return ['status', 'diff', 'log', 'show', 'blame', 'ls-files'].includes(args[0] ?? '')
75
- && !args.some(arg => /^--(?:output|ext-diff|textconv)/.test(arg));
138
+ && !optionWords(args).some(arg => dangerousLongOption(arg, ['--output', '--ext-diff', '--textconv']));
76
139
  }
77
140
  if (!readers.has(name))
78
141
  return false;
79
142
  if (name === 'find' && args.some(arg => /^-(?:delete|exec|ok|fprint|fprintf|fls)/.test(arg)))
80
143
  return false;
81
- if (name === 'fd' && args.some(arg => /^--exec/.test(arg) || /^-[^-]*[xX]/.test(arg)))
144
+ const options = optionWords(args);
145
+ if (name === 'fd' && options.some(arg => /^--exec/.test(arg) || /^-[^-]*[xX]/.test(arg)))
82
146
  return false;
83
- if (name === 'rg' && args.some(arg => /^--pre/.test(arg)))
147
+ if (name === 'rg' && options.some(arg => /^--pre/.test(arg)))
84
148
  return false;
85
- if (name === 'sort' && args.some(arg => /^--[oc]/.test(arg) || /^-[^-]*o/.test(arg)))
149
+ if (name === 'sort' && options.some(arg => dangerousLongOption(arg, ['--output', '--compress-program']) || /^-[^-]*o/.test(arg)))
86
150
  return false;
87
- if (name === 'diff' && args.some(arg => /^--out/.test(arg)))
151
+ if (name === 'diff' && options.some(arg => /^--out/.test(arg)))
88
152
  return false;
89
153
  return true;
90
154
  }
155
+ function optionWords(args) {
156
+ const end = args.indexOf('--');
157
+ return end < 0 ? args : args.slice(0, end);
158
+ }
159
+ /** GNU tools accept abbreviated long options; harmless complete names stay usable. */
160
+ function dangerousLongOption(arg, names) {
161
+ const option = arg.split('=', 1)[0];
162
+ return option.startsWith('--') && names.some(name => name.startsWith(option) || option.startsWith(name));
163
+ }
164
+ function isReadOnlyShellWords(words) {
165
+ const [head, ...args] = words;
166
+ if (!head || head.glob || !isReadOnlyWords(words.map(word => word.text)))
167
+ return false;
168
+ // Expansion must not introduce write/exec options. A fixed path prefix or
169
+ // an explicit end-of-options marker keeps glob results in operand position.
170
+ if (['find', 'fd', 'rg', 'sort', 'diff', 'git', 'curl'].includes(head.text)) {
171
+ let operandsOnly = false;
172
+ for (const arg of args) {
173
+ if (!arg.glob && arg.text === '--')
174
+ operandsOnly = true;
175
+ if (!arg.glob)
176
+ continue;
177
+ const prefix = arg.text.split(/[*?[]/, 1)[0] ?? '';
178
+ const fixedPath = prefix.includes('/') && !prefix.startsWith('-');
179
+ // find expressions remain active after its paths, even with --.
180
+ if (!fixedPath && (!operandsOnly || head.text === 'find'))
181
+ return false;
182
+ }
183
+ }
184
+ return true;
185
+ }
91
186
  export function isReadOnlyCommand(command) {
92
187
  const commands = shellCommands(command);
93
- return commands !== undefined && commands.every(isReadOnlyWords);
188
+ return commands !== undefined && commands.every(isReadOnlyShellWords);
189
+ }
190
+ /** Only syntax whose effects can be decided by the individual command gates. */
191
+ export function inspectionCommands(command) {
192
+ const commands = shellCommands(command);
193
+ if (commands === undefined || !commands.every(words => {
194
+ const head = words[0];
195
+ return head !== undefined && !head.glob && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(head.text)
196
+ && words.every(word => !word.glob || !/[\s'"$`\\<>|&;(){}]/.test(word.text));
197
+ }))
198
+ return undefined;
199
+ // Redirection filenames are not argv. Keep quoting and expansion roles when
200
+ // asking the policy about the actual command, independently of upstream AST.
201
+ return commands.map(words => words.map(word => word.glob || /^[\w./:@%+=,-]+$/.test(word.text)
202
+ ? word.text : `'${word.text.replaceAll("'", "'\\''")}'`).join(' '));
203
+ }
204
+ /** Expand accepted shell globs for path checks without executing the command. */
205
+ export function shellGlobTokens(command) {
206
+ return (shellCommands(command) ?? []).flatMap(words => words.filter(word => word.glob).map(word => word.text));
207
+ }
208
+ export function expandShellGlob(pattern, cwd) {
209
+ return globSync(pattern, { cwd }).map(path => resolve(cwd, path));
94
210
  }
95
211
  export function isReadOnlyCommandInput(input) {
96
212
  if (typeof input === 'string')
@@ -140,12 +256,15 @@ function isLocalReadCurl(args) {
140
256
  }
141
257
  /** Local read requests must not inherit a proxy, curlrc mutation or redirect. */
142
258
  export function normalizeLocalReadCurl(command) {
259
+ // Normalization must not erase the caller's redirect semantics.
260
+ if (/[<>]/.test(command))
261
+ return undefined;
143
262
  const commands = shellCommands(command);
144
- if (commands?.length !== 1 || commands[0]?.[0] !== 'curl' || !isLocalReadCurl(commands[0].slice(1)))
263
+ if (commands?.length !== 1 || commands[0]?.[0]?.text !== 'curl' || !isLocalReadCurl(commands[0].slice(1).map(word => word.text)))
145
264
  return undefined;
146
265
  const quote = (word) => `'${word.replaceAll("'", "'\\''")}'`;
147
266
  return commands.map(words => {
148
- const args = words[0] === 'curl' ? ['curl', '-q', '--noproxy', '*', ...words.slice(1)] : words;
267
+ const args = ['curl', '-q', '--noproxy', '*', ...words.slice(1).map(word => word.text)];
149
268
  return args.map(quote).join(' ');
150
269
  }).join('');
151
270
  }
@@ -1,3 +1,5 @@
1
+ // Datalab Kit adaptation
2
+ import { shellGlobTokens, expandShellGlob, isReadOnlyCommand } from "../../../../../../dist/read-policy.js";
1
3
  import type { AccessPath } from "#src/access-intent/access-path";
2
4
  import {
3
5
  mergeTokenEffects,
@@ -152,11 +154,15 @@ export class BashPathResolver {
152
154
  for (const salvaged of salvagedRoots) {
153
155
  this.walkForCandidates(salvaged, UNKNOWN_BASE, candidates);
154
156
  }
157
+ const globTokens = new Set(shellGlobTokens(rootNode.text));
158
+ const expandedCandidates = candidates.flatMap(candidate => {
159
+ if (candidate.base.kind !== 'known' || !globTokens.has(candidate.token)) return [candidate];
160
+ const cwd = this.normalizer.resolveBase(candidate.base.offset);
161
+ return [candidate, ...expandShellGlob(candidate.token, cwd).map(token => ({ ...candidate, token }))];
162
+ });
155
163
  return {
156
- externalAccesses: this.withWorkdirExternal(
157
- this.projectExternalPaths(candidates),
158
- ),
159
- ruleCandidates: this.projectRuleCandidates(candidates),
164
+ externalAccesses: this.withWorkdirExternal(this.projectExternalPaths(expandedCandidates)),
165
+ ruleCandidates: this.projectRuleCandidates(expandedCandidates),
160
166
  };
161
167
  }
162
168
 
@@ -229,7 +235,13 @@ export class BashPathResolver {
229
235
  case "redirected_statement":
230
236
  return this.walkCurrentShellSequence(node, base, out);
231
237
  case "command":
232
- tagTokens(collectCommandTokens(node), base, out);
238
+ const tokens = collectCommandTokens(node);
239
+ const present = new Set(tokens.map(token => token.token));
240
+ const globEffect = isReadOnlyCommand(node.text) ? { effect: 'read' as const, source: 'core' as const } : UNPROVEN_EFFECT;
241
+ tagTokens([
242
+ ...tokens,
243
+ ...shellGlobTokens(node.text).filter(token => !present.has(token)).map(token => ({ token, effect: globEffect })),
244
+ ], base, out);
233
245
  return this.foldCd(node, base);
234
246
  case "pipeline":
235
247
  // tree-sitter-bash mis-groups a redirect-bearing `&&`/`;` list as the
@@ -1,5 +1,5 @@
1
1
  // Datalab Kit adaptation
2
- import { isReadOnlyWords } from "../../../../../../dist/read-policy.js";
2
+ import { isReadOnlyWords, isKnownReadCommand } from "../../../../../../dist/read-policy.js";
3
3
  import { type TokenEffect, UNPROVEN_EFFECT } from "#src/access-intent/effect";
4
4
 
5
5
  // ── Public surface ─────────────────────────────────────────────────────────
@@ -26,7 +26,7 @@ export function proveCommandEffect(
26
26
  headWord: string,
27
27
  argWords: readonly string[],
28
28
  ): TokenEffect {
29
- if (isReadOnlyWords([headWord, ...argWords])) return CORE_READ_EFFECT;
29
+ if (isKnownReadCommand(headWord)) return isReadOnlyWords([headWord, ...argWords]) ? CORE_READ_EFFECT : UNPROVEN_EFFECT;
30
30
  if (!isBareCoreWord(headWord)) return UNPROVEN_EFFECT;
31
31
  const guard = RETRACTION_GUARDS.get(headWord);
32
32
  if (guard && argWords.some((word) => retractsClaim(word, guard))) {
@@ -1,5 +1,3 @@
1
- // Datalab Kit adaptation
2
- import { confirmPermission } from "../../../../../dist/confirmation.js";
3
1
  import type {
4
2
  ExtensionContext,
5
3
  ExtensionUIContext,
@@ -64,7 +62,6 @@ export interface PermissionPromptView extends PromptPreferences {
64
62
 
65
63
  /** Live prompt-behavior preferences read at prompt time (see `doublePressToConfirm`). */
66
64
  export interface PromptPreferences {
67
- kitUseUserDialog?: boolean;
68
65
  doublePressToConfirm: boolean;
69
66
  /** How much room a render has; the terminal width is added per frame. */
70
67
  budget: RenderBudget;
@@ -89,14 +86,6 @@ export async function requestPermissionDecision(
89
86
  title: string,
90
87
  payload: PromptPayload,
91
88
  options?: RequestPermissionOptions,
92
- ): Promise<PermissionPromptDecision> {
93
- if (view.kitUseUserDialog) return upstreamPermissionDecision(view, title, payload, options);
94
- const approved = await confirmPermission(view.ui, view.mode, title, payload);
95
- return attributeToHuman({ approved, state: approved ? 'approved' : 'denied' }, view.mode === 'tui' ? 'dialog' : 'select');
96
- }
97
-
98
- async function upstreamPermissionDecision(
99
- view: PermissionPromptView, title: string, payload: PromptPayload, options?: RequestPermissionOptions,
100
89
  ): Promise<PermissionPromptDecision> {
101
90
  if (view.mode === "tui") {
102
91
  return attributeToHuman(
@@ -1,5 +1,3 @@
1
- // Datalab Kit adaptation
2
-
3
1
  import {
4
2
  existsSync,
5
3
  mkdirSync,
@@ -84,8 +82,6 @@ export class ConfigStore
84
82
  {
85
83
  private config: PermissionSystemExtensionConfig;
86
84
  private configIssues: readonly string[] = [];
87
- private userDialog = false;
88
- usesUserDialog(): boolean { return this.userDialog; }
89
85
 
90
86
  constructor(private readonly deps: ConfigStoreDeps) {
91
87
  this.config = { ...DEFAULT_EXTENSION_CONFIG };
@@ -137,7 +133,6 @@ export class ConfigStore
137
133
  );
138
134
  const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
139
135
  this.config = runtimeConfig;
140
- this.userDialog = mergeResult.merged.doublePressToConfirm !== undefined || mergeResult.merged.permissionDialogKeys !== undefined;
141
136
 
142
137
  this.configIssues = mergeResult.issues;
143
138
 
@@ -1,5 +1,5 @@
1
1
  // Datalab Kit adaptation
2
-
2
+ import { inspectionCommands } from "../../../../../../dist/read-policy.js";
3
3
  import type {
4
4
  BashCommand,
5
5
  WrapperKind,
@@ -77,7 +77,7 @@ export function resolveBashCommandCheck(
77
77
  resolver: ScopedPermissionResolver,
78
78
  ): PermissionCheckResult {
79
79
  const kitWhole = resolveOnBashSurface(command, agentName, resolver);
80
- if (kitWhole.origin === 'builtin' && kitWhole.state === 'ask') return kitWhole;
80
+ if (kitWhole.state === 'deny') return kitWhole;
81
81
  if (isTriviallyEmptyCommand(command)) {
82
82
  return resolveOnBashSurface(command, agentName, resolver);
83
83
  }
@@ -106,13 +106,13 @@ export function resolveBashCommandCheck(
106
106
  }
107
107
  }
108
108
 
109
- const results = commands.map((cmd) =>
110
- resolveCommandUnit(cmd, command, agentName, resolver),
111
- );
112
- return (
113
- pickMostRestrictive(results) ??
114
- resolveOnBashSurface(command, agentName, resolver)
115
- );
109
+ const parsedCommands = inspectionCommands(command);
110
+ const results = [
111
+ ...commands.map(cmd => resolveCommandUnit(cmd, command, agentName, resolver)),
112
+ ...(parsedCommands?.map(text => resolveOnBashSurface(text, agentName, resolver)) ?? []),
113
+ ];
114
+ const worst = pickMostRestrictive(kitWhole.origin === 'builtin' && parsedCommands !== undefined ? results : [kitWhole, ...results]);
115
+ return worst ?? kitWhole;
116
116
  }
117
117
 
118
118
  /**
@@ -155,7 +155,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
155
155
  detection: subagentDetection,
156
156
  events: pi.events,
157
157
  getPromptPreferences: () => ({
158
- kitUseUserDialog: configStore.usesUserDialog(),
159
158
  doublePressToConfirm: configStore.current().doublePressToConfirm,
160
159
  budget: resolveRenderBudget(configStore.current()),
161
160
  dialogKeys: resolveDialogKeys(configStore.current()).keys,
@@ -381,9 +380,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
381
380
  gateRunner,
382
381
  );
383
382
 
384
- pi.on("session_start", (event, ctx) =>
385
- { kitSession = ctx.sessionManager; return lifecycle.handleSessionStart(event, ctx); },
386
- );
383
+ pi.on("session_start", async (event, ctx) => {
384
+ kitSession = ctx.sessionManager;
385
+ await lifecycle.handleSessionStart(event, ctx);
386
+ });
387
387
  pi.on("resources_discover", (event, ctx) =>
388
388
  lifecycle.handleResourcesDiscover(event, ctx),
389
389
  );
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dev-sergeev/datalab-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A ready-to-use collection of Pi coding agent extensions",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": { "node": ">=22.19.0" },
8
8
  "keywords": ["pi-package", "pi-coding-agent", "gigachat"],
9
9
  "publishConfig": { "access": "public" },
10
- "files": ["dist", "scripts/postinstall.mjs", "docs/adr", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md"],
10
+ "files": ["dist", "scripts/postinstall.mjs", "docs/adr", "README.md", "CHANGELOG.md", "LICENSE", "THIRD_PARTY_NOTICES.md"],
11
11
  "pi": {
12
12
  "extensions": [
13
13
  "./dist/extension.js",
@@ -1,33 +0,0 @@
1
- import type { ExtensionUIContext } from '@earendil-works/pi-coding-agent';
2
- type DialogTheme = {
3
- fg(color: 'warning' | 'accent' | 'muted', text: string): string;
4
- bold(text: string): string;
5
- };
6
- /** One instance represents one decision; it never remembers a session grant. */
7
- export declare class ConfirmationDialog {
8
- private readonly command;
9
- private readonly theme;
10
- private readonly done;
11
- private approve;
12
- private finished;
13
- private scroll;
14
- constructor(command: string, theme: DialogTheme, done: (approved: boolean) => void);
15
- handleInput(data: string): void;
16
- render(width: number): string[];
17
- invalidate(): void;
18
- }
19
- type PermissionPayload = {
20
- request: {
21
- value: string;
22
- toolName: string | null;
23
- surface: string;
24
- executedUnit: string | null;
25
- };
26
- evidence: readonly {
27
- label: string;
28
- text: string;
29
- detail: string | null;
30
- }[];
31
- };
32
- export declare function confirmPermission(ui: Pick<ExtensionUIContext, 'custom' | 'select'>, mode: string, title: string, payload: PermissionPayload): Promise<boolean>;
33
- export {};
@@ -1,64 +0,0 @@
1
- import { matchesKey, wrapTextWithAnsi } from '@earendil-works/pi-tui';
2
- /** One instance represents one decision; it never remembers a session grant. */
3
- export class ConfirmationDialog {
4
- command;
5
- theme;
6
- done;
7
- approve = false;
8
- finished = false;
9
- scroll = 0;
10
- constructor(command, theme, done) {
11
- this.command = command;
12
- this.theme = theme;
13
- this.done = done;
14
- }
15
- handleInput(data) {
16
- if (this.finished)
17
- return;
18
- if (matchesKey(data, 'down'))
19
- this.approve = true;
20
- else if (matchesKey(data, 'up'))
21
- this.approve = false;
22
- else if (matchesKey(data, 'pageDown'))
23
- this.scroll += 10;
24
- else if (matchesKey(data, 'pageUp'))
25
- this.scroll = Math.max(0, this.scroll - 10);
26
- else if (matchesKey(data, 'escape') || matchesKey(data, 'enter')) {
27
- this.finished = true;
28
- this.done(matchesKey(data, 'enter') && this.approve);
29
- }
30
- }
31
- render(width) {
32
- const usableWidth = Math.max(1, width - 4);
33
- const safeText = this.command.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '');
34
- const lines = safeText.split('\n').flatMap(line => wrapTextWithAnsi(line, usableWidth));
35
- this.scroll = Math.min(this.scroll, Math.max(0, lines.length - 12));
36
- const choices = ['Отмена', 'Разрешить один раз'];
37
- return [
38
- this.theme.fg('warning', this.theme.bold('ТРЕБУЕТСЯ ПОДТВЕРЖДЕНИЕ')),
39
- ...lines.slice(this.scroll, this.scroll + 12).map(line => this.theme.fg('warning', line)),
40
- ...(lines.length > 12 ? [this.theme.fg('muted', `Строки ${this.scroll + 1}–${Math.min(this.scroll + 12, lines.length)} из ${lines.length}; PgUp/PgDn — просмотр`)] : []),
41
- '',
42
- ...choices.map((label, index) => this.theme.fg(index === Number(this.approve) ? 'accent' : 'muted', `${index === Number(this.approve) ? '▶' : ' '} ${label}`)),
43
- this.theme.fg('muted', '↓ затем Enter — разрешить один раз; Enter по умолчанию / Esc — отменить'),
44
- ].flatMap(line => wrapTextWithAnsi(line, Math.max(1, width)));
45
- }
46
- invalidate() { }
47
- }
48
- export async function confirmPermission(ui, mode, title, payload) {
49
- const details = [title, `${payload.request.toolName ?? payload.request.surface}: ${payload.request.value}`,
50
- ...(payload.request.executedUnit ? [`Выполняется: ${payload.request.executedUnit}`] : []),
51
- ...payload.evidence.map(item => `${item.label}: ${item.text}${item.detail ? `\n${item.detail}` : ''}`),
52
- ].join('\n');
53
- if (mode !== 'tui') {
54
- return (await ui.select(details, ['Отмена', 'Разрешить один раз'])) === 'Разрешить один раз';
55
- }
56
- return ui.custom((tui, theme, _keys, done) => {
57
- const dialog = new ConfirmationDialog(details, theme, done);
58
- return {
59
- render: width => dialog.render(width),
60
- invalidate: () => dialog.invalidate(),
61
- handleInput: data => { dialog.handleInput(data); tui.requestRender(); },
62
- };
63
- });
64
- }