Compare commits
16 Commits
e98e4ca9ae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1df4ea1b72 | |||
| de006ccf0c | |||
| be2d445a09 | |||
| 38f4ecf81f | |||
| 2995823a0f | |||
| f4ebdd745a | |||
| 3f987cecc5 | |||
| 5f8f5e8bf9 | |||
| 7e50043984 | |||
| 295f620710 | |||
| e04a2ddb72 | |||
| 489a63be69 | |||
| 6521d9c431 | |||
| c2cdf40cad | |||
| e6903f8b13 | |||
| a6a4d4fee3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
|
||||
38
CHANGELOG.md
Normal file
38
CHANGELOG.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии — [SemVer](https://semver.org/lang/ru/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.1] — 2026-07-16
|
||||
|
||||
### Fixed
|
||||
- Установочный one-liner: `git.codelab.vc` — это Gitea/Forgejo, а не GitLab; правильный
|
||||
raw-путь — `/raw/branch/stable/install.sh` (старый `/-/raw/main/` отдавал 404, «установка
|
||||
одной командой» не работала). Скрипт теперь берётся с канала `stable` (vetted).
|
||||
|
||||
## [0.2.0] — 2026-07-16
|
||||
|
||||
### Fixed
|
||||
- `install.sh` проверяет наличие `git` (нужен для `pi install git:...`) — на машине без git
|
||||
установка падала позже с непонятной ошибкой. Найдено при прогоне в чистой cps-песочнице.
|
||||
|
||||
### Added
|
||||
- Security guardrails: `secret-scanner` (блок записи секретов), `commit-guard`
|
||||
(Conventional Commits + скан секретов в сообщении), `llm-redaction` (редакция
|
||||
секретов перед отправкой в LLM), `audit-log` (локальный аудит + опциональный endpoint).
|
||||
- DX-команды: `/kit-doctor` (health-check окружения), `/kit-help` (каталог возможностей).
|
||||
- Каналы установки (`stable`/`beta`) и процесс релизов (`RELEASING.md`); `PI_KIT_CHANNEL`.
|
||||
- `config/company.json`: поле `auditEndpoint`.
|
||||
- Тест-харнес guardrails: `npm test`.
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
### Added
|
||||
- Базовый пакет: extensions (`company-context`, `protected-paths`, `permission-gate`),
|
||||
скилы (jira-workflow, repo-map, docs-map, go/rust/typescript/python/kotlin/swift-standards),
|
||||
команды `/bugfix` `/feature` `/review` `/rfc` `/kit-config`.
|
||||
- `install.sh` с выбором профиля, гейтингом скилов, установкой публичных скилов и
|
||||
интерактивной настройкой окружения (`~/.config/pi-kit/env.sh`).
|
||||
- `config/company.json` как единый источник правды + `remoteConfigUrl`.
|
||||
- Конвенции M.Video: URL (jira/wiki/gitlab), Conventional Commits, деплой `release-*`/`v*`.
|
||||
43
README.md
43
README.md
@@ -13,24 +13,28 @@ guardrails и скиллы со знанием того, где у нас код
|
||||
### Установка (одной командой)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.codelab.vc/ai/pi-kit/-/raw/main/install.sh | bash
|
||||
curl -fsSL https://git.codelab.vc/ai/pi-kit/raw/branch/stable/install.sh | bash
|
||||
```
|
||||
|
||||
Скрипт проверит Node.js (>= 20), поставит Pi (если нет), установит этот пакет, **спросит
|
||||
твой профиль**, поставит нужные скилы и подскажет, какие переменные окружения нужны
|
||||
(ключ вашего LLM-провайдера, `TRACKER_URL`, `JIRA_TOKEN`).
|
||||
твой профиль**, поставит нужные скилы и **интерактивно настроит окружение** (`TRACKER_URL`,
|
||||
`JIRA_TOKEN`, `GITLAB_TOKEN`, ключ LLM-провайдера). Секреты вводятся скрыто и сохраняются в
|
||||
`~/.config/pi-kit/env.sh` (`chmod 600`), который подключается из твоего `~/.zshrc`/`~/.bashrc`.
|
||||
Pi провайдер-агностичен — модель/провайдера пакет не навязывает, каждый выбирает свой
|
||||
(Anthropic, OpenAI, OpenRouter, …).
|
||||
|
||||
Пропустить интерактивную настройку окружения: `PI_KIT_SETUP_ENV=0`. Значения потом можно
|
||||
поменять прямо в `~/.config/pi-kit/env.sh` или перезапустив `./install.sh`.
|
||||
|
||||
Профиль можно задать заранее (для CI/неинтерактивной установки):
|
||||
|
||||
```bash
|
||||
PI_KIT_PROFILE=backend curl -fsSL https://git.codelab.vc/ai/pi-kit/-/raw/main/install.sh | bash
|
||||
PI_KIT_PROFILE=backend curl -fsSL https://git.codelab.vc/ai/pi-kit/raw/branch/stable/install.sh | bash
|
||||
```
|
||||
|
||||
### Профили и скилы
|
||||
|
||||
Общее для всех: команды `/bugfix /feature /review /rfc /kit-config`, guardrails, скилы
|
||||
Общее для всех: команды `/bugfix /feature /review /rfc /kit-config /kit-doctor /kit-help`, guardrails, скилы
|
||||
`jira-workflow`, `repo-map`, `docs-map`, и публичные `grill-me`, `grill-with-docs`,
|
||||
`code-review`, `diagnosing-bugs`. Дополнительно по профилю:
|
||||
|
||||
@@ -43,19 +47,22 @@ PI_KIT_PROFILE=backend curl -fsSL https://git.codelab.vc/ai/pi-kit/-/raw/main/in
|
||||
|
||||
Сменить профиль позже: `PI_KIT_PROFILE=frontend ./install.sh` (или перезапусти установку).
|
||||
|
||||
Либо вручную:
|
||||
Либо вручную (канал `stable` по умолчанию):
|
||||
|
||||
```bash
|
||||
pi install git:git.codelab.vc/ai/pi-kit
|
||||
pi install git:git.codelab.vc/ai/pi-kit@stable
|
||||
```
|
||||
|
||||
### Обновление
|
||||
### Обновление и каналы
|
||||
|
||||
```bash
|
||||
pi update --extensions
|
||||
```
|
||||
|
||||
Пакет установлен без версионного ref, поэтому обновление всегда тянет актуальный `main`.
|
||||
Установка идёт по **каналу** — движущемуся git-ref: `stable` (проверенные релизы, по умолчанию)
|
||||
или `beta` (обкатка). `pi update --extensions` следует за твоим каналом. Сменить канал:
|
||||
`PI_KIT_CHANNEL=beta ./install.sh` (или закрепиться на теге: `PI_KIT_CHANNEL=v1.2.3`).
|
||||
Процесс релизов — в [`RELEASING.md`](RELEASING.md).
|
||||
|
||||
### Команды
|
||||
|
||||
@@ -66,6 +73,8 @@ pi update --extensions
|
||||
| `/review [фокус]` | Ревью текущего диффа по стандартам Go (с фокусом, если задан) |
|
||||
| `/rfc <тема>` | Черновик RFC по типовому шаблону |
|
||||
| `/kit-config` | Показать действующие корпоративные значения и их источник (local/remote) |
|
||||
| `/kit-doctor` | Health-check окружения (node, версия/канал, конфиг, env, токены) |
|
||||
| `/kit-help` | Каталог возможностей: команды, скилы, guardrails |
|
||||
|
||||
### Скиллы
|
||||
|
||||
@@ -78,9 +87,13 @@ pi update --extensions
|
||||
|
||||
### Guardrails (встроенная защита)
|
||||
|
||||
- **protected-paths** — блокирует запись/редактирование секретов, ключей, `.git/`, прод-конфигов.
|
||||
- **protected-paths** — блокирует запись/редактирование секретов, ключей, `.git/`, прод-конфигов (по пути).
|
||||
- **permission-gate** — переспрашивает перед опасными командами (`rm -rf`, force push,
|
||||
push в `main`/`master`/`release/*`, прод-`kubectl`, `docker system prune`, `sudo`).
|
||||
push в `main`/`master`/`release-*`, тег `v*` (прод-деплой), прод-`kubectl`, `docker system prune`, `sudo`).
|
||||
- **secret-scanner** — блокирует запись контента, похожего на секрет (по содержимому).
|
||||
- **commit-guard** — требует Conventional Commits и сканирует сообщение коммита на секреты.
|
||||
- **llm-redaction** — вырезает секреты из payload перед отправкой в LLM.
|
||||
- **audit-log** — локальный аудит срабатываний в `~/.config/pi-kit/audit.jsonl` (опц. endpoint).
|
||||
|
||||
---
|
||||
|
||||
@@ -173,13 +186,21 @@ pi-kit/
|
||||
- Файл в `extensions/*.ts`, `export default function (pi: ExtensionAPI) {}`.
|
||||
- В шапке каждого расширения — блок: **что делает / как настроить / как отключить**.
|
||||
- Пути к файлам пакета резолвь через `import.meta.url`, не через `cwd`.
|
||||
- Общий код расширений — в `extensions/lib/*.ts` (не `index.ts` → Pi не грузит как расширение).
|
||||
- Pi-библиотеки — только в `peerDependencies` (`"*"`), не бандлить.
|
||||
- Guardrails/секреты покрыты тестами — `npm test` (`test/guardrails.test.ts`).
|
||||
|
||||
### Релизы
|
||||
|
||||
Раскатка по каналам (`stable`/`beta`) и semver-тегам — процесс в [`RELEASING.md`](RELEASING.md).
|
||||
`CHANGELOG.md` обновляется в каждом релизе. Аудит-endpoint — поле `auditEndpoint` в `config/company.json`.
|
||||
|
||||
### Локальная проверка перед MR
|
||||
|
||||
```bash
|
||||
# из корня репозитория
|
||||
pi install ./ # установить пакет из локальной папки
|
||||
npm test # прогнать тесты guardrails
|
||||
pi # запустить: расширения загрузятся, команды /bugfix и т.д. появятся
|
||||
|
||||
# опциональный строгий typecheck (нужны peer-типы Pi):
|
||||
|
||||
47
RELEASING.md
Normal file
47
RELEASING.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Релизы и каналы `@mvideo/pi-kit`
|
||||
|
||||
Пакет исполняет код на машинах всех разработчиков, поэтому раскатка контролируется
|
||||
**каналами** и **semver-тегами**, а не «всегда latest».
|
||||
|
||||
## Каналы
|
||||
|
||||
- **`stable`** — ветка, на которую сотрудники установлены по умолчанию
|
||||
(`git:git.codelab.vc/ai/pi-kit@stable`). Указывает на проверенный релиз.
|
||||
- **`beta`** — ветка для обкатки перед `stable` (по желанию). Установка: `PI_KIT_CHANNEL=beta`.
|
||||
- Можно закрепиться на конкретный тег: `PI_KIT_CHANNEL=v1.2.3`.
|
||||
|
||||
`pi update --extensions` тянет текущее состояние канала, на который установлен пользователь.
|
||||
|
||||
## Как выпустить релиз
|
||||
|
||||
1. Собрать изменения в `main` через MR с ревью (правило репозитория).
|
||||
2. Прогнать проверки: `npm test` (guardrails), `npm run typecheck` (при наличии peer-типов),
|
||||
`pi install ./` + `/kit-doctor`.
|
||||
3. Обновить версию в `package.json` и запись в `CHANGELOG.md` (раздел из `Unreleased`).
|
||||
4. Поставить тег на нужный коммит `main`:
|
||||
```bash
|
||||
git tag -a vX.Y.Z -m "vX.Y.Z"
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
5. Перевести канал на этот тег (fast-forward ветки канала):
|
||||
```bash
|
||||
git branch -f beta vX.Y.Z # сначала на beta для обкатки
|
||||
git push origin beta
|
||||
# после обкатки:
|
||||
git branch -f stable vX.Y.Z
|
||||
git push origin stable
|
||||
```
|
||||
6. Сотрудники получат обновление обычным `pi update --extensions`.
|
||||
|
||||
## Semver
|
||||
|
||||
- **patch** (`vX.Y.Z+1`) — фиксы guardrails/скилов/шаблонов без изменения поведения команд.
|
||||
- **minor** — новые скилы/команды/расширения, обратно совместимо.
|
||||
- **major** — несовместимые изменения (удаление команд, смена формата `config/company.json`).
|
||||
|
||||
## Откат
|
||||
|
||||
Перевести `stable` на предыдущий тег и запушить:
|
||||
```bash
|
||||
git branch -f stable vX.Y.(Z-1) && git push origin stable
|
||||
```
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"gitHost": "git.codelab.vc",
|
||||
"trackerUrl": "TODO",
|
||||
"docsUrl": "TODO",
|
||||
"gitHost": "gitlab.tech.mvideo.ru",
|
||||
"trackerUrl": "https://jira.mvideo.ru",
|
||||
"docsUrl": "https://wiki.mvideo.ru",
|
||||
"repoMap": "TODO: где какой код лежит",
|
||||
"rules": [
|
||||
"не коммитить секреты",
|
||||
"не пушить в main",
|
||||
"MR обязателен"
|
||||
],
|
||||
"remoteConfigUrl": null
|
||||
"remoteConfigUrl": null,
|
||||
"auditEndpoint": null
|
||||
}
|
||||
|
||||
35
config/mcp.json
Normal file
35
config/mcp.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"//": "MCP server registry consumed by extensions/mcp-bridge.ts. All servers are DISABLED by default — enable one, set its token env var, then restart pi. URLs are pulled from config/company.json (single source of truth); tokens from env. Templates: {config:KEY} → company.json value, {env:VAR} → environment variable. Requires VPN/creds and the launcher (uvx/npx) to actually connect.",
|
||||
"servers": {
|
||||
"jira": {
|
||||
"enabled": false,
|
||||
"description": "Jira (Server/DC) через mcp-atlassian (Python, запускается uvx)",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-atlassian"],
|
||||
"env": {
|
||||
"JIRA_URL": "{config:trackerUrl}",
|
||||
"JIRA_PERSONAL_TOKEN": "{env:JIRA_TOKEN}"
|
||||
}
|
||||
},
|
||||
"confluence": {
|
||||
"enabled": false,
|
||||
"description": "Confluence (Server/DC) через mcp-atlassian",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-atlassian"],
|
||||
"env": {
|
||||
"CONFLUENCE_URL": "{config:docsUrl}",
|
||||
"CONFLUENCE_PERSONAL_TOKEN": "{env:CONFLUENCE_TOKEN}"
|
||||
}
|
||||
},
|
||||
"gitlab": {
|
||||
"enabled": false,
|
||||
"description": "GitLab (self-hosted) через @zereight/mcp-gitlab (запускается npx)",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@zereight/mcp-gitlab"],
|
||||
"env": {
|
||||
"GITLAB_API_URL": "https://{config:gitHost}/api/v4",
|
||||
"GITLAB_PERSONAL_ACCESS_TOKEN": "{env:GITLAB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
docs/mcp.md
Normal file
47
docs/mcp.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# MCP-интеграции (Jira / Confluence / GitLab)
|
||||
|
||||
Pi не имеет встроенного MCP — его добавляет расширение `extensions/mcp-bridge.ts`. Оно читает
|
||||
`config/mcp.json`, поднимает **включённые** серверы (stdio) и регистрирует их инструменты в pi.
|
||||
**По умолчанию все серверы выключены.** Статус — команда `/mcp-status`.
|
||||
|
||||
## Как устроено
|
||||
|
||||
- Серверы объявлены в `config/mcp.json`. URL берутся из `config/company.json` (`{config:trackerUrl}`,
|
||||
`{config:docsUrl}`, `{config:gitHost}`), токены — из окружения (`{env:JIRA_TOKEN}` и т.д.).
|
||||
В коде и конфиге нет захардкоженных адресов и секретов.
|
||||
- Bridge подставляет значения при запуске сервера. Если URL/токен не заданы — сервер помечается
|
||||
`missing-env` и пропускается.
|
||||
|
||||
## Предпосылки (нужны только при включении)
|
||||
|
||||
- **VPN** и доступ к `jira.mvideo.ru` / `wiki.mvideo.ru` / `gitlab.tech.mvideo.ru`.
|
||||
- MCP SDK: `npm i @modelcontextprotocol/sdk` в пакете (в `optionalDependencies`; ставится только
|
||||
при включении MCP).
|
||||
- Лаунчеры серверов: `uvx` (для `mcp-atlassian`) и/или `npx` (для `@zereight/mcp-gitlab`).
|
||||
- Токены в окружении (см. `~/.config/pi-kit/env.sh`).
|
||||
|
||||
## Включение
|
||||
|
||||
1. В `config/mcp.json` поставь `"enabled": true` нужному серверу.
|
||||
2. Задай токен в окружении:
|
||||
- Jira: `JIRA_TOKEN` (PAT Server/DC).
|
||||
- Confluence: `CONFLUENCE_TOKEN` (PAT Server/DC).
|
||||
- GitLab: `GITLAB_TOKEN` (scope `api`).
|
||||
3. Установи SDK (если ещё нет): `npm i @modelcontextprotocol/sdk`.
|
||||
4. Перезапусти `pi`. Проверь: `/mcp-status` → `connected` и число tools.
|
||||
|
||||
## Серверы
|
||||
|
||||
| Сервер | Пакет | URL из | Токен |
|
||||
|---|---|---|---|
|
||||
| `jira` | `mcp-atlassian` (uvx) | `trackerUrl` | `JIRA_TOKEN` |
|
||||
| `confluence` | `mcp-atlassian` (uvx) | `docsUrl` | `CONFLUENCE_TOKEN` |
|
||||
| `gitlab` | `@zereight/mcp-gitlab` (npx) | `gitHost` → `/api/v4` | `GITLAB_TOKEN` |
|
||||
|
||||
<!-- TODO: сверить с корпоративной политикой — какие MCP-серверы одобрены, разрешён ли uvx/npx
|
||||
на машинах, или нужен предустановленный/зеркалированный образ. -->
|
||||
|
||||
## Безопасность
|
||||
|
||||
MCP-серверы — сторонние процессы с полным доступом к твоим токенам. Включай только одобренные
|
||||
образы. Инструменты MCP проходят через guardrails pi (permission-gate/audit) как обычные вызовы.
|
||||
51
extensions/audit-log.ts
Normal file
51
extensions/audit-log.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Audit Log Extension
|
||||
*
|
||||
* WHAT: Records session starts and slash-command usage to the local audit log
|
||||
* (~/.config/pi-kit/audit.jsonl). Guardrail blocks are recorded by the
|
||||
* guardrail extensions themselves via the shared audit() helper. If an
|
||||
* audit endpoint is configured it is forwarded best-effort (see below).
|
||||
*
|
||||
* CONFIGURE: set `auditEndpoint` in config/company.json, or the PI_KIT_AUDIT_URL
|
||||
* env var, to forward events. Turn everything off with PI_KIT_AUDIT_OFF=1.
|
||||
*
|
||||
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { audit } from "./lib/audit.ts";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_PATH = path.join(HERE, "..", "config", "company.json");
|
||||
|
||||
/** Wire company.json `auditEndpoint` into the env var read by audit() (unless already set). */
|
||||
function initEndpointFromConfig(): void {
|
||||
if (process.env.PI_KIT_AUDIT_URL) return;
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
if (typeof cfg.auditEndpoint === "string" && cfg.auditEndpoint) {
|
||||
process.env.PI_KIT_AUDIT_URL = cfg.auditEndpoint;
|
||||
}
|
||||
} catch {
|
||||
// No config / unreadable — local-only auditing.
|
||||
}
|
||||
}
|
||||
|
||||
export default function auditLogExtension(pi: ExtensionAPI) {
|
||||
initEndpointFromConfig();
|
||||
|
||||
pi.on("session_start", async (event) => {
|
||||
audit({ kind: "session_start", reason: (event as { reason?: string }).reason });
|
||||
});
|
||||
|
||||
// Record slash-command usage (name only, no arguments — args may contain secrets).
|
||||
pi.on("input", async (event) => {
|
||||
const text = (event as { text?: string }).text ?? "";
|
||||
const m = text.match(/^\/([a-z0-9:_-]+)/i);
|
||||
if (m) audit({ kind: "command", command: m[1] });
|
||||
return { action: "continue" };
|
||||
});
|
||||
}
|
||||
60
extensions/commit-guard.ts
Normal file
60
extensions/commit-guard.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Commit Guard Extension
|
||||
*
|
||||
* WHAT: On `git commit -m …`, validates the message against Conventional
|
||||
* Commits and scans it for secrets. Invalid/secret → confirm (UI) or
|
||||
* block (no UI). Non-`-m` commits (editor) are left alone.
|
||||
*
|
||||
* CONFIGURE: edit COMMIT_TYPES below. Secret patterns live in
|
||||
* extensions/lib/secrets.ts.
|
||||
*
|
||||
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { scanSecrets } from "./lib/secrets.ts";
|
||||
import { audit } from "./lib/audit.ts";
|
||||
|
||||
// Conventional Commits types accepted in the header. Extend as needed.
|
||||
const COMMIT_TYPES = ["feat", "fix", "chore", "docs", "refactor", "test", "perf", "build", "ci", "style", "revert"];
|
||||
const HEADER_RE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\(.+\\))?!?: .+`);
|
||||
|
||||
/** Extract the -m / --message value from a `git commit` command, if present. */
|
||||
function extractCommitMessage(command: string): string | null {
|
||||
// Matches -m "msg", -m 'msg', --message=msg, -m msg (best effort).
|
||||
const m =
|
||||
command.match(/(?:-m|--message)[=\s]+"([^"]*)"/) ??
|
||||
command.match(/(?:-m|--message)[=\s]+'([^']*)'/) ??
|
||||
command.match(/(?:-m|--message)[=\s]+(\S+)/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
export default function commitGuardExtension(pi: ExtensionAPI) {
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "bash") return undefined;
|
||||
const command = (event.input.command as string | undefined) ?? "";
|
||||
if (!/\bgit\s+commit\b/.test(command)) return undefined;
|
||||
|
||||
const message = extractCommitMessage(command);
|
||||
if (message === null) return undefined; // editor-based commit; not our concern
|
||||
|
||||
const problems: string[] = [];
|
||||
if (!HEADER_RE.test(message)) problems.push("не Conventional Commits");
|
||||
const secretHits = scanSecrets(message);
|
||||
if (secretHits.length > 0) problems.push(`секрет (${secretHits.join(", ")})`);
|
||||
|
||||
if (problems.length === 0) return undefined;
|
||||
|
||||
audit({ kind: "guardrail_block", guardrail: "commit-guard", problems, hasSecret: secretHits.length > 0 });
|
||||
|
||||
if (ctx.hasUI) {
|
||||
const choice = await ctx.ui.select(
|
||||
`⚠️ Проблема с сообщением коммита: ${problems.join("; ")}.\nФормат: type(scope): описание. Всё равно закоммитить?`,
|
||||
["Нет", "Да"],
|
||||
);
|
||||
if (choice === "Да") return undefined;
|
||||
}
|
||||
|
||||
return { block: true, reason: `Commit message rejected: ${problems.join("; ")}` };
|
||||
});
|
||||
}
|
||||
118
extensions/kit-cli.ts
Normal file
118
extensions/kit-cli.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Kit CLI Extension
|
||||
*
|
||||
* WHAT: developer-experience commands for the corporate kit.
|
||||
* /kit-doctor — environment health check (node, kit version, config, env vars,
|
||||
* env.sh, loaded commands/skills). Read-only, offline-safe.
|
||||
* /kit-help — catalog of everything this package provides (commands, skills,
|
||||
* guardrails).
|
||||
*
|
||||
* CONFIGURE: nothing to configure. Values are read from config/company.json and
|
||||
* the environment. (`/kit-config` — in company-context.ts — shows the
|
||||
* effective corporate config.)
|
||||
*
|
||||
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PKG_ROOT = path.join(HERE, "..");
|
||||
const CONFIG_PATH = path.join(PKG_ROOT, "config", "company.json");
|
||||
const ENV_FILE = path.join(os.homedir(), ".config", "pi-kit", "env.sh");
|
||||
|
||||
const PROVIDER_KEYS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"];
|
||||
|
||||
// Guardrail extensions don't register commands, so list them statically here.
|
||||
const GUARDRAILS: Array<[string, string]> = [
|
||||
["protected-paths", "блок записи в секреты/ключи/.git/прод-конфиги (по пути)"],
|
||||
["permission-gate", "подтверждение опасных bash (rm -rf, force push, release-*/v* деплой, sudo …)"],
|
||||
["secret-scanner", "блок записи контента, похожего на секрет"],
|
||||
["commit-guard", "Conventional Commits + скан секретов в сообщении коммита"],
|
||||
["llm-redaction", "редакция секретов из payload перед отправкой в LLM"],
|
||||
["audit-log", "локальный аудит (~/.config/pi-kit/audit.jsonl)"],
|
||||
];
|
||||
|
||||
function readVersion(): string {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version ?? "?";
|
||||
} catch {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
function readConfig(): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Show lines in a dismissable selector (UI) or as a notification (no UI). */
|
||||
async function show(ctx: any, title: string, lines: string[]): Promise<void> {
|
||||
if (ctx.hasUI) await ctx.ui.select(title, lines);
|
||||
else ctx.ui.notify(`${title}\n${lines.join("\n")}`, "info");
|
||||
}
|
||||
|
||||
export default function kitCliExtension(pi: ExtensionAPI) {
|
||||
pi.registerCommand("kit-doctor", {
|
||||
description: "Проверка окружения pi-kit (node, версия, конфиг, env, токены)",
|
||||
handler: async (_args, ctx) => {
|
||||
const ok = (s: string) => `✓ ${s}`;
|
||||
const warn = (s: string) => `⚠ ${s}`;
|
||||
const cfg = readConfig();
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(ok(`Node.js ${process.version}`));
|
||||
lines.push(ok(`pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`));
|
||||
|
||||
// Config
|
||||
const cfgOk = typeof cfg.gitHost === "string" && cfg.gitHost !== "TODO";
|
||||
lines.push((cfgOk ? ok : warn)(`config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`));
|
||||
if (cfg.remoteConfigUrl) lines.push(ok(`remoteConfigUrl задан`));
|
||||
|
||||
// Env vars (presence only, never values)
|
||||
for (const v of ["TRACKER_URL", "JIRA_TOKEN", "GITLAB_TOKEN"]) {
|
||||
lines.push((process.env[v] ? ok : warn)(`${v} ${process.env[v] ? "задан" : "не задан"}`));
|
||||
}
|
||||
const provider = PROVIDER_KEYS.find((k) => process.env[k]);
|
||||
lines.push((provider ? ok : warn)(provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден"));
|
||||
|
||||
// env.sh
|
||||
lines.push((fs.existsSync(ENV_FILE) ? ok : warn)(`env.sh ${fs.existsSync(ENV_FILE) ? "есть" : "нет"} (${ENV_FILE})`));
|
||||
|
||||
// Loaded commands/skills
|
||||
const cmds = pi.getCommands();
|
||||
const skills = cmds.filter((c) => c.source === "skill").length;
|
||||
lines.push(ok(`команд загружено: ${cmds.length} (скилов: ${skills})`));
|
||||
|
||||
const warns = lines.filter((l) => l.startsWith("⚠")).length;
|
||||
lines.push("", warns === 0 ? "Итог: всё ок." : `Итог: предупреждений — ${warns}.`);
|
||||
await show(ctx, "pi-kit doctor", lines);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("kit-help", {
|
||||
description: "Каталог возможностей pi-kit (команды, скилы, guardrails)",
|
||||
handler: async (_args, ctx) => {
|
||||
const cmds = pi.getCommands();
|
||||
const mine = cmds.filter((c) => c.sourceInfo?.path?.startsWith(PKG_ROOT));
|
||||
const bySource = (src: string) =>
|
||||
mine
|
||||
.filter((c) => c.source === src)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((c) => ` /${c.name}${c.description ? ` — ${c.description}` : ""}`);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push("— Команды —", ...bySource("extension"), ...bySource("prompt"));
|
||||
lines.push("", "— Скилы —", ...bySource("skill"));
|
||||
lines.push("", "— Guardrails (расширения) —", ...GUARDRAILS.map(([n, d]) => ` ${n} — ${d}`));
|
||||
await show(ctx, "pi-kit help", lines);
|
||||
},
|
||||
});
|
||||
}
|
||||
47
extensions/lib/audit.ts
Normal file
47
extensions/lib/audit.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Shared local audit log used by the guardrail extensions.
|
||||
*
|
||||
* WHAT: appends JSONL events to ~/.config/pi-kit/audit.jsonl (chmod 600).
|
||||
* Never logs secret values — only pattern names, paths, command shape.
|
||||
* If PI_KIT_AUDIT_URL is set (or company.json auditEndpoint, wired by
|
||||
* audit-log.ts into that env var), each event is also POSTed best-effort.
|
||||
*
|
||||
* DISABLE: remove the audit-log.ts extension, or set PI_KIT_AUDIT_OFF=1.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const DIR = path.join(os.homedir(), ".config", "pi-kit");
|
||||
const FILE = path.join(DIR, "audit.jsonl");
|
||||
const POST_TIMEOUT_MS = 1500;
|
||||
|
||||
export interface AuditEntry {
|
||||
kind: string; // e.g. "guardrail_block", "command", "session_start"
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Append an audit entry locally and optionally forward it to an endpoint. */
|
||||
export function audit(entry: AuditEntry): void {
|
||||
if (process.env.PI_KIT_AUDIT_OFF === "1") return;
|
||||
const line = JSON.stringify({ t: new Date().toISOString(), ...entry });
|
||||
|
||||
try {
|
||||
fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
||||
fs.appendFileSync(FILE, `${line}\n`, { mode: 0o600 });
|
||||
} catch {
|
||||
// Auditing must never break the agent; swallow local write errors.
|
||||
}
|
||||
|
||||
const url = process.env.PI_KIT_AUDIT_URL;
|
||||
if (url) {
|
||||
// Best-effort, fire-and-forget. Never awaited; failures are ignored.
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: line,
|
||||
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
49
extensions/lib/secrets.ts
Normal file
49
extensions/lib/secrets.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Shared secret detection/redaction used by secret-scanner, commit-guard and
|
||||
* llm-redaction extensions.
|
||||
*
|
||||
* CONFIGURE: add entries to SECRET_PATTERNS below. Each pattern has a `name`
|
||||
* (shown in warnings/audit, never the secret itself) and a `re` (no global
|
||||
* flag — the global flag is added on demand for redaction).
|
||||
*/
|
||||
|
||||
export interface SecretPattern {
|
||||
name: string;
|
||||
re: RegExp;
|
||||
}
|
||||
|
||||
// Patterns are deliberately conservative to limit false positives. The user
|
||||
// still confirms interactively, so a rare false positive is cheap.
|
||||
export const SECRET_PATTERNS: SecretPattern[] = [
|
||||
{ name: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
||||
{ name: "gitlab-pat", re: /\bglpat-[0-9A-Za-z_-]{20,}\b/ },
|
||||
{ name: "github-pat", re: /\bghp_[0-9A-Za-z]{36}\b/ },
|
||||
{ name: "github-fine-grained-pat", re: /\bgithub_pat_[0-9A-Za-z_]{22,}\b/ },
|
||||
{ name: "slack-token", re: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/ },
|
||||
{ name: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
||||
{ name: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/ },
|
||||
// Generic "secret: value" / "token=value" assignments with a non-trivial value.
|
||||
{ name: "generic-credential", re: /\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*["']?[^\s"']{8,}["']?/i },
|
||||
];
|
||||
|
||||
/** Return the names of secret patterns found in `text` (empty if none). */
|
||||
export function scanSecrets(text: string): string[] {
|
||||
if (!text) return [];
|
||||
const hits: string[] = [];
|
||||
for (const p of SECRET_PATTERNS) {
|
||||
// Use a non-stateful copy (no global flag) to avoid lastIndex surprises.
|
||||
if (p.re.test(text)) hits.push(p.name);
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
/** Replace every secret occurrence with a labelled placeholder. */
|
||||
export function redact(text: string): string {
|
||||
if (!text) return text;
|
||||
let out = text;
|
||||
for (const p of SECRET_PATTERNS) {
|
||||
const g = new RegExp(p.re.source, p.re.flags.includes("g") ? p.re.flags : `${p.re.flags}g`);
|
||||
out = out.replace(g, `«REDACTED:${p.name}»`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
51
extensions/llm-redaction.ts
Normal file
51
extensions/llm-redaction.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* LLM Redaction Extension
|
||||
*
|
||||
* WHAT: Before each provider request, redacts secrets from the outbound
|
||||
* payload so tokens/keys never leave the machine in a prompt. Uses the
|
||||
* `before_provider_request` hook and returns a modified payload.
|
||||
*
|
||||
* CONFIGURE: secret patterns live in extensions/lib/secrets.ts.
|
||||
*
|
||||
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { redact } from "./lib/secrets.ts";
|
||||
import { audit } from "./lib/audit.ts";
|
||||
|
||||
/** Recursively redact secrets in every string within a JSON-like value. */
|
||||
function redactDeep(value: unknown, counter: { n: number }): unknown {
|
||||
if (typeof value === "string") {
|
||||
const cleaned = redact(value);
|
||||
if (cleaned !== value) counter.n++;
|
||||
return cleaned;
|
||||
}
|
||||
if (Array.isArray(value)) return value.map((v) => redactDeep(v, counter));
|
||||
if (value && typeof value === "object") {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
out[k] = redactDeep(v, counter);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export default function llmRedactionExtension(pi: ExtensionAPI) {
|
||||
pi.on("before_provider_request", async (event) => {
|
||||
const payload = event.payload;
|
||||
if (!payload) return undefined;
|
||||
|
||||
const counter = { n: 0 };
|
||||
// Only rewrite the message content to avoid disturbing payload metadata.
|
||||
const messages = (payload as { messages?: unknown }).messages;
|
||||
if (messages === undefined) return undefined;
|
||||
|
||||
const redactedMessages = redactDeep(messages, counter);
|
||||
if (counter.n === 0) return undefined;
|
||||
|
||||
audit({ kind: "redaction", occurrences: counter.n });
|
||||
return { ...payload, messages: redactedMessages };
|
||||
});
|
||||
}
|
||||
156
extensions/mcp-bridge.ts
Normal file
156
extensions/mcp-bridge.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* MCP Bridge Extension
|
||||
*
|
||||
* WHAT: Pi has no built-in MCP. This bridge reads config/mcp.json, and for each
|
||||
* ENABLED server launches it (stdio) via the MCP SDK and registers its
|
||||
* tools into pi. URLs come from config/company.json ({config:KEY}), tokens
|
||||
* from the environment ({env:VAR}) — nothing hardcoded. All servers are
|
||||
* disabled by default; `/mcp-status` shows their state.
|
||||
*
|
||||
* ENABLE: set a server's "enabled": true in config/mcp.json, provide its token
|
||||
* env var, install the MCP SDK once (`npm i @modelcontextprotocol/sdk` in
|
||||
* the package, or globally) and the launcher (uvx/npx), then restart pi.
|
||||
* Requires network access to the corporate services (VPN).
|
||||
*
|
||||
* DISABLE: keep servers disabled, or remove/rename this file.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const MCP_CONFIG = path.join(HERE, "..", "config", "mcp.json");
|
||||
const COMPANY_CONFIG = path.join(HERE, "..", "config", "company.json");
|
||||
const CONNECT_TIMEOUT_MS = 8000;
|
||||
|
||||
interface ServerSpec {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
type Status = "disabled" | "connected" | "error" | "no-sdk" | "missing-env";
|
||||
interface ServerState {
|
||||
description: string;
|
||||
status: Status;
|
||||
tools: string[];
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
function readJson(file: string): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Replace {config:KEY} and {env:VAR} tokens; returns null if any resolve empty. */
|
||||
function resolveEnv(
|
||||
env: Record<string, string>,
|
||||
company: Record<string, unknown>,
|
||||
): Record<string, string> | null {
|
||||
const out: Record<string, string> = {};
|
||||
for (const [key, template] of Object.entries(env)) {
|
||||
let missing = false;
|
||||
const value = template.replace(/\{(config|env):([^}]+)\}/g, (_m, kind, name) => {
|
||||
const v = kind === "config" ? company[name] : process.env[name];
|
||||
if (typeof v !== "string" || v === "" || v === "TODO") missing = true;
|
||||
return typeof v === "string" ? v : "";
|
||||
});
|
||||
if (missing) return null;
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default async function mcpBridgeExtension(pi: ExtensionAPI) {
|
||||
const mcpCfg = readJson(MCP_CONFIG);
|
||||
const company = readJson(COMPANY_CONFIG);
|
||||
const servers = (mcpCfg.servers ?? {}) as Record<string, ServerSpec>;
|
||||
const state = new Map<string, ServerState>();
|
||||
const closers: Array<() => Promise<void> | void> = [];
|
||||
|
||||
// Seed status for every configured server.
|
||||
for (const [key, spec] of Object.entries(servers)) {
|
||||
state.set(key, { description: spec.description ?? key, status: spec.enabled ? "error" : "disabled", tools: [] });
|
||||
}
|
||||
|
||||
pi.registerCommand("mcp-status", {
|
||||
description: "Показать сконфигурированные MCP-серверы (Jira/Confluence/GitLab) и их статус",
|
||||
handler: async (_args, ctx) => {
|
||||
const lines: string[] = [];
|
||||
for (const [key, s] of state) {
|
||||
const t = s.tools.length ? ` — ${s.tools.length} tools` : "";
|
||||
lines.push(`${key}: ${s.status}${t}${s.detail ? ` (${s.detail})` : ""}`);
|
||||
lines.push(` ${s.description}`);
|
||||
}
|
||||
if (lines.length === 0) lines.push("MCP-серверы не сконфигурированы (config/mcp.json).");
|
||||
lines.push("", "Включить: enabled:true в config/mcp.json + токен в env + перезапуск pi.");
|
||||
if (ctx.hasUI) await ctx.ui.select("MCP-серверы", lines);
|
||||
else ctx.ui.notify(lines.join("\n"), "info");
|
||||
},
|
||||
});
|
||||
|
||||
// Connect enabled servers. Nothing runs here when all are disabled (default).
|
||||
for (const [key, spec] of Object.entries(servers)) {
|
||||
if (!spec.enabled) continue;
|
||||
const st = state.get(key)!;
|
||||
|
||||
const env = resolveEnv(spec.env ?? {}, company);
|
||||
if (env === null) {
|
||||
st.status = "missing-env";
|
||||
st.detail = "нет URL/токена (config/env)";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamically import so a missing SDK never breaks loading for others.
|
||||
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
||||
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: spec.command,
|
||||
args: spec.args ?? [],
|
||||
env: { ...process.env, ...env } as Record<string, string>,
|
||||
});
|
||||
const client = new Client({ name: `pi-kit-${key}`, version: "0.1.0" }, { capabilities: {} });
|
||||
await Promise.race([
|
||||
client.connect(transport),
|
||||
new Promise((_r, reject) => setTimeout(() => reject(new Error("timeout")), CONNECT_TIMEOUT_MS)),
|
||||
]);
|
||||
closers.push(() => client.close().catch(() => {}));
|
||||
|
||||
const { tools } = await client.listTools();
|
||||
for (const tool of tools ?? []) {
|
||||
const toolName = `${key}_${tool.name}`;
|
||||
pi.registerTool({
|
||||
name: toolName,
|
||||
label: toolName,
|
||||
description: tool.description ?? `${key} MCP tool ${tool.name}`,
|
||||
// MCP inputSchema is JSON Schema; pi accepts a JSON-Schema-shaped object.
|
||||
parameters: (tool.inputSchema as unknown) ?? { type: "object", properties: {} },
|
||||
async execute(_id: string, params: unknown) {
|
||||
const res: any = await client.callTool({ name: tool.name, arguments: (params ?? {}) as Record<string, unknown> });
|
||||
return { content: res.content ?? [{ type: "text", text: JSON.stringify(res) }], details: {} };
|
||||
},
|
||||
});
|
||||
st.tools.push(toolName);
|
||||
}
|
||||
st.status = "connected";
|
||||
// Connection status is visible via /mcp-status (no ctx in the factory).
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
st.status = /Cannot find( package)?|ERR_MODULE_NOT_FOUND|@modelcontextprotocol/.test(msg) ? "no-sdk" : "error";
|
||||
st.detail = st.status === "no-sdk" ? "установи @modelcontextprotocol/sdk" : msg.slice(0, 80);
|
||||
}
|
||||
}
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
for (const close of closers) await close();
|
||||
});
|
||||
}
|
||||
@@ -22,10 +22,12 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
const DANGEROUS_PATTERNS: Array<{ re: RegExp; reason: string }> = [
|
||||
{ re: /\brm\s+(-\w*r\w*f|-\w*f\w*r|--recursive)/i, reason: "recursive force delete (rm -rf)" },
|
||||
{ re: /\bgit\s+push\b[^\n]*(--force\b|--force-with-lease\b|\s-f\b)/i, reason: "force push" },
|
||||
{
|
||||
re: /\bgit\s+push\b[^\n]*\b(main|master|release\/[^\s]+)\b/i,
|
||||
reason: "push to a protected branch (main/master/release/*)",
|
||||
},
|
||||
{ re: /\bgit\s+push\b[^\n]*\b(main|master)\b/i, reason: "push to main/master" },
|
||||
// Deploy convention: a `release-*` branch push deploys to STAGE, and a `v*`
|
||||
// tag push deploys to PROD. Guard both the branch and the tag pushes.
|
||||
{ re: /\bgit\s+push\b[^\n]*\brelease[-/]\S+/i, reason: "push to a release-* branch (deploys to stage)" },
|
||||
{ re: /\bgit\s+push\b[^\n]*(--tags\b|--follow-tags\b)/i, reason: "push tags (a v* tag deploys to PROD)" },
|
||||
{ re: /\bgit\s+push\b[^\n]*\sv\d[\w.-]*/i, reason: "push a v* version tag (deploys to PROD)" },
|
||||
{
|
||||
re: /\bkubectl\b[^\n]*(--context[=\s]?\S*prod|--namespace[=\s]?\S*prod|\bctx-prod\b)/i,
|
||||
reason: "kubectl against a production context/namespace",
|
||||
|
||||
46
extensions/secret-scanner.ts
Normal file
46
extensions/secret-scanner.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Secret Scanner Extension
|
||||
*
|
||||
* WHAT: Blocks `write`/`edit` tool calls whose content looks like a secret
|
||||
* (API keys, tokens, private keys, credentials). Complements
|
||||
* protected-paths (which blocks by path) by scanning content.
|
||||
*
|
||||
* CONFIGURE: edit SECRET_PATTERNS in extensions/lib/secrets.ts.
|
||||
*
|
||||
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { scanSecrets } from "./lib/secrets.ts";
|
||||
import { audit } from "./lib/audit.ts";
|
||||
|
||||
export default function secretScannerExtension(pi: ExtensionAPI) {
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
|
||||
|
||||
// Scan every string field of the input except the path (content, new_string,
|
||||
// etc.) so we don't depend on a specific tool's field naming.
|
||||
const input = event.input as Record<string, unknown>;
|
||||
const parts: string[] = [];
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (key === "path") continue;
|
||||
if (typeof value === "string") parts.push(value);
|
||||
}
|
||||
|
||||
const hits = scanSecrets(parts.join("\n"));
|
||||
if (hits.length === 0) return undefined;
|
||||
|
||||
const path = typeof input.path === "string" ? input.path : "(unknown)";
|
||||
audit({ kind: "guardrail_block", guardrail: "secret-scanner", tool: event.toolName, path, patterns: hits });
|
||||
|
||||
if (ctx.hasUI) {
|
||||
const choice = await ctx.ui.select(
|
||||
`⚠️ Похоже на секрет (${hits.join(", ")}) в записи → ${path}\nВсё равно записать?`,
|
||||
["Нет", "Да"],
|
||||
);
|
||||
if (choice === "Да") return undefined;
|
||||
}
|
||||
|
||||
return { block: true, reason: `Possible secret in content (${hits.join(", ")})` };
|
||||
});
|
||||
}
|
||||
162
install.sh
162
install.sh
@@ -3,7 +3,7 @@
|
||||
# pi-kit bootstrap — one-shot setup for new engineers. Idempotent: safe to re-run.
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://git.codelab.vc/ai/pi-kit/-/raw/main/install.sh | bash
|
||||
# curl -fsSL https://git.codelab.vc/ai/pi-kit/raw/branch/stable/install.sh | bash
|
||||
# # or, from a checkout:
|
||||
# ./install.sh
|
||||
#
|
||||
@@ -14,10 +14,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The ONLY place the repository address appears. No version ref on purpose, so
|
||||
# `pi update --extensions` always pulls the latest main.
|
||||
# The ONLY place the repository address appears. Employees install a CHANNEL
|
||||
# (a moving git ref): `stable` (default, vetted releases) or `beta`. Maintainers
|
||||
# fast-forward the channel branch to a tagged release. `pi update --extensions`
|
||||
# then follows that channel. Override with PI_KIT_CHANNEL=beta (or a tag like v1.2.3).
|
||||
# ---------------------------------------------------------------------------
|
||||
PI_KIT_REPO="git:git.codelab.vc/ai/pi-kit"
|
||||
PI_KIT_HOST="git.codelab.vc/ai/pi-kit"
|
||||
PI_KIT_CHANNEL="${PI_KIT_CHANNEL:-stable}"
|
||||
PI_KIT_REPO="git:${PI_KIT_HOST}@${PI_KIT_CHANNEL}"
|
||||
|
||||
MIN_NODE_MAJOR=20
|
||||
INTERNAL_COURSE_URL="TODO: ссылка на внутренний курс по Pi"
|
||||
@@ -48,6 +52,13 @@ if [ "$NODE_MAJOR" -lt "$MIN_NODE_MAJOR" ]; then
|
||||
fi
|
||||
info "Node.js $(node -v) — ок."
|
||||
|
||||
# --- 1b. git (required by `pi install git:...`) ------------------------------
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
err "git не найден. Установи git (macOS: xcode-select --install; Ubuntu: apt install git) и запусти скрипт снова."
|
||||
exit 1
|
||||
fi
|
||||
info "git $(git --version | awk '{print $3}') — ок."
|
||||
|
||||
# --- 2. Pi ------------------------------------------------------------------
|
||||
if command -v pi >/dev/null 2>&1; then
|
||||
info "Pi уже установлен ($(pi --version 2>/dev/null || echo 'версия неизвестна'))."
|
||||
@@ -187,10 +198,17 @@ if [ "${#PROFILE_PUBLIC[@]}" -gt 0 ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# --- 7. Provider API keys ---------------------------------------------------
|
||||
# Pi is provider-agnostic: engineers use different LLM providers (Anthropic,
|
||||
# OpenAI, OpenRouter, Google, ...). The package does NOT pin a provider/model.
|
||||
# We only check that *some* provider key is present and hint if none is.
|
||||
# --- 7. Environment (URLs + secrets) ---------------------------------------
|
||||
# Interactive setup: prompts (via /dev/tty so it works under curl | bash),
|
||||
# stores chosen values in ~/.config/pi-kit/env.sh (chmod 600) and sources it
|
||||
# from the shell rc. Secrets are read hidden and never echoed/logged.
|
||||
# URLs have no package default yet (config/company.json is filled by maintainers);
|
||||
# the current value is offered as the default. Skip with PI_KIT_SETUP_ENV=0.
|
||||
CONFIG_DIR="$HOME/.config/pi-kit"
|
||||
ENV_FILE="$CONFIG_DIR/env.sh"
|
||||
RC_MARK=">>> pi-kit >>>"
|
||||
|
||||
# Which provider key (if any) is already present in the environment.
|
||||
PROVIDER_KEYS=(ANTHROPIC_API_KEY OPENAI_API_KEY OPENROUTER_API_KEY GEMINI_API_KEY GOOGLE_API_KEY GROQ_API_KEY)
|
||||
found_key=""
|
||||
for k in "${PROVIDER_KEYS[@]}"; do
|
||||
@@ -199,21 +217,109 @@ for k in "${PROVIDER_KEYS[@]}"; do
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$found_key" ]; then
|
||||
info "Найден ключ LLM-провайдера: ${found_key} — ок."
|
||||
else
|
||||
warn "Не найден ни один ключ LLM-провайдера."
|
||||
cat <<'EOF'
|
||||
Pi работает с разными провайдерами — выбери свой и задай ключ, например:
|
||||
export ANTHROPIC_API_KEY="..." # Anthropic
|
||||
export OPENAI_API_KEY="..." # OpenAI
|
||||
export OPENROUTER_API_KEY="..." # OpenRouter
|
||||
Провайдер и модель выбираются флагами --provider/--model или в настройках Pi
|
||||
(см. `pi --list-models`) — пакет их не навязывает.
|
||||
|
||||
Для скилла работы с трекером также понадобятся:
|
||||
export TRACKER_URL="..." # адрес Jira (или возьмётся из корп. контекста)
|
||||
export JIRA_TOKEN="..." # Jira personal access token (Server/DC)
|
||||
detect_rc() {
|
||||
case "$(basename "${SHELL:-}")" in
|
||||
zsh) echo "$HOME/.zshrc" ;;
|
||||
bash) echo "$HOME/.bashrc" ;;
|
||||
*) echo "$HOME/.profile" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Escape a value for safe embedding inside single quotes.
|
||||
esc_sq() { printf "%s" "$1" | sed "s/'/'\\\\''/g"; }
|
||||
|
||||
env_setup_interactive() {
|
||||
info "Настройка окружения — значения сохранятся в ${ENV_FILE} (chmod 600)."
|
||||
# Load previously stored values so re-runs keep them as defaults.
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE" || true
|
||||
|
||||
local tracker="${TRACKER_URL:-}" jira="${JIRA_TOKEN:-}" gitlab="${GITLAB_TOKEN:-}" ans=""
|
||||
|
||||
# TRACKER_URL (not a secret): visible prompt, current value as default.
|
||||
printf 'TRACKER_URL (адрес Jira)%s: ' "${tracker:+ [${tracker}]}" >/dev/tty
|
||||
read -r ans </dev/tty || ans=""
|
||||
[ -n "$ans" ] && tracker="$ans"
|
||||
|
||||
# JIRA_TOKEN (secret): hidden input; empty keeps the current value.
|
||||
printf 'JIRA_TOKEN (скрытый ввод, Enter — оставить как есть): ' >/dev/tty
|
||||
read -rs ans </dev/tty || ans=""
|
||||
printf '\n' >/dev/tty
|
||||
[ -n "$ans" ] && jira="$ans"
|
||||
|
||||
# GITLAB_TOKEN (secret, для create-mr.sh): hidden input.
|
||||
printf 'GITLAB_TOKEN (скрытый ввод, Enter — пропустить): ' >/dev/tty
|
||||
read -rs ans </dev/tty || ans=""
|
||||
printf '\n' >/dev/tty
|
||||
[ -n "$ans" ] && gitlab="$ans"
|
||||
|
||||
# LLM provider key — only ask if none is already set.
|
||||
local prov_name="" prov_val=""
|
||||
if [ -z "$found_key" ]; then
|
||||
printf 'LLM-провайдер: 1) anthropic 2) openai 3) openrouter 4) пропустить [1-4]: ' >/dev/tty
|
||||
read -r ans </dev/tty || ans=""
|
||||
case "$ans" in
|
||||
1 | anthropic) prov_name=ANTHROPIC_API_KEY ;;
|
||||
2 | openai) prov_name=OPENAI_API_KEY ;;
|
||||
3 | openrouter) prov_name=OPENROUTER_API_KEY ;;
|
||||
*) prov_name="" ;;
|
||||
esac
|
||||
if [ -n "$prov_name" ]; then
|
||||
printf '%s (скрытый ввод): ' "$prov_name" >/dev/tty
|
||||
read -rs prov_val </dev/tty || prov_val=""
|
||||
printf '\n' >/dev/tty
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write env file with restrictive permissions (umask 077 => 600).
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
(
|
||||
umask 077
|
||||
{
|
||||
echo "# Managed by pi-kit install.sh — не коммить. chmod 600, plaintext."
|
||||
[ -n "$tracker" ] && echo "export TRACKER_URL='$(esc_sq "$tracker")'"
|
||||
[ -n "$jira" ] && echo "export JIRA_TOKEN='$(esc_sq "$jira")'"
|
||||
[ -n "$gitlab" ] && echo "export GITLAB_TOKEN='$(esc_sq "$gitlab")'"
|
||||
[ -n "$prov_name" ] && [ -n "$prov_val" ] && echo "export ${prov_name}='$(esc_sq "$prov_val")'"
|
||||
} >"$ENV_FILE"
|
||||
)
|
||||
chmod 600 "$ENV_FILE"
|
||||
info "Сохранено: ${ENV_FILE}"
|
||||
|
||||
# Ensure the shell rc sources the env file (managed block, idempotent).
|
||||
local rc
|
||||
rc="$(detect_rc)"
|
||||
if [ -f "$rc" ] && grep -qF "$RC_MARK" "$rc" 2>/dev/null; then
|
||||
info "Подключение уже есть в ${rc}."
|
||||
else
|
||||
{
|
||||
echo ""
|
||||
echo "# ${RC_MARK}"
|
||||
echo '[ -f "$HOME/.config/pi-kit/env.sh" ] && . "$HOME/.config/pi-kit/env.sh"'
|
||||
echo "# <<< pi-kit <<<"
|
||||
} >>"$rc"
|
||||
info "Добавил подключение в ${rc}. Применить сейчас: source ${rc}"
|
||||
fi
|
||||
warn "Токены лежат в ${ENV_FILE} (chmod 600, plaintext). Для строгих требований — секрет-менеджер."
|
||||
}
|
||||
|
||||
if [ "${PI_KIT_SETUP_ENV:-1}" != 0 ] && [ -r /dev/tty ]; then
|
||||
env_setup_interactive
|
||||
else
|
||||
# Non-interactive fallback: just check for a provider key and print guidance.
|
||||
if [ -n "$found_key" ]; then
|
||||
info "Найден ключ LLM-провайдера: ${found_key} — ок."
|
||||
else
|
||||
warn "Не найден ключ LLM-провайдера (установка неинтерактивная — пропускаю настройку)."
|
||||
fi
|
||||
cat <<'EOF'
|
||||
Задай нужные переменные вручную (например, в ~/.config/pi-kit/env.sh):
|
||||
export ANTHROPIC_API_KEY="..." # или OPENAI_API_KEY / OPENROUTER_API_KEY
|
||||
export TRACKER_URL="..." # адрес Jira (Server/DC)
|
||||
export JIRA_TOKEN="..." # Jira personal access token
|
||||
export GITLAB_TOKEN="..." # для создания MR (scripts/create-mr.sh)
|
||||
Провайдер/модель — флагами --provider/--model или в настройках Pi (пакет не навязывает).
|
||||
EOF
|
||||
fi
|
||||
|
||||
@@ -221,14 +327,16 @@ fi
|
||||
cat <<EOF
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
Готово! Профиль: ${PROFILE}
|
||||
Готово! Профиль: ${PROFILE} Канал: ${PI_KIT_CHANNEL}
|
||||
|
||||
Запуск: pi
|
||||
Обновление: pi update --extensions
|
||||
Обновление: pi update --extensions (следует за каналом ${PI_KIT_CHANNEL})
|
||||
Окружение: ${ENV_FILE} (TRACKER_URL/JIRA_TOKEN/GITLAB_TOKEN/ключ провайдера)
|
||||
Сменить профиль: PI_KIT_PROFILE=backend ./install.sh (или перезапусти и выбери)
|
||||
Команды: /bugfix <тикет> /feature <тикет>
|
||||
/review [фокус] /rfc <тема> /kit-config
|
||||
Проверка: /kit-config — покажет действующие корпоративные значения
|
||||
Сменить канал: PI_KIT_CHANNEL=beta ./install.sh
|
||||
Команды: /bugfix <тикет> /feature <тикет> /review [фокус] /rfc <тема>
|
||||
/kit-config /kit-doctor /kit-help
|
||||
Проверка: /kit-doctor — health-check окружения
|
||||
|
||||
Внутренний курс по Pi: ${INTERNAL_COURSE_URL}
|
||||
────────────────────────────────────────────────────────
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mvideo/pi-kit",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Корпоративный Pi-пакет: шаблоны задач, guardrails и скиллы для команды.",
|
||||
@@ -27,15 +27,19 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --experimental-strip-types test/guardrails.test.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"@earendil-works/pi-ai": "*",
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"@earendil-works/pi-tui": "*",
|
||||
"typebox": "*"
|
||||
},
|
||||
"dependencies": {},
|
||||
"optionalDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Используй при написании, ревью и про
|
||||
|
||||
Наш основной стек — **Go**. Скилл описывает, как писать, проверять и оформлять код.
|
||||
Реалистичные общепринятые дефолты приведены ниже; то, что специфично для компании,
|
||||
помечено `TODO` — уточняй, не выдумывай.
|
||||
заполнено дефолтами индустрии — тюним под M.Video позже.
|
||||
|
||||
## Структура сервиса
|
||||
|
||||
@@ -21,7 +21,7 @@ pkg/ # переиспользуемый публичный
|
||||
api/ # спецификации API (proto/openapi)
|
||||
```
|
||||
|
||||
<!-- TODO: заполнить реальной эталонной структурой сервиса компании -->
|
||||
> Ориентир по индустрии: `cmd/` + `internal/` + `pkg/`. Эталон под M.Video уточним позже.
|
||||
|
||||
## Линтеры
|
||||
|
||||
@@ -29,19 +29,23 @@ api/ # спецификации API (proto/openapi)
|
||||
- Статический анализ: `go vet ./...`.
|
||||
- Основной линтер: **golangci-lint** — `golangci-lint run ./...`.
|
||||
Набор линтеров задаётся в `.golangci.yml` в корне репозитория.
|
||||
<!-- TODO: указать корпоративный .golangci.yml / общий пресет, если он есть -->
|
||||
Базовый набор: `govet`, `staticcheck`, `errcheck`, `ineffassign`, `gosimple`, `unused`.
|
||||
Корпоративный `.golangci.yml` добавим позже.
|
||||
|
||||
## Тесты
|
||||
|
||||
- Прогон всех тестов: `go test ./...`.
|
||||
- С гонками и покрытием: `go test -race -cover ./...`.
|
||||
- Требования к покрытию и обязательные виды тестов: `<!-- TODO: пороги покрытия, интеграционные тесты -->`.
|
||||
- Покрытие: мягкий ориентир ~70%+ для бизнес-логики, критичные пути — обязательно;
|
||||
интеграционные тесты для внешних зависимостей. (пороги уточним)
|
||||
- Именование: `TestXxx`, табличные тесты приветствуются.
|
||||
|
||||
## Коммиты и MR
|
||||
|
||||
- **Формат коммитов:** `<!-- TODO: например Conventional Commits (feat:, fix:, chore:) или свой -->`.
|
||||
- **Ветки:** от основной, именование `<!-- TODO: напр. feature/PROJ-123-short-desc -->`.
|
||||
- **Формат коммитов:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` …).
|
||||
- **Ветки и деплой:** фича-ветки — от `main` (в имя ветки желательно включать ключ тикета).
|
||||
Деплой управляется ветками/тегами: ветка `release-*` → раскатка на **stage**; тег `v*` → раскатка в **прод**.
|
||||
Ветки `release-*` и теги `v*` без необходимости не пушь — `permission-gate` переспросит.
|
||||
- **MR обязателен** (см. корпоративные правила из контекста сессии). Прямой push в защищённые
|
||||
ветки запрещён — расширение `permission-gate` дополнительно переспросит.
|
||||
- **Описание MR:** что и зачем изменено, как проверено (линтеры + тесты), ссылка на тикет
|
||||
|
||||
@@ -52,16 +52,17 @@ GITLAB_TOKEN=... ./scripts/create-mr.sh "Fix PROJ-123: NPE" main "" PROJ-123
|
||||
|
||||
Флаги окружения `create-mr.sh`: `GITLAB_TOKEN` (обязателен), `GITLAB_HOST` (override хоста),
|
||||
`MR_PUSH=0` (не пушить ветку), `MR_DRY_RUN=1` (показать запрос без вызова API).
|
||||
MR из защищённой ветки (`main`/`master`/`release/*`) скрипт создавать откажется.
|
||||
MR из защищённой/релизной ветки (`main`/`master`/`release-*`) скрипт создавать откажется.
|
||||
|
||||
## Соглашения
|
||||
|
||||
<!-- TODO: заполнить реальными значениями компании -->
|
||||
|
||||
- **Формат ключей задач:** `<!-- TODO: например PROJ-123, префиксы проектов -->`
|
||||
- **Статусы и переходы:** `<!-- TODO: рабочий процесс, напр. To Do → In Progress → In Review → Done -->`
|
||||
- **Кто и когда меняет статус:** `<!-- TODO: договорённости команды -->`
|
||||
- **Что писать в комментарии при старте/готовности:** `<!-- TODO -->`
|
||||
- **Формат ключей задач:** вида `PROJ-123` (буквенный префикс проекта + номер).
|
||||
`<!-- TODO: перечислить реальные префиксы проектов -->`
|
||||
- **Статусы и переходы:** To Do → In Progress → Review → Done.
|
||||
- **Кто и когда меняет статус:** разработчик переводит тикет: в **In Progress** — при старте работы;
|
||||
в **Review** — при выставлении MR (в тикет добавляется ссылка на MR); в **Done** — после мержа.
|
||||
- **Что писать в комментарии:** при старте — кратко о начале работы; при готовности — ссылку на MR
|
||||
(используй `create-mr.sh`, который сразу приложит ссылку, либо `link-mr.sh`).
|
||||
|
||||
## Полезное
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ JIRA_KEY="${4:-}"
|
||||
|
||||
# --- Refuse to open an MR *from* a protected branch (safety). ----------------
|
||||
case "$SOURCE" in
|
||||
main | master | release/*)
|
||||
echo "Refusing: source branch '$SOURCE' looks protected. Create a feature branch first." >&2
|
||||
main | master | release-* | release/*)
|
||||
echo "Refusing: source branch '$SOURCE' looks protected/release. Create a feature branch first." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Используй при написании, ревью и про
|
||||
# Стандарты Kotlin
|
||||
|
||||
Один из наших языков — **Kotlin** (в первую очередь Android). Ниже — общепринятые дефолты
|
||||
экосистемы; специфичное для компании помечено `TODO` — уточняй, не выдумывай.
|
||||
экосистемы; специфичное для компании заполнено дефолтами индустрии — тюним под M.Video позже.
|
||||
|
||||
## Структура
|
||||
|
||||
@@ -22,20 +22,20 @@ app/ # Android-модуль приложения
|
||||
<feature-module>/ # модульная структура
|
||||
```
|
||||
|
||||
<!-- TODO: заполнить реальной эталонной структурой проекта/модулей и версией AGP/Gradle -->
|
||||
> Ориентир: модульный Android-проект на Gradle (Kotlin DSL), актуальные AGP/Gradle. Эталон уточним.
|
||||
|
||||
## Линтеры и форматирование
|
||||
|
||||
- Стиль/формат: **ktlint** — `./gradlew ktlintCheck` (автоформат `./gradlew ktlintFormat`).
|
||||
- Статический анализ: **detekt** — `./gradlew detekt` (конфиг `detekt.yml`).
|
||||
- Android Lint: `./gradlew lint`.
|
||||
<!-- TODO: корпоративные конфиги ktlint/detekt (общий пресет), если есть -->
|
||||
Дефолт: ktlint (official code style) + стартовый `detekt.yml`. Корпоративный пресет добавим позже.
|
||||
|
||||
## Тесты
|
||||
|
||||
- Unit: `./gradlew test`.
|
||||
- Инструментальные (Android): `./gradlew connectedAndroidTest` (нужен эмулятор/устройство).
|
||||
- Требования к покрытию / обязательные виды тестов: `<!-- TODO -->`.
|
||||
- Покрытие: замер через JaCoCo, мягкий ориентир; критичную логику — обязательно. (пороги уточним)
|
||||
|
||||
## Сборка и зависимости
|
||||
|
||||
@@ -44,7 +44,10 @@ app/ # Android-модуль приложения
|
||||
|
||||
## Коммиты и MR
|
||||
|
||||
- **Формат коммитов:** `<!-- TODO: например Conventional Commits или свой -->`.
|
||||
- **Формат коммитов:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` …).
|
||||
- **Ветки и деплой:** фича-ветки — от `main` (в имя ветки желательно включать ключ тикета).
|
||||
Деплой управляется ветками/тегами: ветка `release-*` → раскатка на **stage**; тег `v*` → раскатка в **прод**.
|
||||
Ветки `release-*` и теги `v*` без необходимости не пушь — `permission-gate` переспросит.
|
||||
- **MR обязателен** (см. корпоративные правила из контекста сессии). Прямой push в защищённые
|
||||
ветки запрещён — расширение `permission-gate` дополнительно переспросит.
|
||||
- Перед MR: `ktlintCheck`, `detekt`, `test` (и `lint`) — всё зелёное.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Используй при написании, ревью и про
|
||||
# Стандарты Python
|
||||
|
||||
Один из наших языков — **Python**. Ниже — общепринятые дефолты экосистемы; специфичное для
|
||||
компании помечено `TODO` — уточняй, не выдумывай.
|
||||
компании заполнено дефолтами индустрии — тюним под M.Video позже.
|
||||
|
||||
## Структура
|
||||
|
||||
@@ -19,28 +19,32 @@ src/<package>/ # src-layout
|
||||
tests/
|
||||
```
|
||||
|
||||
<!-- TODO: заполнить реальной эталонной структурой / шаблоном (менеджер: poetry/uv/pip-tools?) -->
|
||||
> Ориентир: src-layout, `pyproject.toml`, окружение через uv. Эталон уточним.
|
||||
|
||||
## Линтеры и форматирование
|
||||
|
||||
- Линтер + форматтер: **ruff** — `ruff check .` и `ruff format --check .`
|
||||
(локально `ruff format .`). Конфиг — в `pyproject.toml` (`[tool.ruff]`).
|
||||
- Типы: **mypy** — `mypy .` (конфиг `[tool.mypy]`).
|
||||
<!-- TODO: корпоративный набор правил ruff/mypy (строгость), если есть -->
|
||||
Дефолт: ruff (правила `E`,`F`,`I`,`UP`,`B`) + mypy строгий для нового кода. Корпоративный пресет уточним.
|
||||
|
||||
## Тесты
|
||||
|
||||
- Прогон: **pytest** — `pytest` (с покрытием — `pytest --cov`).
|
||||
- Требования к покрытию / обязательные виды тестов: `<!-- TODO -->`.
|
||||
- Покрытие: `uv run pytest --cov`, мягкий ориентир ~80% для бизнес-логики. (пороги уточним)
|
||||
|
||||
## Окружение и зависимости
|
||||
|
||||
- Менеджер зависимостей и lock: `<!-- TODO: poetry / uv / pip-tools -->` — lock коммитим.
|
||||
- Виртуальное окружение — обязательно; глобально пакеты не ставим.
|
||||
- Менеджер зависимостей: **uv** — `uv sync` (установка), `uv add <pkg>`; lock `uv.lock` коммитим.
|
||||
- Команды запускай через `uv run …` (напр. `uv run pytest`, `uv run ruff check .`, `uv run mypy .`).
|
||||
- Виртуальное окружение (`.venv`) создаёт uv; глобально пакеты не ставим.
|
||||
|
||||
## Коммиты и MR
|
||||
|
||||
- **Формат коммитов:** `<!-- TODO: например Conventional Commits или свой -->`.
|
||||
- **Формат коммитов:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` …).
|
||||
- **Ветки и деплой:** фича-ветки — от `main` (в имя ветки желательно включать ключ тикета).
|
||||
Деплой управляется ветками/тегами: ветка `release-*` → раскатка на **stage**; тег `v*` → раскатка в **прод**.
|
||||
Ветки `release-*` и теги `v*` без необходимости не пушь — `permission-gate` переспросит.
|
||||
- **MR обязателен** (см. корпоративные правила из контекста сессии). Прямой push в защищённые
|
||||
ветки запрещён — расширение `permission-gate` дополнительно переспросит.
|
||||
- Перед MR: `ruff check .`, `ruff format --check .`, `mypy .`, `pytest` — всё зелёное.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Используй при написании, ревью и про
|
||||
# Стандарты Rust
|
||||
|
||||
Один из наших языков — **Rust**. Ниже — общепринятые дефолты экосистемы; специфичное для
|
||||
компании помечено `TODO` — уточняй, не выдумывай.
|
||||
компании заполнено дефолтами индустрии — тюним под M.Video позже.
|
||||
|
||||
## Структура
|
||||
|
||||
@@ -21,20 +21,20 @@ tests/ # интеграционные тесты
|
||||
benches/ # бенчмарки (если есть)
|
||||
```
|
||||
|
||||
<!-- TODO: заполнить реальной эталонной структурой crate/workspace компании -->
|
||||
> Ориентир: стандартный cargo-лейаут (`src/`, `tests/`), workspace при нескольких crate. Эталон уточним.
|
||||
|
||||
## Линтеры и форматирование
|
||||
|
||||
- Форматирование: `cargo fmt --all` (проверка в CI — `cargo fmt --all -- --check`).
|
||||
- Линтер: **clippy** — `cargo clippy --all-targets --all-features -- -D warnings`.
|
||||
Настройки — в `clippy.toml` / атрибутах crate.
|
||||
<!-- TODO: корпоративный набор clippy-lints, если есть -->
|
||||
Дефолт: clippy с `-D warnings`; точечные `allow` — только с комментарием. Корпоративный набор уточним.
|
||||
|
||||
## Тесты
|
||||
|
||||
- Прогон: `cargo test --all-features`.
|
||||
- Для workspace: `cargo test --workspace`.
|
||||
- Требования к покрытию / обязательные виды тестов: `<!-- TODO -->`.
|
||||
- Покрытие: `cargo-llvm-cov` (или tarpaulin), мягкий ориентир ~70%+. (пороги уточним)
|
||||
|
||||
## Сборка и зависимости
|
||||
|
||||
@@ -44,7 +44,10 @@ benches/ # бенчмарки (если есть)
|
||||
|
||||
## Коммиты и MR
|
||||
|
||||
- **Формат коммитов:** `<!-- TODO: например Conventional Commits или свой -->`.
|
||||
- **Формат коммитов:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` …).
|
||||
- **Ветки и деплой:** фича-ветки — от `main` (в имя ветки желательно включать ключ тикета).
|
||||
Деплой управляется ветками/тегами: ветка `release-*` → раскатка на **stage**; тег `v*` → раскатка в **прод**.
|
||||
Ветки `release-*` и теги `v*` без необходимости не пушь — `permission-gate` переспросит.
|
||||
- **MR обязателен** (см. корпоративные правила из контекста сессии). Прямой push в защищённые
|
||||
ветки запрещён — расширение `permission-gate` дополнительно переспросит.
|
||||
- Перед MR: `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test` — всё зелёное.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Используй при написании, ревью и про
|
||||
# Стандарты Swift
|
||||
|
||||
Один из наших языков — **Swift** (iOS/macOS). Ниже — общепринятые дефолты экосистемы;
|
||||
специфичное для компании помечено `TODO` — уточняй, не выдумывай.
|
||||
значения под M.Video заполнены дефолтами индустрии — тюним позже.
|
||||
|
||||
## Структура
|
||||
|
||||
@@ -18,28 +18,31 @@ Sources/<Target>/...
|
||||
Tests/<Target>Tests/...
|
||||
```
|
||||
|
||||
<!-- TODO: заполнить реальной эталонной структурой (SwiftPM vs Xcodeproj, менеджер зависимостей: SPM/CocoaPods) -->
|
||||
> Ориентир: SwiftPM-пакет (`Package.swift`), `Sources/` + `Tests/`. Эталон уточним.
|
||||
|
||||
## Линтеры и форматирование
|
||||
|
||||
- Форматирование: **swiftformat** — `swiftformat --lint .` (автоформат `swiftformat .`).
|
||||
- Линтер: **swiftlint** — `swiftlint` (конфиг `.swiftlint.yml`), в CI `swiftlint --strict`.
|
||||
<!-- TODO: корпоративные конфиги swiftformat/swiftlint (общий пресет), если есть -->
|
||||
Дефолт: SwiftLint + SwiftFormat (дефолтные конфиги). Корпоративный пресет уточним.
|
||||
|
||||
## Тесты
|
||||
|
||||
- SwiftPM: `swift test`.
|
||||
- Xcode-проект: `xcodebuild test -scheme <Scheme> -destination 'platform=iOS Simulator,name=iPhone 15'`.
|
||||
- Требования к покрытию / обязательные виды тестов: `<!-- TODO -->`.
|
||||
- Покрытие: `swift test --enable-code-coverage`, мягкий ориентир. (пороги уточним)
|
||||
|
||||
## Сборка и зависимости
|
||||
|
||||
- Сборка: `swift build` (или `xcodebuild build -scheme <Scheme>`).
|
||||
- Зависимости: `<!-- TODO: SwiftPM / CocoaPods -->` — lock (`Package.resolved`/`Podfile.lock`) коммитим.
|
||||
- Зависимости: **SwiftPM** (`Package.resolved` коммитим); CocoaPods — только для легаси.
|
||||
|
||||
## Коммиты и MR
|
||||
|
||||
- **Формат коммитов:** `<!-- TODO: например Conventional Commits или свой -->`.
|
||||
- **Формат коммитов:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` …).
|
||||
- **Ветки и деплой:** фича-ветки — от `main` (в имя ветки желательно включать ключ тикета).
|
||||
Деплой управляется ветками/тегами: ветка `release-*` → раскатка на **stage**; тег `v*` → раскатка в **прод**.
|
||||
Ветки `release-*` и теги `v*` без необходимости не пушь — `permission-gate` переспросит.
|
||||
- **MR обязателен** (см. корпоративные правила из контекста сессии). Прямой push в защищённые
|
||||
ветки запрещён — расширение `permission-gate` дополнительно переспросит.
|
||||
- Перед MR: `swiftformat --lint .`, `swiftlint --strict`, тесты — всё зелёное.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Используй при написании, ревью и про
|
||||
# Стандарты TypeScript
|
||||
|
||||
Один из наших языков — **TypeScript** (фронтенд и Node-сервисы). Ниже — общепринятые дефолты
|
||||
экосистемы; специфичное для компании помечено `TODO` — уточняй, не выдумывай.
|
||||
экосистемы; значения под M.Video заполнены дефолтами индустрии — тюним позже.
|
||||
|
||||
## Структура
|
||||
|
||||
@@ -21,28 +21,31 @@ src/
|
||||
test/ | *.test.ts # рядом с кодом или в test/
|
||||
```
|
||||
|
||||
<!-- TODO: заполнить реальной эталонной структурой / шаблоном проекта (менеджер пакетов: npm/pnpm/yarn?) -->
|
||||
> Ориентир: TS-проект/монорепо на pnpm workspaces, strict `tsconfig`. Эталон уточним.
|
||||
|
||||
## Линтеры и форматирование
|
||||
|
||||
- Типы: `tsc --noEmit` — без ошибок типов.
|
||||
- Линтер: **ESLint** — `eslint .` (конфиг `.eslintrc*` / `eslint.config.*`).
|
||||
- Форматирование: **Prettier** — `prettier --check .` (в CI), `prettier --write .` локально.
|
||||
<!-- TODO: корпоративный конфиг eslint/prettier (общий пресет?), если есть -->
|
||||
Дефолт: typescript-eslint (recommended) + Prettier (дефолтный конфиг). Корпоративный пресет уточним.
|
||||
|
||||
## Тесты
|
||||
|
||||
- Прогон: `<!-- TODO: vitest / jest / node --test -->` — напр. `npm test`.
|
||||
- Требования к покрытию: `<!-- TODO -->`.
|
||||
- Прогон: **vitest** — `pnpm test` (покрытие `pnpm test --coverage`).
|
||||
- Покрытие: мягкий ориентир ~80% для бизнес-логики. (пороги уточним)
|
||||
|
||||
## Сборка и зависимости
|
||||
|
||||
- Сборка: `npm run build` (или скрипт проекта).
|
||||
- Пакетный менеджер и lock-файл: `<!-- TODO: npm/pnpm/yarn -->` — lock коммитим.
|
||||
- Сборка: `pnpm build` (или скрипт проекта).
|
||||
- Пакетный менеджер: **pnpm** — `pnpm install`; lock `pnpm-lock.yaml` коммитим.
|
||||
|
||||
## Коммиты и MR
|
||||
|
||||
- **Формат коммитов:** `<!-- TODO: например Conventional Commits или свой -->`.
|
||||
- **Формат коммитов:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` …).
|
||||
- **Ветки и деплой:** фича-ветки — от `main` (в имя ветки желательно включать ключ тикета).
|
||||
Деплой управляется ветками/тегами: ветка `release-*` → раскатка на **stage**; тег `v*` → раскатка в **прод**.
|
||||
Ветки `release-*` и теги `v*` без необходимости не пушь — `permission-gate` переспросит.
|
||||
- **MR обязателен** (см. корпоративные правила из контекста сессии). Прямой push в защищённые
|
||||
ветки запрещён — расширение `permission-gate` дополнительно переспросит.
|
||||
- Перед MR: `tsc --noEmit`, `eslint .`, `prettier --check .`, тесты — всё зелёное.
|
||||
|
||||
80
test/guardrails.test.ts
Normal file
80
test/guardrails.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Behavioral tests for the guardrail extensions. No test framework — run with:
|
||||
* node --experimental-strip-types test/guardrails.test.ts
|
||||
* (or `npm test`). Exits non-zero on any failure.
|
||||
*
|
||||
* Extensions only `import type` the Pi API, so they run here with node's
|
||||
* type-stripping and mocked pi/ctx objects.
|
||||
*/
|
||||
|
||||
process.env.PI_KIT_AUDIT_OFF = "1"; // don't touch the real audit log during tests
|
||||
|
||||
import protectedPaths from "../extensions/protected-paths.ts";
|
||||
import permissionGate from "../extensions/permission-gate.ts";
|
||||
import secretScanner from "../extensions/secret-scanner.ts";
|
||||
import commitGuard from "../extensions/commit-guard.ts";
|
||||
import llmRedaction from "../extensions/llm-redaction.ts";
|
||||
import { redact, scanSecrets } from "../extensions/lib/secrets.ts";
|
||||
|
||||
let fails = 0;
|
||||
function check(name: string, cond: boolean) {
|
||||
console.log(`${cond ? "PASS" : "FAIL"} ${name}`);
|
||||
if (!cond) fails++;
|
||||
}
|
||||
|
||||
/** Capture the handler an extension registers for a given event. */
|
||||
function grab(ext: (pi: any) => void, evName: string): any {
|
||||
let handler: any;
|
||||
ext({ on: (ev: string, fn: any) => { if (ev === evName) handler = fn; }, registerCommand() {} });
|
||||
return handler;
|
||||
}
|
||||
|
||||
const noUI: any = { hasUI: false };
|
||||
const AKIA = "AKIA" + "ABCDEFGHIJKLMNOP";
|
||||
const GLPAT = "glpat-" + "aBcDeF1234567890ghXY12"; // 22 chars after prefix (>=20)
|
||||
const PRIVKEY = "-----BEGIN RSA PRIVATE KEY-----";
|
||||
const JWT =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
|
||||
|
||||
async function run() {
|
||||
// --- protected-paths ---
|
||||
const pp = grab(protectedPaths, "tool_call");
|
||||
check("protected: block .env", !!(await pp({ toolName: "write", input: { path: ".env" } }, noUI))?.block);
|
||||
check("protected: allow src/app.go", !(await pp({ toolName: "write", input: { path: "src/app.go" } }, noUI))?.block);
|
||||
|
||||
// --- permission-gate ---
|
||||
const pg = grab(permissionGate, "tool_call");
|
||||
check("perm: block push v* tag", !!(await pg({ toolName: "bash", input: { command: "git push origin v1.2.3" } }, noUI))?.block);
|
||||
check("perm: allow ls", !(await pg({ toolName: "bash", input: { command: "ls -la" } }, noUI))?.block);
|
||||
|
||||
// --- secret-scanner ---
|
||||
const ss = grab(secretScanner, "tool_call");
|
||||
check("secret: block write AKIA", !!(await ss({ toolName: "write", input: { path: "cfg.txt", content: `key=${AKIA}` } }, noUI))?.block);
|
||||
check("secret: block edit private key", !!(await ss({ toolName: "edit", input: { path: "id_rsa", new_string: PRIVKEY } }, noUI))?.block);
|
||||
check("secret: allow clean content", !(await ss({ toolName: "write", input: { path: "a.go", content: "package main" } }, noUI))?.block);
|
||||
check("secret: ignore non-write tool", (await ss({ toolName: "bash", input: { command: "echo hi" } }, noUI)) === undefined);
|
||||
|
||||
// --- commit-guard ---
|
||||
const cg = grab(commitGuard, "tool_call");
|
||||
check("commit: block non-conventional", !!(await cg({ toolName: "bash", input: { command: 'git commit -m "fixed stuff"' } }, noUI))?.block);
|
||||
check("commit: allow conventional", !(await cg({ toolName: "bash", input: { command: 'git commit -m "feat(auth): add login"' } }, noUI))?.block);
|
||||
check("commit: block secret in message", !!(await cg({ toolName: "bash", input: { command: `git commit -m "chore: token ${GLPAT}"` } }, noUI))?.block);
|
||||
check("commit: ignore editor commit", (await cg({ toolName: "bash", input: { command: "git commit" } }, noUI)) === undefined);
|
||||
|
||||
// --- llm-redaction ---
|
||||
const lr = grab(llmRedaction, "before_provider_request");
|
||||
const payload = { model: "x", messages: [{ role: "user", content: `here is my token ${GLPAT}` }] };
|
||||
const out: any = await lr({ payload });
|
||||
const redacted = JSON.stringify(out?.messages ?? []);
|
||||
check("redact: token removed from payload", !redacted.includes(GLPAT) && redacted.includes("REDACTED"));
|
||||
check("redact: no-op when clean", (await lr({ payload: { messages: [{ role: "user", content: "hello" }] } })) === undefined);
|
||||
|
||||
// --- lib units ---
|
||||
check("scanSecrets finds jwt", scanSecrets(JWT).length > 0);
|
||||
check("redact keeps clean text", redact("just normal text") === "just normal text");
|
||||
|
||||
console.log(fails === 0 ? "\nALL PASS" : `\n${fails} FAILED`);
|
||||
process.exit(fails === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user