Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/operational-maturity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Operational maturity

# GitHub Actions is public mirror validation only.
# Live DetMir production checks must be run explicitly by an operator with
# private network access and must not enable heavy DLP, Loki or always-on
# Velociraptor.

on:
push:
pull_request:
workflow_dispatch:
inputs:
live:
description: "Run live endpoint contract if AWATCH_OPS_LIVE_URL is configured"
required: false
default: "false"
schedule:
- cron: "41 3 * * 2"

permissions:
contents: read

jobs:
offline-operational-maturity:
name: Offline operational maturity
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: false

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Script syntax
run: node --check scripts/operational-maturity-check.mjs

- name: Operational maturity harness
run: node scripts/operational-maturity-check.mjs --json

live-operational-contract:
name: Live operational contract
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && inputs.live == 'true'
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: false

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Run live contract when URL is available
env:
AWATCH_OPS_LIVE_URL: ${{ secrets.AWATCH_OPS_LIVE_URL }}
run: |
if [[ -z "${AWATCH_OPS_LIVE_URL}" ]]; then
echo "skipped: AWATCH_OPS_LIVE_URL secret is not configured"
exit 0
fi
node scripts/operational-maturity-check.mjs --json --live
143 changes: 143 additions & 0 deletions configs/operational-maturity-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{
"version": "2026-07-01.operational-maturity-v1",
"apiCompatibility": {
"openapiPath": "adk-rust/crates/detmir-portal/src/contracts/openapi.json",
"requiredOpenApiPaths": {
"/contracts": ["get"],
"/contracts/openapi.json": ["get"],
"/health": ["get"],
"/operator": ["get"],
"/reports": ["get"],
"/workforce/kpi/explain": ["get"],
"/ueba": ["get"],
"/risk/narrative": ["get"],
"/actions": ["get"],
"/readiness/latest": ["get"],
"/telemetry": ["post"]
},
"requiredSchemas": [
"ContractIndex",
"JsonObject",
"ActionCenterResponse",
"RiskNarrative",
"UebaPayload",
"WorkforceKpiExplain"
],
"runtimeEndpoints": [
"/healthz",
"/readyz",
"/version",
"/metrics"
],
"runtimeSourcePath": "adk-rust/crates/detmir-portal/src/main.rs"
},
"integrationFixtures": [
{
"path": "/healthz",
"fixture": "docs/fixtures/operational-maturity/portal-healthz.json",
"status": 200,
"contentType": "application/json",
"requiredFields": ["status", "service", "schema_version"],
"equals": { "status": "ok", "schema_version": "pilot-v1" }
},
{
"path": "/readyz",
"fixture": "docs/fixtures/operational-maturity/portal-readyz.json",
"status": 200,
"contentType": "application/json",
"requiredFields": ["status", "checks.portal", "checks.dlp_runtime"],
"equals": { "status": "ready", "checks.dlp_runtime.status": "skipped" }
},
{
"path": "/version",
"fixture": "docs/fixtures/operational-maturity/portal-version.json",
"status": 200,
"contentType": "application/json",
"requiredFields": ["app_version", "schema_version", "environment"],
"equals": { "schema_version": "pilot-v1" }
},
{
"path": "/reports/worktime/management",
"fixture": "docs/fixtures/operational-maturity/worktime-management-ok.json",
"status": 200,
"contentType": "application/json",
"requiredFields": ["status", "stale", "report_cache_hit", "aw_query_duration_ms"],
"equals": { "status": "OK", "stale": false }
},
{
"path": "/reports/worktime/management?fault=aw_timeout",
"fixture": "docs/fixtures/operational-maturity/worktime-management-degraded.json",
"status": 200,
"contentType": "application/json",
"requiredFields": ["status", "reason", "stale", "report_stale_served", "aw_query_timeout_count"],
"equals": { "status": "DEGRADED", "reason": "report_unavailable", "stale": false }
},
{
"path": "/security/finding-inbox",
"fixture": "docs/fixtures/operational-maturity/security-finding-inbox-shadow.json",
"status": 200,
"contentType": "application/json",
"requiredFields": ["mode", "auto_apply", "workflow"],
"equals": { "mode": "shadow", "auto_apply": false }
}
],
"observability": {
"metricsFixture": "docs/fixtures/operational-maturity/portal-metrics.prom",
"requiredMetrics": [
"awatch_http_requests_total",
"awatch_http_request_duration_seconds_sum",
"awatch_http_request_duration_seconds_count",
"awatch_report_requests_total",
"awatch_report_cache_hits_total",
"awatch_report_cache_misses_total",
"awatch_report_cache_stale_hits_total",
"awatch_reports_generated_total",
"awatch_ingestion_records_total",
"awatch_ingestion_rejected_total",
"awatch_role_denied_total",
"awatch_readyz_status"
],
"sourcePath": "adk-rust/crates/detmir-portal/src/production/metrics.rs",
"diagnosticHeaders": ["x-request-id", "x-correlation-id"]
},
"configValidation": {
"jsonFiles": [
"configs/worktime-interpretation-policy.example.json",
"configs/detmir-workforce-policy.example.json",
"adk-rust/crates/detmir-portal/src/contracts/openapi.json",
"docs/fixtures/operational-maturity/portal-healthz.json",
"docs/fixtures/operational-maturity/portal-readyz.json",
"docs/fixtures/operational-maturity/portal-version.json",
"docs/fixtures/operational-maturity/worktime-management-ok.json",
"docs/fixtures/operational-maturity/worktime-management-degraded.json",
"docs/fixtures/operational-maturity/security-finding-inbox-shadow.json"
],
"systemdDirs": [
"ops/systemd",
"clickhouse-1c/ops"
],
"clickhouseInitDir": "clickhouse-1c/clickhouse/init",
"yamlFiles": [
"configs/detmir-ueba-risk-policy.example.yaml",
"clickhouse-1c/etl/config.example.yml",
"clickhouse-1c/grafana/provisioning/datasources/clickhouse.yml",
"clickhouse-1c/grafana/provisioning/dashboards/dashboards.yml"
]
},
"faultInjection": {
"clientTimeoutMs": 350,
"maxFailureClassificationMs": 1200
},
"boundedLoad": {
"requests": 60,
"concurrency": 8,
"p95MaxMs": 750,
"heapGrowthMaxBytes": 20971520,
"paths": [
"/healthz",
"/readyz",
"/version",
"/reports/worktime/management"
]
}
}
58 changes: 58 additions & 0 deletions docs/OPERATIONAL_MATURITY_RU.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Эксплуатационная зрелость DetMir/AWatch-rus

