mcp-baf-audit 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_baf_audit-0.2.0/.gitignore +36 -0
- mcp_baf_audit-0.2.0/LICENSE +21 -0
- mcp_baf_audit-0.2.0/PKG-INFO +160 -0
- mcp_baf_audit-0.2.0/README.md +139 -0
- mcp_baf_audit-0.2.0/pyproject.toml +34 -0
- mcp_baf_audit-0.2.0/src/mcp_baf_audit/__init__.py +54 -0
- mcp_baf_audit-0.2.0/src/mcp_baf_audit/events.py +99 -0
- mcp_baf_audit-0.2.0/src/mcp_baf_audit/reader.py +97 -0
- mcp_baf_audit-0.2.0/src/mcp_baf_audit/redact.py +43 -0
- mcp_baf_audit-0.2.0/src/mcp_baf_audit/trace.py +93 -0
- mcp_baf_audit-0.2.0/src/mcp_baf_audit/writer.py +291 -0
- mcp_baf_audit-0.2.0/tests/test_events.py +76 -0
- mcp_baf_audit-0.2.0/tests/test_reader.py +89 -0
- mcp_baf_audit-0.2.0/tests/test_redact.py +46 -0
- mcp_baf_audit-0.2.0/tests/test_trace.py +157 -0
- mcp_baf_audit-0.2.0/tests/test_writer.py +238 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Virtual environments
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
env/
|
|
10
|
+
|
|
11
|
+
# Packaging / build
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
.eggs/
|
|
16
|
+
|
|
17
|
+
# Test / lint caches
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
coverage.xml
|
|
23
|
+
htmlcov/
|
|
24
|
+
|
|
25
|
+
# Runtime artifacts (audit journals)
|
|
26
|
+
audit.log
|
|
27
|
+
audit-*.log
|
|
28
|
+
*.log
|
|
29
|
+
|
|
30
|
+
# IDE
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
|
|
34
|
+
# OS
|
|
35
|
+
.DS_Store
|
|
36
|
+
Thumbs.db
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andriy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-baf-audit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared JSONL audit log for BAF MCP services (mcp-baf, baf-write-mcp, hermes)
|
|
5
|
+
Project-URL: Repository, https://github.com/andriy4k07/mcp-baf-audit
|
|
6
|
+
Project-URL: Issues, https://github.com/andriy4k07/mcp-baf-audit/issues
|
|
7
|
+
Author: andriy4k07
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: 1c,audit,baf,jsonl,logging,mcp
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: System :: Logging
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# mcp-baf-audit
|
|
23
|
+
|
|
24
|
+
Єдиний JSONL-журнал аудиту для сервісів BAF. Формат запису — спільний
|
|
25
|
+
контракт, щоб логи не розходилися між сервісами.
|
|
26
|
+
|
|
27
|
+
Пакет **повністю незалежний**: без рантайм-залежностей, тільки стандартна
|
|
28
|
+
бібліотека Python (≥3.11). Він нічого не імпортує зі споживачів — навпаки,
|
|
29
|
+
споживачі залежать від нього.
|
|
30
|
+
|
|
31
|
+
## Споживачі
|
|
32
|
+
|
|
33
|
+
- [mcp-baf](https://github.com/andriy4k07/mcp-baf) — MCP-сервер читання 1С
|
|
34
|
+
(інтеграція запланована окремим PR).
|
|
35
|
+
- [baf-write-mcp](https://github.com/andriy4k07/baf-write-mcp) — MCP-сервер
|
|
36
|
+
контрольованого створення номенклатури в 1С (вже переведений на цей пакет).
|
|
37
|
+
- hermes — інтеграція запланована.
|
|
38
|
+
|
|
39
|
+
## Схема запису (v2)
|
|
40
|
+
|
|
41
|
+
Кожен рядок журналу — один JSON-об'єкт зі спільним конвертом:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"schema_version": "2",
|
|
46
|
+
"ts": "2026-06-14T10:51:23.354+00:00",
|
|
47
|
+
"service": "baf-write-mcp",
|
|
48
|
+
"session": "d6dfd09f2d7d",
|
|
49
|
+
"seq": 1043,
|
|
50
|
+
"trace_id": "abc123…",
|
|
51
|
+
"event": "product.create",
|
|
52
|
+
"level": "info",
|
|
53
|
+
"tool": "create_product_catalog_item",
|
|
54
|
+
"request_id": "req-7",
|
|
55
|
+
"object": { "type": "product", "ref": "uuid…", "code": "000001", "name": "Болт" },
|
|
56
|
+
"actor": "write-svc",
|
|
57
|
+
"source_channel": "mcp",
|
|
58
|
+
"ok": true,
|
|
59
|
+
"duration_ms": 87,
|
|
60
|
+
"status": 200,
|
|
61
|
+
"payload": { },
|
|
62
|
+
"error": null
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`service` — ім'я сервіса (`mcp-baf` | `baf-write-mcp` | `hermes`);
|
|
67
|
+
`session` — hex12, один на процес; `seq` — монотонний лічильник у сеансі;
|
|
68
|
+
`trace_id` — наскрізний ідентифікатор операції між сервісами, **ніколи не
|
|
69
|
+
null у нових записах** (вкладені події успадковують його через contextvar).
|
|
70
|
+
|
|
71
|
+
Optional-поля конверта v2 (`request_id`, `object`, `actor`, `source_channel`,
|
|
72
|
+
`ok`, `duration_ms`, `status`) nullable: їхня відсутність у рядку (зокрема в
|
|
73
|
+
логах v1) не ламає читання. Редакція секретів застосовується і до `payload`,
|
|
74
|
+
і до `object`.
|
|
75
|
+
|
|
76
|
+
> Будь-яка зміна формату рядка → bump `SCHEMA_VERSION` у `writer.py`
|
|
77
|
+
> і minor/major тега пакета. v2 додав optional-поля конверта; читач
|
|
78
|
+
> розуміє і v1, і v2.
|
|
79
|
+
|
|
80
|
+
## Канонічні імена подій
|
|
81
|
+
|
|
82
|
+
`events.py` фіксує namespaced-таксономію (`tool.call`, `product.create`,
|
|
83
|
+
`counterparty.propose`, `one_c.http`, `index.search`, …). Writer нормалізує
|
|
84
|
+
ім'я кожного запису через `canonical()`, тож **нові логи завжди канонічні** —
|
|
85
|
+
навіть якщо доменний код передає звичне плоске ім'я. Старі логи лишаються
|
|
86
|
+
читабельними: той самий `canonical()` мапить плоскі імена v1 у канонічні.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from mcp_baf_audit import canonical, events
|
|
90
|
+
|
|
91
|
+
canonical("create") # -> "product.create"
|
|
92
|
+
canonical("tool_call") # -> "tool.call"
|
|
93
|
+
canonical("product.create") # -> "product.create" (ідемпотентно)
|
|
94
|
+
events.PRODUCT_CREATE # "product.create"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Використання
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from mcp_baf_audit import AuditWriter, traced, iter_events
|
|
101
|
+
|
|
102
|
+
audit = AuditWriter(service="mcp-baf", path="/path/to/audit.log")
|
|
103
|
+
audit.server_start(version="1.2.3")
|
|
104
|
+
|
|
105
|
+
# Канонічний метод v2 (payload і object редагуються від секретів):
|
|
106
|
+
audit.event("product.create", tool="create_product_catalog_item",
|
|
107
|
+
request_id="req-7", status=200, duration_ms=12, ok=True,
|
|
108
|
+
obj={"type": "product", "ref": "uuid…", "name": "Болт"},
|
|
109
|
+
payload={"name": "Болт"})
|
|
110
|
+
|
|
111
|
+
# Обгортка інструмента: пише tool.call/tool.error, міряє duration,
|
|
112
|
+
# гарантує не-null trace_id (явний -> contextvar -> новий) і фіксує його
|
|
113
|
+
# в contextvar, щоб вкладені події успадкували.
|
|
114
|
+
result_json = await traced(audit, "query", call, args={"limit": 10})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Читання журналу
|
|
118
|
+
|
|
119
|
+
`iter_events()` — єдина читацька функція. Вона обходить ротовані архіви в
|
|
120
|
+
хронологічному порядку, мовчки пропускає биті рядки (повідомляє про кожен
|
|
121
|
+
через `on_bad_line`) і нормалізує `event` через `canonical()`. На ній
|
|
122
|
+
будуються зовнішні проєкція/дашборд — сама бібліотека ні БД, ні форматів
|
|
123
|
+
представлення не знає.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from mcp_baf_audit import iter_events
|
|
127
|
+
|
|
128
|
+
bad: list[str] = []
|
|
129
|
+
for ev in iter_events("/path/to/audit.log", on_bad_line=bad.append):
|
|
130
|
+
print(ev["seq"], ev["event"], ev.get("trace_id"))
|
|
131
|
+
print("пропущено битих рядків:", len(bad))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### HTTP-події `one_c.http`
|
|
135
|
+
|
|
136
|
+
`baf-write-mcp` передає аудит у HTTP-клієнт (`OneCClient(config, audit=audit)`),
|
|
137
|
+
і кожен виклик 1С лишає рівно одну подію `one_c.http` (метод, ендпоінт,
|
|
138
|
+
статус, `duration_ms`, `response_bytes`, `request_id`, `trace_id`). **Тіло
|
|
139
|
+
запиту/відповіді не логується**; помилки пишуться з `level:"error"`, `ok:false`.
|
|
140
|
+
|
|
141
|
+
### Секрети
|
|
142
|
+
|
|
143
|
+
Редакція централізована у writer'і: ключі `password|token|authorization|secret`
|
|
144
|
+
(регістронезалежно, рекурсивно) замінюються на `***` у кожному записі.
|
|
145
|
+
Передавати «сирий» payload безпечно — секрети у лог не потрапляють.
|
|
146
|
+
|
|
147
|
+
## Постачання / версіонування
|
|
148
|
+
|
|
149
|
+
Тег `v0.2.0` (minor-bump: формат запису перейшов на schema v2). Споживачі
|
|
150
|
+
пінять версію:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
pip install "git+https://github.com/andriy4k07/mcp-baf-audit.git@v0.2.0"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
У монорепо — path-залежність від сусіднього каталогу:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
pip install -e ../mcp-baf-audit
|
|
160
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# mcp-baf-audit
|
|
2
|
+
|
|
3
|
+
Єдиний JSONL-журнал аудиту для сервісів BAF. Формат запису — спільний
|
|
4
|
+
контракт, щоб логи не розходилися між сервісами.
|
|
5
|
+
|
|
6
|
+
Пакет **повністю незалежний**: без рантайм-залежностей, тільки стандартна
|
|
7
|
+
бібліотека Python (≥3.11). Він нічого не імпортує зі споживачів — навпаки,
|
|
8
|
+
споживачі залежать від нього.
|
|
9
|
+
|
|
10
|
+
## Споживачі
|
|
11
|
+
|
|
12
|
+
- [mcp-baf](https://github.com/andriy4k07/mcp-baf) — MCP-сервер читання 1С
|
|
13
|
+
(інтеграція запланована окремим PR).
|
|
14
|
+
- [baf-write-mcp](https://github.com/andriy4k07/baf-write-mcp) — MCP-сервер
|
|
15
|
+
контрольованого створення номенклатури в 1С (вже переведений на цей пакет).
|
|
16
|
+
- hermes — інтеграція запланована.
|
|
17
|
+
|
|
18
|
+
## Схема запису (v2)
|
|
19
|
+
|
|
20
|
+
Кожен рядок журналу — один JSON-об'єкт зі спільним конвертом:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"schema_version": "2",
|
|
25
|
+
"ts": "2026-06-14T10:51:23.354+00:00",
|
|
26
|
+
"service": "baf-write-mcp",
|
|
27
|
+
"session": "d6dfd09f2d7d",
|
|
28
|
+
"seq": 1043,
|
|
29
|
+
"trace_id": "abc123…",
|
|
30
|
+
"event": "product.create",
|
|
31
|
+
"level": "info",
|
|
32
|
+
"tool": "create_product_catalog_item",
|
|
33
|
+
"request_id": "req-7",
|
|
34
|
+
"object": { "type": "product", "ref": "uuid…", "code": "000001", "name": "Болт" },
|
|
35
|
+
"actor": "write-svc",
|
|
36
|
+
"source_channel": "mcp",
|
|
37
|
+
"ok": true,
|
|
38
|
+
"duration_ms": 87,
|
|
39
|
+
"status": 200,
|
|
40
|
+
"payload": { },
|
|
41
|
+
"error": null
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`service` — ім'я сервіса (`mcp-baf` | `baf-write-mcp` | `hermes`);
|
|
46
|
+
`session` — hex12, один на процес; `seq` — монотонний лічильник у сеансі;
|
|
47
|
+
`trace_id` — наскрізний ідентифікатор операції між сервісами, **ніколи не
|
|
48
|
+
null у нових записах** (вкладені події успадковують його через contextvar).
|
|
49
|
+
|
|
50
|
+
Optional-поля конверта v2 (`request_id`, `object`, `actor`, `source_channel`,
|
|
51
|
+
`ok`, `duration_ms`, `status`) nullable: їхня відсутність у рядку (зокрема в
|
|
52
|
+
логах v1) не ламає читання. Редакція секретів застосовується і до `payload`,
|
|
53
|
+
і до `object`.
|
|
54
|
+
|
|
55
|
+
> Будь-яка зміна формату рядка → bump `SCHEMA_VERSION` у `writer.py`
|
|
56
|
+
> і minor/major тега пакета. v2 додав optional-поля конверта; читач
|
|
57
|
+
> розуміє і v1, і v2.
|
|
58
|
+
|
|
59
|
+
## Канонічні імена подій
|
|
60
|
+
|
|
61
|
+
`events.py` фіксує namespaced-таксономію (`tool.call`, `product.create`,
|
|
62
|
+
`counterparty.propose`, `one_c.http`, `index.search`, …). Writer нормалізує
|
|
63
|
+
ім'я кожного запису через `canonical()`, тож **нові логи завжди канонічні** —
|
|
64
|
+
навіть якщо доменний код передає звичне плоске ім'я. Старі логи лишаються
|
|
65
|
+
читабельними: той самий `canonical()` мапить плоскі імена v1 у канонічні.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from mcp_baf_audit import canonical, events
|
|
69
|
+
|
|
70
|
+
canonical("create") # -> "product.create"
|
|
71
|
+
canonical("tool_call") # -> "tool.call"
|
|
72
|
+
canonical("product.create") # -> "product.create" (ідемпотентно)
|
|
73
|
+
events.PRODUCT_CREATE # "product.create"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Використання
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from mcp_baf_audit import AuditWriter, traced, iter_events
|
|
80
|
+
|
|
81
|
+
audit = AuditWriter(service="mcp-baf", path="/path/to/audit.log")
|
|
82
|
+
audit.server_start(version="1.2.3")
|
|
83
|
+
|
|
84
|
+
# Канонічний метод v2 (payload і object редагуються від секретів):
|
|
85
|
+
audit.event("product.create", tool="create_product_catalog_item",
|
|
86
|
+
request_id="req-7", status=200, duration_ms=12, ok=True,
|
|
87
|
+
obj={"type": "product", "ref": "uuid…", "name": "Болт"},
|
|
88
|
+
payload={"name": "Болт"})
|
|
89
|
+
|
|
90
|
+
# Обгортка інструмента: пише tool.call/tool.error, міряє duration,
|
|
91
|
+
# гарантує не-null trace_id (явний -> contextvar -> новий) і фіксує його
|
|
92
|
+
# в contextvar, щоб вкладені події успадкували.
|
|
93
|
+
result_json = await traced(audit, "query", call, args={"limit": 10})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Читання журналу
|
|
97
|
+
|
|
98
|
+
`iter_events()` — єдина читацька функція. Вона обходить ротовані архіви в
|
|
99
|
+
хронологічному порядку, мовчки пропускає биті рядки (повідомляє про кожен
|
|
100
|
+
через `on_bad_line`) і нормалізує `event` через `canonical()`. На ній
|
|
101
|
+
будуються зовнішні проєкція/дашборд — сама бібліотека ні БД, ні форматів
|
|
102
|
+
представлення не знає.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from mcp_baf_audit import iter_events
|
|
106
|
+
|
|
107
|
+
bad: list[str] = []
|
|
108
|
+
for ev in iter_events("/path/to/audit.log", on_bad_line=bad.append):
|
|
109
|
+
print(ev["seq"], ev["event"], ev.get("trace_id"))
|
|
110
|
+
print("пропущено битих рядків:", len(bad))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### HTTP-події `one_c.http`
|
|
114
|
+
|
|
115
|
+
`baf-write-mcp` передає аудит у HTTP-клієнт (`OneCClient(config, audit=audit)`),
|
|
116
|
+
і кожен виклик 1С лишає рівно одну подію `one_c.http` (метод, ендпоінт,
|
|
117
|
+
статус, `duration_ms`, `response_bytes`, `request_id`, `trace_id`). **Тіло
|
|
118
|
+
запиту/відповіді не логується**; помилки пишуться з `level:"error"`, `ok:false`.
|
|
119
|
+
|
|
120
|
+
### Секрети
|
|
121
|
+
|
|
122
|
+
Редакція централізована у writer'і: ключі `password|token|authorization|secret`
|
|
123
|
+
(регістронезалежно, рекурсивно) замінюються на `***` у кожному записі.
|
|
124
|
+
Передавати «сирий» payload безпечно — секрети у лог не потрапляють.
|
|
125
|
+
|
|
126
|
+
## Постачання / версіонування
|
|
127
|
+
|
|
128
|
+
Тег `v0.2.0` (minor-bump: формат запису перейшов на schema v2). Споживачі
|
|
129
|
+
пінять версію:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
pip install "git+https://github.com/andriy4k07/mcp-baf-audit.git@v0.2.0"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
У монорепо — path-залежність від сусіднього каталогу:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
pip install -e ../mcp-baf-audit
|
|
139
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-baf-audit"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Shared JSONL audit log for BAF MCP services (mcp-baf, baf-write-mcp, hermes)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "andriy4k07" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
keywords = ["mcp", "audit", "jsonl", "logging", "1c", "baf"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: System :: Logging",
|
|
18
|
+
]
|
|
19
|
+
# Без рантайм-зависимостей: только стандартная библиотека.
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = ["pytest"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/andriy4k07/mcp-baf-audit"
|
|
27
|
+
Issues = "https://github.com/andriy4k07/mcp-baf-audit/issues"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/mcp_baf_audit"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""mcp-baf-audit — единый JSONL-аудит для сервисов BAF (mcp-baf, baf-write-mcp, hermes).
|
|
2
|
+
|
|
3
|
+
Схема записи общая (см. writer.SCHEMA_VERSION). Публичный API намеренно
|
|
4
|
+
повторяет знакомые из baf-write имена, чтобы интеграция была минимальной.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from . import events
|
|
10
|
+
from .events import ALIASES, canonical
|
|
11
|
+
from .reader import iter_events
|
|
12
|
+
from .redact import REDACTED, default_redactor
|
|
13
|
+
from .trace import (
|
|
14
|
+
get_trace_id,
|
|
15
|
+
new_trace_id,
|
|
16
|
+
set_trace_id,
|
|
17
|
+
to_json,
|
|
18
|
+
traced,
|
|
19
|
+
)
|
|
20
|
+
from .writer import (
|
|
21
|
+
DEFAULT_AUDIT_ARCHIVES,
|
|
22
|
+
DEFAULT_AUDIT_MAX_SIZE_MIB,
|
|
23
|
+
DEFAULT_MAX_BYTES,
|
|
24
|
+
SCHEMA_VERSION,
|
|
25
|
+
AuditLog,
|
|
26
|
+
AuditWriter,
|
|
27
|
+
default_cache_dir,
|
|
28
|
+
user_cache_dir,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.2.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AuditWriter",
|
|
35
|
+
"AuditLog",
|
|
36
|
+
"traced",
|
|
37
|
+
"to_json",
|
|
38
|
+
"set_trace_id",
|
|
39
|
+
"get_trace_id",
|
|
40
|
+
"new_trace_id",
|
|
41
|
+
"default_redactor",
|
|
42
|
+
"REDACTED",
|
|
43
|
+
"events",
|
|
44
|
+
"canonical",
|
|
45
|
+
"ALIASES",
|
|
46
|
+
"iter_events",
|
|
47
|
+
"SCHEMA_VERSION",
|
|
48
|
+
"DEFAULT_AUDIT_MAX_SIZE_MIB",
|
|
49
|
+
"DEFAULT_AUDIT_ARCHIVES",
|
|
50
|
+
"DEFAULT_MAX_BYTES",
|
|
51
|
+
"default_cache_dir",
|
|
52
|
+
"user_cache_dir",
|
|
53
|
+
"__version__",
|
|
54
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Каноническая таксономия событий аудита.
|
|
2
|
+
|
|
3
|
+
Единый словарь namespaced-имён событий для всех сервисов BAF. Раньше каждый
|
|
4
|
+
сервис писал плоские имена ("create", "tool_call", "propose_counterparty"),
|
|
5
|
+
из-за чего одно и то же действие выглядело по-разному и плохо группировалось.
|
|
6
|
+
Здесь зафиксированы канонические имена вида ``<домен>.<действие>``.
|
|
7
|
+
|
|
8
|
+
Совместимость двусторонняя:
|
|
9
|
+
|
|
10
|
+
* writer нормализует имя каждой записи через :func:`canonical`, поэтому новые
|
|
11
|
+
логи всегда несут канонические имена — даже если доменный код по-прежнему
|
|
12
|
+
передаёт привычное плоское имя ("create" -> "product.create");
|
|
13
|
+
* старые логи остаются читаемыми: потребитель прогоняет прочитанное имя через
|
|
14
|
+
ту же :func:`canonical` и получает канонический вид, а имена без записи в
|
|
15
|
+
:data:`ALIASES` (или уже канонические) возвращаются как есть.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
# ── Канонические имена событий ──────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
# Вызовы инструментов (обёртка traced()).
|
|
23
|
+
TOOL_CALL = "tool.call"
|
|
24
|
+
TOOL_ERROR = "tool.error"
|
|
25
|
+
|
|
26
|
+
# Жизненный цикл сервера.
|
|
27
|
+
SERVER_START = "server.start"
|
|
28
|
+
SERVER_STOP = "server.stop"
|
|
29
|
+
SERVER_VERSION_MISMATCH = "server.version_mismatch"
|
|
30
|
+
|
|
31
|
+
# Номенклатура (каталожная позиция): propose -> validate -> create -> verify.
|
|
32
|
+
PRODUCT_PROPOSE = "product.propose"
|
|
33
|
+
PRODUCT_VALIDATE = "product.validate"
|
|
34
|
+
PRODUCT_CREATE = "product.create"
|
|
35
|
+
PRODUCT_VERIFY = "product.verify"
|
|
36
|
+
PRODUCT_REFUSED = "product.refused"
|
|
37
|
+
|
|
38
|
+
# Значения списковых свойств номенклатуры.
|
|
39
|
+
PROPERTY_VALUE_PROPOSE = "property_value.propose"
|
|
40
|
+
PROPERTY_VALUE_VALIDATE = "property_value.validate"
|
|
41
|
+
PROPERTY_VALUE_CREATE = "property_value.create"
|
|
42
|
+
|
|
43
|
+
# Контрагенты.
|
|
44
|
+
COUNTERPARTY_PROPOSE = "counterparty.propose"
|
|
45
|
+
COUNTERPARTY_VALIDATE = "counterparty.validate"
|
|
46
|
+
COUNTERPARTY_CREATE = "counterparty.create"
|
|
47
|
+
COUNTERPARTY_VERIFY = "counterparty.verify"
|
|
48
|
+
|
|
49
|
+
# Обращения к HTTP-сервису 1С.
|
|
50
|
+
ONE_C_HTTP = "one_c.http"
|
|
51
|
+
|
|
52
|
+
# Поисковый индекс (mcp-baf): обновление, поиск, кэш.
|
|
53
|
+
INDEX_REFRESH_START = "index.refresh_start"
|
|
54
|
+
INDEX_REFRESH_FINISH = "index.refresh_finish"
|
|
55
|
+
INDEX_REFRESH_ERROR = "index.refresh_error"
|
|
56
|
+
INDEX_SEARCH = "index.search"
|
|
57
|
+
INDEX_CACHE_HIT = "index.cache_hit"
|
|
58
|
+
INDEX_CACHE_MISS = "index.cache_miss"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Алиасы: прежние плоские имена -> канонические ───────────────────
|
|
62
|
+
#
|
|
63
|
+
# Только имена, у которых есть канонический эквивалент в таксономии выше.
|
|
64
|
+
# Прочие плоские имена (*_error, *_incomplete, накладные и т.п.) намеренно
|
|
65
|
+
# не отображаются и проходят через canonical() без изменений.
|
|
66
|
+
ALIASES: dict[str, str] = {
|
|
67
|
+
# Инструменты.
|
|
68
|
+
"tool_call": TOOL_CALL,
|
|
69
|
+
"tool_error": TOOL_ERROR,
|
|
70
|
+
# Сервер.
|
|
71
|
+
"server_start": SERVER_START,
|
|
72
|
+
"server_stop": SERVER_STOP,
|
|
73
|
+
"extension_version_mismatch": SERVER_VERSION_MISMATCH,
|
|
74
|
+
# Номенклатура (плоские имена baf-write).
|
|
75
|
+
"propose": PRODUCT_PROPOSE,
|
|
76
|
+
"validate": PRODUCT_VALIDATE,
|
|
77
|
+
"create": PRODUCT_CREATE,
|
|
78
|
+
"verify": PRODUCT_VERIFY,
|
|
79
|
+
"create_refused": PRODUCT_REFUSED,
|
|
80
|
+
# Значения свойств.
|
|
81
|
+
"propose_property_value": PROPERTY_VALUE_PROPOSE,
|
|
82
|
+
"validate_property_value": PROPERTY_VALUE_VALIDATE,
|
|
83
|
+
"create_property_value": PROPERTY_VALUE_CREATE,
|
|
84
|
+
# Контрагенты.
|
|
85
|
+
"propose_counterparty": COUNTERPARTY_PROPOSE,
|
|
86
|
+
"validate_counterparty": COUNTERPARTY_VALIDATE,
|
|
87
|
+
"create_counterparty": COUNTERPARTY_CREATE,
|
|
88
|
+
"verify_counterparty": COUNTERPARTY_VERIFY,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def canonical(name: str) -> str:
|
|
93
|
+
"""Нормализует имя события в каноническое.
|
|
94
|
+
|
|
95
|
+
Прежнее плоское имя из :data:`ALIASES` отображается в каноническое;
|
|
96
|
+
уже каноническое имя (или имя без алиаса) возвращается без изменений.
|
|
97
|
+
Функция идемпотентна: ``canonical(canonical(x)) == canonical(x)``.
|
|
98
|
+
"""
|
|
99
|
+
return ALIASES.get(name, name)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Чтение журнала аудита — единственная «читающая» функция библиотеки.
|
|
2
|
+
|
|
3
|
+
:func:`iter_events` отдаёт события из активного журнала и (по умолчанию) из
|
|
4
|
+
ротированных архивов в хронологическом порядке. Битые строки молча
|
|
5
|
+
пропускаются — о каждой сообщается через callback ``on_bad_line``, чтобы
|
|
6
|
+
потребитель мог их посчитать. Имя события нормализуется через
|
|
7
|
+
:func:`mcp_baf_audit.events.canonical`, поэтому старые плоские имена (v1) и
|
|
8
|
+
канонические (v2) приходят к читателю единообразно.
|
|
9
|
+
|
|
10
|
+
На этой функции строятся внешние проекции/дашборд: сама библиотека ни БД,
|
|
11
|
+
ни форматов представления не знает.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import glob
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from collections.abc import Callable, Iterator
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .events import canonical
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sort_key(file_path: str) -> str:
|
|
30
|
+
"""Ключ сортировки архивов: имя без расширения .log.
|
|
31
|
+
|
|
32
|
+
Имена архивов: ``<stem>-<YYYYMMDD-HHMMSS>[-<n>].log``. Сортировка по
|
|
33
|
+
строке без ".log" ставит базовый архив метки раньше его collision-версии
|
|
34
|
+
``-<n>`` (та же секунда), потому что более короткая строка-префикс идёт
|
|
35
|
+
первой; точка ".log" не вмешивается в сравнение.
|
|
36
|
+
"""
|
|
37
|
+
return file_path[:-4] if file_path.endswith(".log") else file_path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _files_in_order(path: str | Path, follow_rotation: bool) -> list[str]:
|
|
41
|
+
"""Архивы (старые -> новые) и затем активный журнал последним."""
|
|
42
|
+
p = str(path)
|
|
43
|
+
files: list[str] = []
|
|
44
|
+
if follow_rotation:
|
|
45
|
+
directory = os.path.dirname(p) or "."
|
|
46
|
+
name = os.path.basename(p)
|
|
47
|
+
stem = name[:-4] if name.endswith(".log") else name
|
|
48
|
+
pattern = os.path.join(directory, f"{stem}-*.log")
|
|
49
|
+
files.extend(sorted(glob.glob(pattern), key=_sort_key))
|
|
50
|
+
files.append(p) # активный журнал — самый свежий, читаем последним
|
|
51
|
+
return files
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def iter_events(
|
|
55
|
+
path: str | Path,
|
|
56
|
+
*,
|
|
57
|
+
follow_rotation: bool = True,
|
|
58
|
+
on_bad_line: Callable[[str], None] | None = None,
|
|
59
|
+
) -> Iterator[dict[str, Any]]:
|
|
60
|
+
"""Итерирует события журнала аудита в хронологическом порядке.
|
|
61
|
+
|
|
62
|
+
path — путь к активному журналу (например ``<cache>/audit.log``).
|
|
63
|
+
follow_rotation — включать ротированные архивы ``<stem>-*.log`` перед
|
|
64
|
+
активным журналом (по умолчанию да).
|
|
65
|
+
on_bad_line — вызывается с исходным текстом каждой пропущенной (битой)
|
|
66
|
+
строки; удобно для подсчёта. Битой считается строка, которая не парсится
|
|
67
|
+
как JSON или не является JSON-объектом. Пустые строки пропускаются молча.
|
|
68
|
+
|
|
69
|
+
Каждое отданное событие — dict с нормализованным каноническим ``event``.
|
|
70
|
+
Отсутствующие/недоступные файлы пропускаются без ошибки.
|
|
71
|
+
"""
|
|
72
|
+
for file_path in _files_in_order(path, follow_rotation):
|
|
73
|
+
try:
|
|
74
|
+
handle = open(file_path, encoding="utf-8")
|
|
75
|
+
except OSError:
|
|
76
|
+
continue # файла нет или нет доступа — не наша забота
|
|
77
|
+
with handle as f:
|
|
78
|
+
for line in f:
|
|
79
|
+
line = line.strip()
|
|
80
|
+
if not line:
|
|
81
|
+
continue # пустые строки не считаем битыми
|
|
82
|
+
try:
|
|
83
|
+
record = json.loads(line)
|
|
84
|
+
except ValueError:
|
|
85
|
+
logger.debug("audit reader: пропущена битая строка")
|
|
86
|
+
if on_bad_line is not None:
|
|
87
|
+
on_bad_line(line)
|
|
88
|
+
continue
|
|
89
|
+
if not isinstance(record, dict):
|
|
90
|
+
logger.debug("audit reader: пропущена не-объектная строка")
|
|
91
|
+
if on_bad_line is not None:
|
|
92
|
+
on_bad_line(line)
|
|
93
|
+
continue
|
|
94
|
+
event = record.get("event")
|
|
95
|
+
if isinstance(event, str):
|
|
96
|
+
record["event"] = canonical(event)
|
|
97
|
+
yield record
|