Skip to content

Commit 598056f

Browse files
committed
Add agent customization: agents, hooks, skills, and instructions
- Add security-reviewer agent definition - Add run-validation-after-edits hook (config + script) - Add feature-doc-updates skill; expand bsblan-testing skill - Restructure AGENTS.md and slim copilot-instructions.md - Point CLAUDE.md at AGENTS.md - Update code-review prompt
1 parent 217e2f4 commit 598056f

9 files changed

Lines changed: 375 additions & 287 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
description: "Use when you need a security review, threat modeling, vulnerability triage, secrets exposure checks, auth/session risk analysis, input validation review, dependency risk review, or secure coding feedback."
3+
name: "Security Reviewer"
4+
tools: [read, search]
5+
argument-hint: "Provide scope (files, PR, or feature), threat context, and any known attack surface."
6+
---
7+
You are a specialist security code reviewer. Your job is to find exploitable risks and provide precise, actionable remediation guidance.
8+
9+
## Constraints
10+
- DO NOT refactor for style or performance unless it directly affects security.
11+
- DO NOT propose broad rewrites when a targeted fix is sufficient.
12+
- DO NOT claim an issue without explaining exploitability, impact, and preconditions.
13+
- ONLY report findings that are security-relevant or materially increase security risk.
14+
15+
## Approach
16+
1. Map attack surface first: inputs, trust boundaries, secrets, authn/authz, network calls, file/system access, and third-party dependencies.
17+
2. Prioritize exploitability over code smell; assess realistic attacker paths and blast radius.
18+
3. Verify mitigations already present to avoid false positives.
19+
4. Provide concrete fixes with smallest safe change and validation steps.
20+
21+
## Output Format
22+
1. Findings (ordered by severity: critical, high, medium, low)
23+
- Title
24+
- Severity
25+
- Location (file and line)
26+
- Why this is vulnerable
27+
- Exploit scenario
28+
- Recommended fix
29+
2. Open questions or assumptions
30+
3. Residual risk and security test gaps
31+
32+
If no findings are discovered, explicitly state that and list residual risks or testing gaps.

.github/copilot-instructions.md

Lines changed: 4 additions & 274 deletions
Original file line numberDiff line numberDiff line change
@@ -1,277 +1,7 @@
1-
# GitHub Copilot Instructions for python-bsblan
1+
# Canonical Instructions
22

3-
This repository contains the `python-bsblan` library, an asynchronous Python client for BSB-LAN devices (heating controllers).
3+
The canonical instruction file for this repository is:
44

5-
## Project Overview
5+
- `AGENTS.md`
66