Дата актуализации: 2026-07-01.

Этот контур добавляет автоматическую проверку эксплуатационной зрелости без
нагрузки на production. Public CI запускает только offline checks: fixtures,
локальный mock HTTP, статическую проверку конфигураций, ClickHouse DDL и
контракт наблюдаемости. Live checks запускаются только вручную оператором с
private access.

## Что проверяется

- Integration harness: локальный mock обслуживает ключевые endpoints
`/healthz`, `/readyz`, `/version`, Worktime management и Security Finding
Inbox shadow payload.
- Fault injection: клиент должен быстро классифицировать `503`, timeout и
connection reset, не зависая сверх заданного бюджета.
- API compatibility: DetMir Portal OpenAPI обязан сохранять ключевые paths,
schemas и runtime endpoints.
- Config/migration validation: JSON/YAML examples, systemd units/timers и
ClickHouse init SQL проверяются на базовую пригодность и idempotency.
- Bounded load: короткий локальный load smoke с concurrency и p95 budget,
без sizing claims.
- Observability: обязательные health/version/readiness fields, diagnostic
headers и Prometheus metric names закреплены manifest/fixtures/source check.

## Команды

Offline PR/CI gate:

```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
node --check scripts/operational-maturity-check.mjs
node scripts/operational-maturity-check.mjs --json
```

Live contract, только оператором и только если контур доступен:

```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
AWATCH_OPS_LIVE_URL=http://127.0.0.1:8720 \
node scripts/operational-maturity-check.mjs --json --live
```

Live check не включает DLP, Loki или Velociraptor. Он только читает
health/readiness/version/metrics endpoints и принимает controlled statuses
`200` или `503`.

## Governance

Контракт расположен в `configs/operational-maturity-contract.json`.
Fixture payloads лежат в `docs/fixtures/operational-maturity/`.
CI workflow: `.github/workflows/operational-maturity.yml`.

