@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 +25 -0
- package/README.md +12 -4
- 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/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/authority/permission-prompt-component.ts +0 -11
- package/node_modules/@gotgenes/pi-permission-system/src/config/config-store.ts +0 -5
- 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 +4 -4
- package/package.json +2 -2
- package/dist/confirmation.d.ts +0 -33
- package/dist/confirmation.js +0 -64
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
|
|
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.
|
|
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
|
-
-
|
|
51
|
+
- Подтверждения показывает штатный диалог `@gotgenes/pi-permission-system`: разрешение одного запуска или на сессию, отказ и отказ с причиной. Настройки клавиш и повторного нажатия управляются самим расширением; собственного диалога Datalab Kit нет.
|
|
50
52
|
- Одновременно работает один субагент. Инструмент `subagent` принимает одну задачу за вызов.
|
|
51
53
|
|
|
52
|
-
Уже заданные пользователем настройки имеют приоритет, включая
|
|
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:
|
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/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))) {
|
package/node_modules/@gotgenes/pi-permission-system/src/authority/permission-prompt-component.ts
CHANGED
|
@@ -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.
|
|
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
|
/**
|
|
@@ -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
|
-
|
|
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.
|
|
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",
|
package/dist/confirmation.d.ts
DELETED
|
@@ -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 {};
|
package/dist/confirmation.js
DELETED
|
@@ -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
|
-
}
|