@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 +17 -0
- package/README.md +9 -1
- package/dist/defaults.d.ts +9 -2
- package/dist/defaults.js +3 -3
- package/dist/read-policy.d.ts +6 -0
- package/dist/read-policy.js +136 -17
- package/dist/yolo.d.ts +6 -0
- package/dist/yolo.js +33 -0
- package/node_modules/@gotgenes/pi-permission-system/src/access-intent/bash/bash-path-resolver.ts +17 -5
- package/node_modules/@gotgenes/pi-permission-system/src/access-intent/bash/command-effects.ts +2 -2
- package/node_modules/@gotgenes/pi-permission-system/src/handlers/gates/bash-command.ts +9 -9
- package/node_modules/@gotgenes/pi-permission-system/src/index.ts +18 -6
- package/package.json +2 -2
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
|
|
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:
|
package/dist/defaults.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
};
|
package/dist/read-policy.d.ts
CHANGED
|
@@ -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;
|
package/dist/read-policy.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
|
|
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 = () => {
|
|
9
|
-
|
|
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 === '`'
|
|
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
|
|
62
|
+
if (next === undefined)
|
|
32
63
|
return undefined;
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
if (next !== '\n') {
|
|
65
|
+
word += next;
|
|
66
|
+
started = true;
|
|
67
|
+
}
|
|
35
68
|
continue;
|
|
36
69
|
}
|
|
37
|
-
if (
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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' &&
|
|
147
|
+
if (name === 'rg' && options.some(arg => /^--pre/.test(arg)))
|
|
84
148
|
return false;
|
|
85
|
-
if (name === 'sort' &&
|
|
149
|
+
if (name === 'sort' && options.some(arg => dangerousLongOption(arg, ['--output', '--compress-program']) || /^-[^-]*o/.test(arg)))
|
|
86
150
|
return false;
|
|
87
|
-
if (name === 'diff' &&
|
|
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(
|
|
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 =
|
|
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
|
+
}
|
package/node_modules/@gotgenes/pi-permission-system/src/access-intent/bash/bash-path-resolver.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/node_modules/@gotgenes/pi-permission-system/src/access-intent/bash/command-effects.ts
CHANGED
|
@@ -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])
|
|
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.
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
392
|
-
|
|
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.
|
|
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",
|