Правило изменений: если endpoint, metric, schema, systemd unit или ClickHouse
migration меняется, сначала обновляется manifest/fixture, затем код. Удаление
полей или paths считается breaking change, если нет отдельного operator-approved
major contract change.
4 changes: 4 additions & 0 deletions docs/OPERATIONS_VALIDATION_RUNBOOK_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,16 @@ Repository-specific gate:
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
python3 scripts/public_secret_pattern_check.py
node scripts/operational-maturity-check.mjs --json

cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
export CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target
cargo run -p quality-gate -- --root /mnt/usb_hdd2/Projects/ActivityWatch-Russian
```

Подробности эксплуатационного maturity harness: [эксплуатационная зрелость
DetMir/AWatch-rus](OPERATIONAL_MATURITY_RU.md).

## Browser smoke

Browser smoke не заменяет API/CLI проверки. Он подтверждает, что операторский
Expand Down
6 changes: 6 additions & 0 deletions docs/fixtures/operational-maturity/portal-healthz.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"status": "ok",
"service": "detmir-portal",
"schema_version": "pilot-v1",
"timestamp": "2026-07-01T00:00:00Z"
}
26 changes: 26 additions & 0 deletions docs/fixtures/operational-maturity/portal-metrics.prom
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# HELP awatch_http_requests_total HTTP requests handled by AWatch-rus portal
# TYPE awatch_http_requests_total counter
awatch_http_requests_total{method="GET",route="/healthz",status="200",module="portal"} 1
# HELP awatch_http_request_duration_seconds HTTP request duration in seconds
# TYPE awatch_http_request_duration_seconds summary
awatch_http_request_duration_seconds_sum{method="GET",route="/healthz",status="200",module="portal"} 0.001
awatch_http_request_duration_seconds_count{method="GET",route="/healthz",status="200",module="portal"} 1
# TYPE awatch_report_requests_total counter
awatch_report_requests_total 1
# TYPE awatch_report_cache_hits_total counter
awatch_report_cache_hits_total 0
# TYPE awatch_report_cache_misses_total counter
awatch_report_cache_misses_total 1
# TYPE awatch_report_cache_stale_hits_total counter
awatch_report_cache_stale_hits_total 0
# TYPE awatch_reports_generated_total counter
awatch_reports_generated_total 1
# TYPE awatch_ingestion_records_total counter
awatch_ingestion_records_total 0
# TYPE awatch_ingestion_rejected_total counter
awatch_ingestion_rejected_total 0
# TYPE awatch_role_denied_total counter
awatch_role_denied_total 0
# HELP awatch_readyz_status Portal readiness status, 1=ready, 0=not_ready
# TYPE awatch_readyz_status gauge
awatch_readyz_status 1
26 changes: 26 additions & 0 deletions docs/fixtures/operational-maturity/portal-readyz.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"status": "ready",
"checks": {
"portal": {
"status": "ok",
"latency_ms": 1
},
"worktime_api": {
"status": "ok",
"stale": false
},
"clickhouse": {
"status": "ok",
"mode": "external"
},
"dlp_runtime": {
"status": "skipped",
"profile": "core_only",
"reason": "disabled_by_production_default"
},
"security_finding_inbox": {
"status": "ok",
"mode": "shadow"
}
}
}
10 changes: 10 additions & 0 deletions docs/fixtures/operational-maturity/portal-version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"app_name": "AWatch-rus DetMir Portal",
"app_version": "0.1.0",
"schema_version": "pilot-v1",
"environment": "test",
"build": {
"profile": "ci",
"source": "fixture"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"mode": "shadow",
"auto_apply": false,
"workflow": ["decide", "plan", "approve", "apply", "verify"],
"findings": [
{
"id": "fixture-001",
"source": "hayabusa",
"severity": "medium",
"status": "new",
"containment": {
"recommended": true,
"approved": false,
"applied": false
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"status": "DEGRADED",
"stale": false,
"reason": "report_unavailable",
"report_cache_hit": false,
"report_stale_served": false,
"aw_query_duration_ms": 350,
"aw_query_timeout_count": 1,
"report_build_error_count": 1,
"users": []
}
Loading
Loading