@dev-sergeev/datalab-kit 0.1.0 → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 — 2026-10-01
4
+
5
+ - Служебный todo и безопасные читающие цепочки с шаблонами файлов больше не требуют подтверждения по правилам набора.
6
+ - Добавлены сессионные команды `/yolo on`, `/yolo off`, `/yolo status`; постоянная настройка YOLO сохранена. Явные запреты и ограничения Plan mode остаются активными.
7
+ - Исправлен разбор имён файлов после `<`, которые могли скрыть изменяющую команду или опасную опцию.
8
+ - Исправлена атрибуция эффектов опасных опций и приоритет пользовательских правил в составных командах.
9
+ - Пользовательские ограничения путей проверяются для файлов, раскрытых shell-шаблонами, включая симлинки и команды после `cd`.
10
+ - Добавлены интеграционные матрицы читающих и изменяющих команд, обновление todo, проверки симлинков, режимов Plan/YOLO и смены сессии.
11
+
12
+ ## 0.1.0 — 2026-09-30
13
+
14
+ - Первый публичный выпуск набора из восьми дополнений для Pi coding agent.
15
+ - Значения по умолчанию разрешают чтение и требуют подтверждения изменений; пользовательские настройки имеют приоритет.
16
+ - Plan mode контролирует допустимость операций без дополнительных подтверждений набора.
17
+ - Общий предел параллельности субагентов; 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
 
@@ -42,6 +42,8 @@ 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` и прокси; перенаправления, вывод в файл и изменяющие запросы требуют подтверждения.
@@ -53,6 +55,12 @@ pi install ./dev-sergeev-datalab-kit-0.1.0.tgz
53
55
 
54
56
  В **Plan mode** допустимые инструменты и команды проверяет pi-plan-mode без дополнительных подтверждений по правилам набора. Явные пользовательские `ask` и `deny` продолжают действовать; некорректная конфигурация разрешений также не обходится. После выхода из Plan mode или перехода к реализации правила набора снова действуют. Это поведение сохраняется при восстановлении сессии и смене ветки. Plan mode допускает некоторые тесты и проверки, которые могут создавать артефакты.
55
57
 
58
+ ## YOLO
59
+
60
+ `/yolo on` отключает запросы подтверждения в текущей сессии; в статусе появляется `yolo`. `/yolo off` возвращает подтверждения, а `/yolo status` показывает эффективный режим. Команды не переписывают настройки; новая или повторно открытая сессия использует постоянную настройку.
61
+
62
+ YOLO автоматически разрешает любые `ask`, включая явные пользовательские запросы и запросы из-за некорректной конфигурации. Явные `deny` и ограничения Plan mode сохраняются. Для постоянного включения откройте `/permission-system` → **YOLO mode** или задайте `"yoloMode": true` в конфигурации permission-system. Сессионный `/yolo off` временно выключает и постоянно включённый YOLO.
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
  }
package/dist/yolo.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+ /** Session overrides stay in memory; persistent settings retain their own value. */
3
+ export declare function registerYoloControl(pi: ExtensionAPI, configured: () => boolean): {
4
+ isEnabled(): boolean;
5
+ refresh(ctx?: ExtensionContext): void;
6
+ };
package/dist/yolo.js ADDED
@@ -0,0 +1,33 @@
1
+ /** Session overrides stay in memory; persistent settings retain their own value. */
2
+ export function registerYoloControl(pi, configured) {
3
+ let override;
4
+ let context;
5
+ const enabled = () => override ?? configured();
6
+ const refresh = (ctx) => {
7
+ if (ctx)
8
+ context = ctx;
9
+ context?.ui.setStatus('pi-permission-system', enabled() ? 'yolo' : undefined);
10
+ };
11
+ pi.on('session_start', (_event, ctx) => {
12
+ override = undefined;
13
+ context = ctx;
14
+ });
15
+ pi.registerCommand('yolo', {
16
+ description: 'Set session YOLO: on, off, or status; explicit deny and Plan mode remain active',
17
+ getArgumentCompletions: prefix => ['on', 'off', 'status'].filter(value => value.startsWith(prefix))
18
+ .map(value => ({ value, label: value })),
19
+ handler: async (args, ctx) => {
20
+ context = ctx;
21
+ const command = args.trim().toLowerCase() || 'status';
22
+ if (!['on', 'off', 'status'].includes(command)) {
23
+ ctx.ui.notify('Usage: /yolo on|off|status', 'warning');
24
+ return;
25
+ }
26
+ if (command !== 'status')
27
+ override = command === 'on';
28
+ refresh();
29
+ ctx.ui.notify(`YOLO ${enabled() ? 'on' : 'off'} (${override === undefined ? 'settings' : 'session'}). Explicit deny and Plan mode remain active.`, 'info');
30
+ },
31
+ });
32
+ return { isEnabled: () => { refresh(); return enabled(); }, refresh };
33
+ }
@@ -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,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
  /**
@@ -1,6 +1,7 @@
1
1
  // Datalab Kit adaptation
2
2
  import { normalizeLocalReadCurl } from "../../../../dist/read-policy.js";
3
3
  import { isPlanModeActive } from "../../../../dist/plan-mode.js";
4
+ import { registerYoloControl } from "../../../../dist/yolo.js";
4
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
6
  import { getAgentDir, getPackageDir } from "@earendil-works/pi-coding-agent";
6
7
  import { warmBashParser } from "#src/access-intent/bash/parser";
@@ -113,7 +114,8 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
113
114
  // close over it; every call runs after configStore is assigned below. yolo is
114
115
  // a composition-stage ask→allow rewrite (#526) that the gate runner extends
115
116
  // to asks synthesized after resolution (#712), so both share this reader.
116
- const isYoloEnabled = (): boolean => isYoloModeEnabled(configStore.current());
117
+ const kitYolo = registerYoloControl(pi, () => isYoloModeEnabled(configStore.current()));
118
+ const isYoloEnabled = (): boolean => kitYolo.isEnabled();
117
119
 
118
120
  let kitSession: ExtensionContext["sessionManager"] | undefined;
119
121
  const permissionManager = new PermissionManager({
@@ -381,15 +383,25 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
381
383
  gateRunner,
382
384
  );
383
385
 
384
- pi.on("session_start", (event, ctx) =>
385
- { kitSession = ctx.sessionManager; return lifecycle.handleSessionStart(event, ctx); },
386
- );
386
+ pi.on("session_start", async (event, ctx) => {
387
+ kitSession = ctx.sessionManager;
388
+ await lifecycle.handleSessionStart(event, ctx);
389
+ kitYolo.refresh(ctx);
390
+ });
387
391
  pi.on("resources_discover", (event, ctx) =>
388
392
  lifecycle.handleResourcesDiscover(event, ctx),
389
393
  );
390
394
  pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
391
- pi.on("before_agent_start", (event, ctx) => agentPrep.handle(event, ctx));
392
- pi.on("input", (event, ctx) => gates.handleInput(event, ctx));
395
+ pi.on("before_agent_start", async (event, ctx) => {
396
+ const result = await agentPrep.handle(event, ctx);
397
+ kitYolo.refresh(ctx);
398
+ return result;
399
+ });
400
+ pi.on("input", async (event, ctx) => {
401
+ const result = await gates.handleInput(event, ctx);
402
+ kitYolo.refresh(ctx);
403
+ return result;
404
+ });
393
405
  pi.on(
394
406
  "tool_call",
395
407
  createFailClosedToolCall(
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.1",
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",