7-
- **Language**: Python 3.12+
8-
- **Type**: Async library using `aiohttp`
9-
- **Purpose**: Communicate with BSB-LAN devices to read/write heating parameters
10-
- **License**: MIT
11-
12-
## Code Quality Standards
13-
14-
### Required Before Committing
15-
16-
Always run these commands after making changes:
17-
18-
```bash
19-
# Run all prek hooks (ruff, ty, pylint)
20-
uv run prek run --all-files
21-
```
22-
23-
### Prek Includes
24-
- **Ruff**: Linting and formatting (88 char line limit)
25-
- **ty**: Static type checking
26-
- **Pylint**: Code analysis
27-
28-
### Coverage Requirements
29-
- Maintain **95%+ total test coverage**
30-
- **Patch coverage must be 100%** - all new/modified code must be fully tested
31-
- GitHub Actions will fail if patch coverage is below 100%
32-
- Run coverage check: `uv run pytest --cov=src/bsblan --cov-report=term-missing`
33-
34-
## Project Structure
35-
36-
```
37-
src/bsblan/
38-
├── __init__.py # Package exports
39-
├── bsblan.py # Main BSBLAN client class
40-
├── constants.py # Parameter IDs and mappings
41-
├── models.py # Dataclass models for API responses
42-
├── utility.py # Helper utilities
43-
├── exceptions.py # Custom exceptions
44-
└── py.typed # PEP-561 marker
45-
46-
tests/
47-
├── conftest.py # Pytest fixtures
48-
├── fixtures/ # JSON test data
49-
└── test_*.py # Test files
50-
```
51-
52-
## Parameter Naming Conventions
53-
54-
### BSB-LAN Parameters
55-
Parameters are identified by numeric IDs and mapped to readable names in `constants.py`.
56-
57-
**Naming Rules:**
58-
- Use `snake_case` for all parameter names
59-
- Group related parameters with common prefixes
60-
- Legionella-related parameters use `legionella_function_*` prefix:
61-
- `legionella_function_setpoint` (ID: 1645)
62-
- `legionella_function_periodicity` (ID: 1641)
63-
- `legionella_function_day` (ID: 1642)
64-
- `legionella_function_time` (ID: 1644)
65-
- `legionella_function_dwelling_time` (ID: 1646)
66-
- DHW (Domestic Hot Water) parameters use `dhw_*` prefix
67-
68-
### Adding New Parameters
69-
70-
1. **Add to `constants.py`**:
71-
```python
72-
BASE_HOT_WATER_PARAMS: Final[dict[str, str]] = {
73-
"1645": "legionella_function_setpoint", # Parameter ID: name
74-
}
75-
```
76-
77-
2. **Add to model in `models.py`**:
78-
```python
79-
class HotWaterConfig(BaseModel):
80-
legionella_function_setpoint: EntityInfo[float] | None = None
81-
```
82-
83-
3. **Update method in `bsblan.py`** if the parameter is settable:
84-
```python
85-
async def set_hot_water(
86-
self,
87-
params: SetHotWaterParam,
88-
) -> None:
89-
```
90-
And add the field to `SetHotWaterParam` in `models.py`:
91-
```python
92-
@dataclass
93-
class SetHotWaterParam:
94-
legionella_function_setpoint: float | None = None
95-
```
96-
97-
4. **Add tests in `tests/test_*.py`**
98-
99-
## Polling Categories
100-
101-
Parameters are organized into polling categories based on how frequently they change:
102-
103-
### Fast Poll (State - every update)
104-
- Current temperatures
105-
- HVAC action/state
106-
- Pump states
107-
108-
### Slow Poll (Config - every 5 minutes)
109-
- Operating modes
110-
- Setpoints
111-
- Legionella function settings
112-
- Time programs
113-
114-
### Static (rarely changes)
115-
- Device identification
116-
- Min/max temperature limits
117-
118-
## Hot Water Parameter Groups
119-
120-
Hot water parameters are split into groups for granular lazy loading:
121-
122-
| Group | Params | Method | Use Case |
123-
|-------|--------|--------|----------|
124-
| essential | 5 | `hot_water_state()` | Frequent polling |
125-
| config | 16 | `hot_water_config()` | Advanced settings |
126-
| schedule | 8 | `hot_water_schedule()` | Time programs |
127-
128-
Defined in `constants.py`:
129-
- `HOT_WATER_ESSENTIAL_PARAMS` - operating_mode, nominal_setpoint, etc.
130-
- `HOT_WATER_CONFIG_PARAMS` - legionella settings, eco mode, etc.
131-
- `HOT_WATER_SCHEDULE_PARAMS` - daily time programs
132-
133-
## Data Models
134-
135-
### Model Pattern
136-
All models use `pydantic` `BaseModel` for validation and serialization:
137-
138-
```python
139-
from pydantic import BaseModel
140-
141-
class HotWaterConfig(BaseModel):
142-
"""Hot water configuration parameters."""
143-
operating_mode: EntityInfo[int] | None = None
144-
nominal_setpoint: EntityInfo[float] | None = None
145-
```
146-
147-
### EntityInfo Structure
148-
Each parameter returns an `EntityInfo[T]` (generic `BaseModel`) with:
149-
- `value`: The actual value (typed via generic `T`)
150-
- `unit`: Unit of measurement
151-
- `desc`: Human-readable description
152-
- `data_type`: Data type information
153-
154-
## Async Patterns
155-
156-
### Client Usage
157-
```python
158-
from bsblan import BSBLAN, BSBLANConfig, SetHotWaterParam
159-
160-
async with BSBLAN(BSBLANConfig(host="192.168.1.100")) as client:
161-
state = await client.state()
162-
await client.set_hot_water(SetHotWaterParam(nominal_setpoint=55.0))
163-
```
164-
165-
### Lazy Loading Architecture
166-
The library uses lazy loading for optimal performance:
167-
- **Initialization**: Only fetches firmware version (fast startup)
168-
- **Section validation**: Deferred until section is first accessed
169-
- **Hot water granular loading**: Each method validates only its param group
170-
- **Race condition prevention**: Per-section/group asyncio locks
171-
172-
```python
173-
# Initialize() is fast - only fetches firmware
174-
await client.initialize() # ~0.02s
175-
176-
# Section validated on first access
177-
await client.state() # Validates heating section on first call
178-
179-
# Hot water methods validate only their param groups:
180-
await client.hot_water_state() # 5 essential params only
181-
await client.hot_water_config() # 16 config params only
182-
await client.hot_water_schedule() # 8 schedule params only
183-
```
184-
185-
### Concurrency & Locking
186-
The library uses asyncio locks to prevent race conditions during lazy loading:
187-
- `_section_locks`: Per-section locks (heating, sensor, etc.)
188-
- `_hot_water_group_locks`: Per-group locks (essential, config, schedule)
189-
190-
Double-checked locking pattern:
191-
1. Fast path: Check if validated (no lock)
192-
2. Acquire lock for specific section/group
193-
3. Double-check after acquiring lock
194-
4. Perform validation inside the lock
195-
196-
This prevents duplicate network requests when concurrent calls access the same section before validation completes.
197-
198-
### Error Handling
199-
- Use `BSBLANError` for general errors
200-
- Use `BSBLANConnectionError` for connection issues
201-
- Always validate only one parameter is set per API call
202-
203-
## Testing Patterns
204-
205-
### Test Structure
206-
```python
207-
@pytest.mark.asyncio
208-
async def test_set_hot_water(mock_bsblan: BSBLAN) -> None:
209-
"""Test setting BSBLAN hot water state."""
210-
await mock_bsblan.set_hot_water(
211-
SetHotWaterParam(nominal_setpoint=60.0)
212-
)
213-
mock_bsblan._request.assert_awaited_with(
214-
base_path="/JS",
215-
data={"Parameter": "1610", "Value": "60.0", "Type": "1"},
216-
)
217-
```
218-
219-
### Fixtures Location
220-
Test fixtures (JSON responses) are in `tests/fixtures/`
221-
222-
## Common Tasks
223-
224-
### Fetching Parameters from a Real Device
225-
226-
Use `examples/fetch_param.py` to query raw parameter data from a real BSB-LAN device before adding new parameters:
227-
228-
```bash
229-
# Set your device connection details
230-
# Leave BSBLAN_HOST unset to use auto-discovery, or set it explicitly:
231-
export BSBLAN_HOST="192.168.1.100"
232-
export BSBLAN_PASSKEY=your_passkey # if needed
233-
234-
# Fetch one or more parameters
235-
cd examples && python fetch_param.py 1645
236-
cd examples && python fetch_param.py 1645 1641 1642 1644 1646
237-
```
238-
239-
The output shows the raw JSON response including value, unit, description, and data type — use this to determine the correct model field type and naming.
240-
241-
### Adding a New Settable Parameter
242-
243-
0. Fetch the parameter from a real device using `examples/fetch_param.py` to inspect the raw response
244-
1. Add parameter ID mapping in `constants.py`
245-
2. Add field to appropriate model in `models.py`
246-
3. Add parameter to method signature in `bsblan.py`
247-
4. Update docstring with parameter description
248-
5. Add state preparation logic in `_prepare_*_state()` method
249-
6. Add tests for the new parameter
250-
7. Run `uv run prek run --all-files`
251-
252-
### Renaming a Parameter
253-
254-
When renaming parameters for consistency:
255-
1. Update `constants.py` - parameter mapping
256-
2. Update `models.py` - dataclass field
257-
3. Update `bsblan.py` - method parameters and state handling
258-
4. Update `tests/` - all test files using the parameter
259-
5. Update `examples/` - any example code
260-
6. Run `uv run prek run --all-files`
261-
262-
## API Versions
263-
264-
The library supports BSB-LAN API versions:
265-
- **v1**: Original API
266-
- **v3**: Extended API with additional parameters
267-
268-
Version-specific parameters are handled in `constants.py` with extension dictionaries.
269-
270-
## Don't Forget
271-
272-
- ✅ Run `uv run prek run --all-files` after every change
273-
- ✅ Maintain 95%+ test coverage
274-
- ✅ Use type hints on all functions
275-
- ✅ Add docstrings to public methods
276-
- ✅ Keep line length under 88 characters
277-
- ✅ Use consistent parameter naming (check existing patterns)
7+
Keep this file as a lightweight compatibility pointer to avoid duplicated guidance.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"hooks": {
3+
"PostToolUse": [
4+
{
5+
"type": "command",
6+
"command": "./.github/hooks/run_validation_after_edits.sh",
7+
"timeout": 1800
8+
}
9+
]
10+
}
11+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
payload="$(cat)"
5+
6+
should_run="$(HOOK_PAYLOAD="$payload" python3 - <<'PY'
7+
import json
8+
import os
9+
import sys
10+
11+
edit_tool_tokens = {
12+
"edit",
13+
"write",
14+
"multi_edit",
15+
"multiedit",
16+
"apply_patch",
17+
"create_file",
18+
"edit_notebook_file",
19+
"create_new_jupyter_notebook",
20+
"mcp_github_create_or_update_file",
21+
"mcp_io_github_git_create_or_update_file",
22+
}
23+
24+
def walk(value):
25+
if isinstance(value, dict):
26+
for key, item in value.items():
27+
yield str(key)
28+
yield from walk(item)
29+
elif isinstance(value, list):
30+
for item in value:
31+
yield from walk(item)
32+
elif isinstance(value, str):
33+
yield value
34+
35+
raw = os.environ.get("HOOK_PAYLOAD", "").strip()
36+
if not raw:
37+
print("skip")
38+
raise SystemExit(0)
39+
40+
try:
41+
data = json.loads(raw)
42+
except json.JSONDecodeError:
43+
print("skip")
44+
raise SystemExit(0)
45+
46+
haystack = "\n".join(s.lower() for s in walk(data))
47+
if any(token in haystack for token in edit_tool_tokens):
48+
print("run")
49+
else:
50+
print("skip")
51+
PY
52+
)"
53+
54+
if [[ "$should_run" != "run" ]]; then
55+
exit 0
56+
fi
57+
58+
echo "[hook] File edit detected. Running validation commands..."
59+
60+
echo "[hook] Running tests"
61+
if ! uv run pytest --no-cov; then
62+
echo "[hook] Tests failed"
63+
exit 2
64+
fi
65+
66+
echo "[hook] Running prek"
67+
if ! uv run prek run --all-files; then
68+
echo "[hook] Prek failed"
69+
exit 2
70+
fi
71+
72+
echo "[hook] Validation passed"

0 commit comments

Comments
 (0)