mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-30 05:55:53 +03:00
refactor(hooks): предметная диагностика гарда по причине + README для читателя
- support-guard: вместо общего списка всех вариантов — текст под конкретную причину отказа (decideSupport.code: capability-off | locked | not-removed), с подставленным реальным путём и точными командами support-edit. Понятно модели вне контекста: что за состояние и что именно сделать. - support-state: decideSupport возвращает code (дискриминатор причины). - README: переписан для читателя — убраны отсылки к внутренней реализации (§-нумерация, декодер, разбор common/); назначение, установка, настройка (.v8-project.json), что делать при отказе, проверка. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ebd620d262
commit
378b19b59f
+47
-42
@@ -1,52 +1,35 @@
|
|||||||
# Hooks: guardrail поддержки + суфлёр навыков
|
# Хуки: защита конфигураций на поддержке + подсказка навыков
|
||||||
|
|
||||||
Харнес-хуки Claude Code, **усиливающие** защиту типовых конфигураций 1С на поддержке. Это **бонус-слой
|
Два хука Claude Code, которые помогают безопасно дорабатывать типовые конфигурации 1С:
|
||||||
поверх пола безопасности** (гард §1B внутри навыков-мутаторов + видимость состояния в info-навыках), который
|
|
||||||
едет всеми каналами установки. Хуки ловят то, что навыки не видят — правки **мимо навыков**.
|
|
||||||
|
|
||||||
> Хуки — фича только Claude Code. На других платформах (Cursor/Codex/…) их нет; там работает переносимый
|
- **Защита от правки «на замке».** Если модель пытается напрямую (инструментами `Edit`/`Write`)
|
||||||
> пол §1B. Поэтому хуки — усиление, а не замена.
|
изменить объект типовой конфигурации, который стоит на поддержке поставщика, правка **блокируется** —
|
||||||
|
иначе она молча сломает будущие обновления вендора. В отказе сразу даётся, что делать дальше под
|
||||||
|
конкретный случай (доработать в расширении или явно разрешить правку).
|
||||||
|
- **Подсказка навыков.** Когда модель работает с исходниками 1С «вручную» (читает сырой XML, ищет по
|
||||||
|
метаданным), хук ненавязчиво напоминает, что для этой задачи есть профильный навык (`meta-info`,
|
||||||
|
`form-edit`, `mxl-*`, `skd-*` и т.п.). Не блокирует, подсказывает не чаще одного раза за сессию на группу.
|
||||||
|
|
||||||
## Что внутри
|
Это дополнительный слой поверх проверок, которые уже встроены в сами навыки: навыки-мутаторы и так не дадут
|
||||||
|
испортить объект на поддержке. Хуки добавляют защиту для случаев, когда правят файлы **в обход навыков**.
|
||||||
|
|
||||||
| Файл | Событие | Назначение |
|
> Хуки — возможность только Claude Code. На других платформах их нет; там работают встроенные в навыки проверки.
|
||||||
|------|---------|------------|
|
|
||||||
| `support-guard.mjs` | **PreToolUse** `Edit\|Write\|MultiEdit` | §1A: блокирует сырую правку объекта поставщика «на замке» / read-only конфигурации мимо навыков. |
|
|
||||||
| `skill-suggester.mjs` | **PostToolUse** `Read\|Grep\|Glob\|Edit\|Write\|MultiEdit` | Ненавязчиво подсказывает профильный навык 1С, когда модель работает с исходниками напрямую. Не блокирует. |
|
|
||||||
| `common/support-state.mjs` | — | Декодер `Ext/ParentConfigurations.bin` + правило `G`/`f1` (канон; зеркало гарда §1B). |
|
|
||||||
| `common/project.mjs` | — | Чтение реакции из `.v8-project.json`. |
|
|
||||||
| `common/object-class.mjs` | — | Карта путь→навык (с различением cf/cfe и mxl/скд). |
|
|
||||||
| `test/run.mjs` | — | Standalone-тесты на корпусе `cfsrc` + синтетике. |
|
|
||||||
|
|
||||||
Рантайм — **Node.js 18+** (как и для `/web-test`). Скрипты рантайм-независимы (не зависят от PS/Python-порта навыков).
|
## Требования
|
||||||
|
|
||||||
## Поведение
|
**Node.js 18+** (тот же, что нужен для `/web-test`). Команда `node` должна быть доступна в PATH.
|
||||||
|
|
||||||
**Гард (§1A).** Срабатывает по наличию `Ext/ParentConfigurations.bin` (walk-up от пути правки). Реакция —
|
|
||||||
из `.v8-project.json`, поле `editingAllowedCheck`:
|
|
||||||
- `deny` (**по умолчанию**) — блокирует (`permissionDecision: deny`) с диагностикой (безопасные пути: `cfe-*`,
|
|
||||||
`support-edit`, осознанное снятие с поддержки);
|
|
||||||
- `warn` — пропускает с предупреждением;
|
|
||||||
- `off` — выключено.
|
|
||||||
|
|
||||||
Раскладка: глобальный дефолт + переопределение на запись базы (`databases[].editingAllowedCheck`). Читается
|
|
||||||
**идентично** гарду §1B внутри навыков.
|
|
||||||
|
|
||||||
**Суфлёр.** Поле `skillSuggester` (`on` по умолчанию | `off`). Подсказывает не чаще **1×/сессия/группа навыков**,
|
|
||||||
мягкой формулировкой, через `additionalContext` (видит модель). Молчит на коде модулей (`*.bsl`), нераспознанных
|
|
||||||
путях и при `off`.
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
### Плагин (рекомендуется) — автоматически
|
### Через плагин — автоматически
|
||||||
`.claude-plugin/plugin.json` декларирует `"hooks": "./hooks/hooks.json"`. При включении плагина хуки
|
Если навыки установлены как плагин (`/plugin install 1c-skills@cc-1c-skills`), хуки подключаются сами —
|
||||||
подключаются сами, пути резолвятся через `${CLAUDE_PLUGIN_ROOT}`. Ничего настраивать не нужно.
|
настраивать ничего не нужно.
|
||||||
|
|
||||||
|
### Вручную (при установке копированием папки навыков)
|
||||||
|
Копирование `.claude/skills/` хуки не переносит. Чтобы включить их:
|
||||||
|
|
||||||
### Копия папки / `switch.py` — вручную (опт-ин)
|
|
||||||
Эти каналы не несут хуки автоматически (копируется только `.claude/skills/`, а `settings.json` не переносится).
|
|
||||||
Чтобы включить:
|
|
||||||
1. Скопируйте каталог `hooks/` в проект, например в `<проект>/.claude/hooks/`.
|
1. Скопируйте каталог `hooks/` в проект, например в `<проект>/.claude/hooks/`.
|
||||||
2. Добавьте в `<проект>/.claude/settings.json` (проектный, **не** `settings.local.json`):
|
2. Добавьте в `<проект>/.claude/settings.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -65,12 +48,34 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Тесты
|
## Настройка (`.v8-project.json`)
|
||||||
|
|
||||||
|
Поведение настраивается в файле проекта `.v8-project.json` — глобально и/или по конкретной базе
|
||||||
|
(`databases[].…`, переопределяет глобальное):
|
||||||
|
|
||||||
|
| Поле | Значения | По умолчанию | Что делает |
|
||||||
|
|------|----------|--------------|------------|
|
||||||
|
| `editingAllowedCheck` | `deny` / `warn` / `off` | `deny` | Реакция защиты: блокировать правку объекта на замке / только предупреждать / выключить проверку. |
|
||||||
|
| `skillSuggester` | `on` / `off` | `on` | Включает/выключает подсказки навыков. |
|
||||||
|
|
||||||
|
Источник истины по состоянию поддержки — сама выгрузка конфигурации; `.v8-project.json` лишь настраивает
|
||||||
|
реакцию.
|
||||||
|
|
||||||
|
## Что делать при отказе защиты
|
||||||
|
|
||||||
|
Текст отказа сам подсказывает варианты под конкретную ситуацию. Кратко:
|
||||||
|
|
||||||
|
- **Безопаснее всего** — вести доработку в расширении (навыки `cfe-borrow` / `cfe-patch-method`):
|
||||||
|
состояние поддержки менять не нужно, обновления вендора сохраняются.
|
||||||
|
- **Либо** осознанно разрешить правку через навык `support-edit` (включить редактирование объекта,
|
||||||
|
снять его с поддержки или включить возможность изменения всей конфигурации). Готовую команду под ваш
|
||||||
|
случай печатает сам отказ.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node hooks/test/run.mjs
|
node hooks/test/run.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
Прогоняет декодер/гард/суфлёр на корпусе `cfsrc` (bp `G=1` → deny, erp `K=0` → allow) и на синтетических
|
Прогоняет защиту и подсказку на реальных выгрузках и на временных тестовых данных (в `test-tmp/`, не
|
||||||
фикстурах (`test-tmp/`, gitignored; корпус не трогается). Все ветки: per-object `f1=0/1/2`, warn/off,
|
попадает в git; рабочие выгрузки не затрагиваются).
|
||||||
throttle, слепые пятна, cf/cfe, mxl/скд.
|
|
||||||
|
|||||||
@@ -64,9 +64,11 @@ export function findConfigRoot(startPath) {
|
|||||||
|
|
||||||
// Decode the bin header + per-object rules and apply the support rule for `require`
|
// Decode the bin header + per-object rules and apply the support rule for `require`
|
||||||
// ('editable' — blocked if locked f1=0; 'removed' — blocked unless f1=2).
|
// ('editable' — blocked if locked f1=0; 'removed' — blocked unless f1=2).
|
||||||
// Returns { blocked, reason, cfgDir, targetPath }. Never throws.
|
// Returns { blocked, reason, code, cfgDir, targetPath }. `code` discriminates the cause
|
||||||
|
// ('capability-off' | 'locked' | 'not-removed') so callers can tailor the remedy.
|
||||||
|
// Never throws.
|
||||||
export function decideSupport(targetPath, require = 'editable') {
|
export function decideSupport(targetPath, require = 'editable') {
|
||||||
const result = { blocked: false, reason: '', cfgDir: null, targetPath };
|
const result = { blocked: false, reason: '', code: null, cfgDir: null, targetPath };
|
||||||
try {
|
try {
|
||||||
let elemUuid = rootUuid(targetPath);
|
let elemUuid = rootUuid(targetPath);
|
||||||
// Walk up: collect elemUuid (from <dir>.xml of a sub-element) and the config root.
|
// Walk up: collect elemUuid (from <dir>.xml of a sub-element) and the config root.
|
||||||
@@ -119,15 +121,18 @@ export function decideSupport(targetPath, require = 'editable') {
|
|||||||
|
|
||||||
if (G === 1) {
|
if (G === 1) {
|
||||||
result.blocked = true;
|
result.blocked = true;
|
||||||
|
result.code = 'capability-off';
|
||||||
result.reason = 'возможность изменения конфигурации выключена (вся конфигурация read-only)';
|
result.reason = 'возможность изменения конфигурации выключена (вся конфигурация read-only)';
|
||||||
} else if (require === 'removed') {
|
} else if (require === 'removed') {
|
||||||
if (best !== null && best !== 2) {
|
if (best !== null && best !== 2) {
|
||||||
result.blocked = true;
|
result.blocked = true;
|
||||||
|
result.code = 'not-removed';
|
||||||
result.reason = 'объект на поддержке (не снят с поддержки) — удаление сломает обновления';
|
result.reason = 'объект на поддержке (не снят с поддержки) — удаление сломает обновления';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (best !== null && best === 0) {
|
if (best !== null && best === 0) {
|
||||||
result.blocked = true;
|
result.blocked = true;
|
||||||
|
result.code = 'locked';
|
||||||
result.reason = 'объект на замке (поддержка поставщика) — прямая правка сломает обновления';
|
result.reason = 'объект на замке (поддержка поставщика) — прямая правка сломает обновления';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-9
@@ -25,14 +25,51 @@ function candidatePaths(toolInput) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function diagnostic(reason, target) {
|
// Tailored, model-actionable diagnostic per block cause (see decideSupport `code`).
|
||||||
return (
|
// Fills in the real target/config paths so the suggested commands are ready to run.
|
||||||
`[support-guard] Операция запрещена: ${reason}.\n` +
|
function diagnostic(code, target, cfgDir) {
|
||||||
` Цель: ${target}\n` +
|
const head =
|
||||||
` Безопасные пути: доработка через расширение (cfe-*); либо support-edit -Path <цель> -Set editable ` +
|
'[support-guard] Правка отклонена: это объект типовой конфигурации на поддержке поставщика, ' +
|
||||||
`(включить объект) / -Path <дамп> -Capability on (вся конфа read-only) / -Set off-support (снять с поддержки).\n` +
|
'прямая правка молча сломает будущие обновления.';
|
||||||
` Отключить проверку: editingAllowedCheck = warn|off в .v8-project.json.`
|
const cfe =
|
||||||
);
|
'Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — ' +
|
||||||
|
'состояние поддержки менять не нужно, обновления вендора сохраняются.';
|
||||||
|
const offNote = 'Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json.';
|
||||||
|
const root = cfgDir || '<каталог дампа>';
|
||||||
|
|
||||||
|
if (code === 'capability-off') {
|
||||||
|
return [
|
||||||
|
head,
|
||||||
|
`Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — ` +
|
||||||
|
`поэтому объект «${target}» править нельзя.`,
|
||||||
|
cfe,
|
||||||
|
`Либо снять защиту явно (навык support-edit, два шага):`,
|
||||||
|
` 1. support-edit -Path "${root}" -Capability on — включить возможность изменения (объекты пока остаются на замке);`,
|
||||||
|
` 2. support-edit -Path "${target}" -Set editable — открыть этот объект для правки.`,
|
||||||
|
`Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора.`,
|
||||||
|
offNote,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
if (code === 'not-removed') {
|
||||||
|
return [
|
||||||
|
head,
|
||||||
|
`Состояние: объект «${target}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора.`,
|
||||||
|
cfe,
|
||||||
|
`Либо сначала снять объект с поддержки, затем удалять:`,
|
||||||
|
` support-edit -Path "${target}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.`,
|
||||||
|
offNote,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
// locked (G=0, f1=0)
|
||||||
|
return [
|
||||||
|
head,
|
||||||
|
`Состояние: объект «${target}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется).`,
|
||||||
|
cfe,
|
||||||
|
`Либо разрешить правку этого объекта (навык support-edit, выбрать одно):`,
|
||||||
|
` • support-edit -Path "${target}" -Set editable — править и дальше получать обновления вендора (при обновлении возможны конфликты слияния);`,
|
||||||
|
` • support-edit -Path "${target}" -Set off-support — снять с поддержки: правки свободны, обновления по объекту больше не приходят.`,
|
||||||
|
offNote,
|
||||||
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core decision. Returns { stdout, stderr, exitCode }. Pure (no I/O) for testability.
|
// Core decision. Returns { stdout, stderr, exitCode }. Pure (no I/O) for testability.
|
||||||
@@ -55,7 +92,7 @@ export function processInput(input) {
|
|||||||
hookSpecificOutput: {
|
hookSpecificOutput: {
|
||||||
hookEventName: 'PreToolUse',
|
hookEventName: 'PreToolUse',
|
||||||
permissionDecision: 'deny',
|
permissionDecision: 'deny',
|
||||||
permissionDecisionReason: diagnostic(r.reason, target),
|
permissionDecisionReason: diagnostic(r.code, target, r.cfgDir),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { stdout: JSON.stringify(decision), stderr: '', exitCode: 0 };
|
return { stdout: JSON.stringify(decision), stderr: '', exitCode: 0 };
|
||||||
|
|||||||
+6
-2
@@ -109,16 +109,20 @@ console.log('=== support-guard: §1A PreToolUse ===');
|
|||||||
const SYNTH = join(REPO, 'test-tmp', 'hooks-synth');
|
const SYNTH = join(REPO, 'test-tmp', 'hooks-synth');
|
||||||
const edit = (fp, cwd = REPO) => guard({ tool_name: 'Edit', cwd, tool_input: { file_path: fp } });
|
const edit = (fp, cwd = REPO) => guard({ tool_name: 'Edit', cwd, tool_input: { file_path: fp } });
|
||||||
|
|
||||||
// deny (default): G=1 corpus + synth locked.
|
// deny (default): G=1 corpus → capability-off remedy; synth locked → editable remedy.
|
||||||
if (existsSync(join(ACC, 'Configuration.xml'))) {
|
if (existsSync(join(ACC, 'Configuration.xml'))) {
|
||||||
const r = edit(join(ACC, 'Configuration.xml'));
|
const r = edit(join(ACC, 'Configuration.xml'));
|
||||||
let d = null; try { d = JSON.parse(r.stdout); } catch { /* */ }
|
let d = null; try { d = JSON.parse(r.stdout); } catch { /* */ }
|
||||||
|
const reason = d?.hookSpecificOutput?.permissionDecisionReason || '';
|
||||||
check('guard acc (G=1) → deny JSON', d?.hookSpecificOutput?.permissionDecision === 'deny', r.stdout);
|
check('guard acc (G=1) → deny JSON', d?.hookSpecificOutput?.permissionDecision === 'deny', r.stdout);
|
||||||
|
check('guard G=1 reason → -Capability on remedy', /-Capability on/.test(reason) && /возможность изменения/.test(reason), reason);
|
||||||
}
|
}
|
||||||
const rLocked = edit(join(SYNTH, 'Catalogs', 'Locked.xml'));
|
const rLocked = edit(join(SYNTH, 'Catalogs', 'Locked.xml'));
|
||||||
let dLocked = null; try { dLocked = JSON.parse(rLocked.stdout); } catch { /* */ }
|
let dLocked = null; try { dLocked = JSON.parse(rLocked.stdout); } catch { /* */ }
|
||||||
|
const reasonLocked = dLocked?.hookSpecificOutput?.permissionDecisionReason || '';
|
||||||
check('guard synth locked → deny JSON', dLocked?.hookSpecificOutput?.permissionDecision === 'deny', rLocked.stdout);
|
check('guard synth locked → deny JSON', dLocked?.hookSpecificOutput?.permissionDecision === 'deny', rLocked.stdout);
|
||||||
check('guard deny reason has safe paths', /cfe-\*/.test(dLocked?.hookSpecificOutput?.permissionDecisionReason || ''));
|
check('guard locked reason → -Set editable with real path', /-Set editable/.test(reasonLocked) && reasonLocked.includes('Locked.xml'), reasonLocked);
|
||||||
|
check('guard reason offers cfe + support-edit', /cfe-borrow/.test(reasonLocked) && /support-edit/.test(reasonLocked), reasonLocked);
|
||||||
|
|
||||||
// allow: erp + synth editable + non-config file → empty stdout, exit 0.
|
// allow: erp + synth editable + non-config file → empty stdout, exit 0.
|
||||||
const rEdit = edit(join(SYNTH, 'Catalogs', 'Editable.xml'));
|
const rEdit = edit(join(SYNTH, 'Catalogs', 'Editable.xml'));
|
||||||
|
|||||||
Reference in New Issue
Block a user