feat: harden firewall policies telemetry and operations
quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
This commit is contained in:
1 parent
aa475848fd
commit
9fa6269174
242 files changed
+19872
-3057
No files matched your search
@@ -0,0 +1,33 @@
|
||||
# Agent enrollment and token lifecycle
|
||||
|
||||
Install links are bearer invitations lasting 24 hours. The controller renders a credential scoped to that link with HMAC-SHA256; the global enrollment seed stays on the controller as the HMAC key. Changing that key invalidates outstanding invitations. Install IDs and slugs use 32 random characters. Both download paths use `Cache-Control: no-store`.
|
||||
|
||||
## Enrollment
|
||||
|
||||
`POST /v1/agent/enroll` requires `X-Evofw-Enrollment-Credential` and `install_link_id`, together with the agent-generated token and identity fields. Global `X-Evofw-Seed` enrollment is disabled. Already enrolled agents continue authenticating with their existing tokens.
|
||||
|
||||
Downloading does not consume an invitation. Successful enrollment consumes it in the same SQLite transaction that installs the agent token and changes the invited agent to pending. Approval remains a separate operator action. Concurrent submissions cannot replace the winner. An identical token retry returns the original agent, including after approval; a different token is rejected. A revoked/expired invitation or revoked agent is rejected before retry handling, so a previously downloaded script cannot bypass revocation or expiry. Existing pre-upgrade invitations receive a 24-hour expiry during migration; their old downloaded seed-based scripts must be downloaded again.
|
||||
|
||||
The Linux installer saves the generated enrollment token in a private `enrollment-pending.json` before its first request. A failed request keeps it for retry. The active config is committed with fsync, rename and directory fsync before pending cleanup. The RouterOS installer similarly saves its token before enrollment and uses the same token after restart. Reusing the invitation on a different machine after consumption is refused.
|
||||
|
||||
## Rotation API
|
||||
|
||||
An operator requests rotation with `POST /api/v1/agents/:id/token-rotation` (`fw:agents:write`). The server requires an approved agent running a known supporting client version (`linux/10`, `rsc/10`, or the matching `install.sh/2`). Older clients receive an unsupported-capability response and retain their current token. Repeating an outstanding request returns the same request ID. Requests expire after 24 hours.
|
||||
|
||||
Operators inspect safe status with `GET /api/v1/agents/:id/token-rotation`, which returns support, client version, agent status, pending request and last completed request receipt. The agent polls `GET /v1/agent/credentials`. The response contains `client_id`, an optional pending `rotation` with request ID and expiry, and an optional `completed_rotation` receipt containing only its request ID. It never contains tokens or token hashes.
|
||||
|
||||
The client generates its next token and saves `{request_id, old_token, next_token}` privately before `POST /v1/agent/token-rotation` with `{request_id, next_token}`, authenticated by the old token. SQLite atomically changes the active hash and records the exact exchange. The old token immediately stops working on all normal agent endpoints. Only the exchange endpoint recognizes the old hash for a 24-hour exact replay, and only while the recorded next hash remains the current credential and the agent remains approved. Wrong request/token pairs, revoked agents, and expired replays are refused.
|
||||
|
||||
After a matching ACK, Linux commits the active configuration before removing pending state; RouterOS commits its credential file before changing the active global. A restart resumes pending recovery before policy, telemetry or self-update. An ACK loss retries the same exchange and token. After the old-token replay window expires, the saved next token can authenticate a normal credential-status read: matching client ID and completed request receipt prove it is already active. A separate last-completed receipt survives a newer operator request and contains no old hash. Failed or mismatched responses preserve pending state and do not overwrite the active credential. When an uncommitted request expires or is superseded, a successful old-token status read proves that the old credential is still active. Only if the old request is no longer active does the client remove the abandoned pending attempt and continue policy/telemetry using the old token; the next sync handles a newer request. A request still active on the controller or an unavailable status read preserves pending state.
|
||||
|
||||
Rotation does not alter enforcement, policy hashes, telemetry queues or counter epochs. Credentials are never placed in logs or operator list responses. Revoking an agent blocks ordinary authentication and all exchange/recovery paths. Link revocation prevents new enrollment; agent revocation independently disables an enrolled credential.
|
||||
|
||||
## Client storage and limits
|
||||
|
||||
Linux uses mode-0600 pending files under its private config/state directories, an existing sync lock, and fsync/rename commits. Pending credentials must be treated as secrets and retained until recovery completes. RouterOS uses `flash/evofw-credentials.json` when flash storage exists, otherwise `evofw-credentials.json`. This authoritative file overrides the legacy token baked into the sync script on every run, including after reboot. Existing baked credentials migrate without resetting enforcement.
|
||||
|
||||
RouterOS file writes do not provide Linux crash-atomic durability. Power loss can leave corrupt or empty files; invalid state stops synchronization and requires recovery while existing firewall rules remain. RouterOS scripting syntax, persistence, and credential exchange have not been executed on a device or CHR. Linux kernel commands were not executed in this Windows environment.
|
||||
|
||||
## Validation
|
||||
|
||||
The credential suite executes the actual embedded Linux Python worker with a mocked HTTP transport and temporary files, covering lost ACK, process restart, expired old-token replay, and mismatched ACK. Directory fsync calls are adapted only in the Windows test harness. API tests cover scoped invitations, failed attempts without consumption, competing enrollment, exact retry, expiry/revocation after download, disabled global seed, client version gating, immediate normal-path old-token rejection, bounded exchange replay, and retained recovery receipts. RouterOS assertions inspect generated script contracts and ordering; they are not native execution.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Agent hardening and telemetry v2 checkpoint
|
||||
|
||||
This checkpoint implements agent safety, report delivery and capability-gated dual-stack compilation. It is not a production or device certification. Linux nft compiles and collects both IPv4 and IPv6. The ipset backend supports IPv6 when all required ip6tables tools are available; otherwise requested IPv6 enforcement is explicitly unsupported. RouterOS renders both families and probes native IPv6 availability before mutation; native execution remains unverified.
|
||||
|
||||
## Linux agent
|
||||
|
||||
- The downloaded policy is validated as JSON and written to data files. CIDRs, hash, actions, protocols and port ranges never pass through shell `eval`.
|
||||
- Mixed CIDR arrays are canonicalized and collapsed independently by family. nft creates separate IPv4/IPv6 address and Port ACL source sets. An IPv4 any-source ACL is restricted to IPv4 and cannot bypass IPv6 restrictions.
|
||||
- Strict default-drop semantics apply to both families. Required IPv6 ND/RA/control and management traffic must be explicitly allowed; capability support does not configure network autoconfiguration or add hidden allow exceptions.
|
||||
- The policy hash contract is `sha256:<64 lowercase hex characters>`, matching `GET /v1/agent/policy`.
|
||||
- A process lock serializes runs. Private run directories, HTTP deadlines and bounded kernel command execution prevent shared temporary-file and concurrent-apply races.
|
||||
- nft enforcement is compiled into one batch, checked with `nft --check --file`, then submitted as one transaction. Optional detail-counter features may be omitted after a failed feature check; required enforcement is retained.
|
||||
- ipset candidates are populated in inactive sets for both families when IPv6 tooling is available. Both family rule candidates are checked before any swap. If the second rule-family transaction fails, the already-applied first-family owned rules/jump positions and all four sets are rolled back. Foreign rules are preserved. Port ACL remains explicitly unsupported for ipset.
|
||||
- ipset candidates are populated in inactive sets. Swap or iptables-restore failure triggers compensating rollback. The pair of swaps is not one atomic kernel operation.
|
||||
- The owned iptables INPUT jump is reinserted at position one through the checked restore transaction. Drift verification includes INPUT order and rejects a foreign rule preempting this jump. Foreign INPUT rules are preserved.
|
||||
- Unchanged policies skip apply only after structural kernel verification. Traffic counters, kernel handles and expiring detail elements are excluded from this fingerprint.
|
||||
- The last successful hash and policy are committed only after successful required apply operations.
|
||||
- The JSON policy limit is 8 MiB, with 200,000 raw CIDR memberships, 50,000 canonical unique memberships, and 1,000 serialized Port ACL rules. Effective memberships are counted per deny/allow/source array before optional network collapse. Repeated entries in distinct arrays or expanded TCP/UDP rules count again. Adjacent and overlapping unique CIDRs count separately. Source import capacity is separate from assignment capacity. Rejected candidates report `POLICY_BUDGET_EXCEEDED` and retain last good enforcement.
|
||||
- A strong ETag is sent only with its exact durable last-good JSON body. Both commit together after successful apply or verified no-op. A 304 still runs validation and structural drift verification/repair using that saved body. Failed candidates never replace the cache. A bounded upstream 422 error body propagates only known structured codes; authentication failure remains an authentication failure.
|
||||
|
||||
## Reports
|
||||
|
||||
- Reports use the shared telemetry v2 schema on `POST /v1/agent/apply-report`.
|
||||
- Linux persists sequence allocation and up to 32 immutable pending report payloads in one state file using file fsync, rename and directory fsync. Reports are retried with their original ID, sequence and contents.
|
||||
- An HTTP success is insufficient by itself: the response must acknowledge the same report ID and sequence as accepted, duplicate, or already superseded by a newer sequence.
|
||||
- A matching authenticated stale ACK supplies the durable server `sequence_floor`. A rolled-back client resumes at floor + 1 with a fresh counter epoch and explicit `sequence_rollback_coverage_unknown`; retries retain their original payloads. Invalid floors preserve pending bytes. A MAX_SAFE_INTEGER floor stops sync without discarding pending state and requires re-enrollment; sequence numbers never wrap.
|
||||
- Loss counts are per report interval, drained only in the same durable commit as the queued report. They are not lifetime totals repeated in every subsequent report.
|
||||
- A newer generation with the same hash is acknowledged after kernel verification without apply or epoch rotation; previously queued report payloads remain immutable.
|
||||
- A full or unwritable queue blocks counter-resetting apply. A final snapshot of the previous epoch is queued before apply. New policy installation rotates counter epochs; reboot and observed counter decreases also rotate the affected epochs.
|
||||
- nft traffic collection reads JSON and stable owned rule IDs. Prerouting accepts are excluded from accepted totals because the same packet subsequently traverses input or forward. Drops are terminal and counted at the rejecting hook.
|
||||
- ipset traffic collection reads owned iptables counters. Detail collection does not present prefix aggregates as measured individual-host traffic.
|
||||
- Expected active families are persisted. If any required iptables-family read fails, aggregate packet totals are omitted and quality is partial. The prior complete per-rule baselines survive the omission, so recovery detects a family reset even when the other family's counters grew.
|
||||
- Missing, unsupported and unsafe integer counters are omitted and labelled in quality metadata. They are not sent as observed zeroes. Detail truncation includes found/sent counts when known.
|
||||
- Host firewall snapshots are refreshed every five minutes or after apply. Failed observation is unknown and omitted. A successful report ACK does not trigger a separate heartbeat.
|
||||
|
||||
## RouterOS
|
||||
|
||||
- Target minimum version is RouterOS 7.21, with the required scripting, JSON, random-string, file-read and import commands. That minimum has not been executed on a device. Native IPv6 is probed before either family mutates; a policy requesting IPv6 is rejected with `UNSUPPORTED_CAPABILITY` when unavailable. An IPv4-only policy can still use the supported family.
|
||||
- The renderer stages unique address-list banks and complete disabled canonical rules for both families before activation. IPv4 and IPv6 are separate native transactions: failure attempts compensation for both. Original owned rules retain their full matchers and positions; the transaction changes only their comment/disabled fields, and rollback restores those snapshot values while removing newly created rules. Originally disabled rules remain disabled on rollback. Foreign rules are preserved.
|
||||
- Missing, duplicated, modified or reordered owned rules trigger canonical replacement. Successful verification checks chain, action, enabled state, source bank, unexpected matchers and static rule order. One prior disabled retired wave is retained for rollback and reaped on the next apply; only unreferenced owned banks are cleaned. An unexpectedly active retired rule requires manual recovery.
|
||||
- RouterOS enforcement is INPUT deny only, with remaining INPUT behavior controlled externally. FORWARD is deny, allow, then configured default; default accept leaves the default-drop rule disabled. This scope applies equally to both families. No hidden ND/RA or management exceptions are added.
|
||||
- The sync reads JSON and then imports the current RSC endpoint. It acknowledges the actual hash/revision published only after successful import verification, so a controller generation change between those requests cannot produce a false ACK. Verified no-op updates the applied revision without resetting counters.
|
||||
- The script retains one serialized complete v2 pending report and next sequence together, using `flash/evofw-telemetry.json` when a flash disk/directory is present and `evofw-telemetry.json` otherwise. Only a matching ACK clears it. Valid rolled-back state resynchronizes from the authenticated server floor, rotates the traffic epoch and labels unknown coverage. Corrupt or exhausted state stops sync instead of silently resetting numbering.
|
||||
- RouterOS file updates do not provide the Linux fsync-and-rename durability contract. This limitation is included in report quality metadata. Power-loss behavior requires device validation.
|
||||
- Address-list presence is not a packet measurement. RouterOS omits per-IP and per-port packet metrics and reports those capabilities as unavailable. It does not synthesize `packets: 1` from presence.
|
||||
- Default FORWARD DROP verification requires an empty source address list in both families; unexpected narrowing matcher keys are rejected before accepting a no-op.
|
||||
- Rule counters are checked for safe integer range before aggregation across both active families. Complete observations retain separate stable family/rule baselines; missing-family reads leave those baselines intact. A decrease in any individual rule rotates the traffic epoch even when other counters grew, and both regular and pre-apply final snapshots label `counter_reset_coverage_unknown`. Successful apply clears the prior rule baselines. A missing required family omits aggregate totals. Traffic epochs rotate after detected resets; the final old-epoch snapshot must be saved and ACKed before an apply that resets counters. No separate post-ACK heartbeat is emitted.
|
||||
- Only successful probe and verified enforcement advertise `policy-ipv6` and `traffic-counters-ipv6`. RouterOS Port ACL is explicitly unsupported; IP and port detail metrics remain omitted. Linux nft separately advertises implemented IPv6 Port ACL/detail capabilities; ipset advertises IPv6 enforcement without Port ACL.
|
||||
- Policy downloads use files and bounded chunk reads, avoiding the native `output=user` response truncation. Both JSON and generated RSC are checked against 8 MiB before parsing/import. RouterOS requests `?transport=routeros`: only the controller's known budget rejection becomes a 200 error envelope with no hash/revision/cache validator. The installer recognizes that envelope before policy validation or import. Authentication and unexpected failures keep normal HTTP failure behavior. Native fetch has no documented hard byte quota: the cooperative Range header may be ignored, so download disk bounds rely on the controller's pre-transmission budget plus a one-minute deadline and local post-download check.
|
||||
|
||||
### RouterOS primary sources and assumptions
|
||||
|
||||
- [Scripting manual](https://manual.mikrotik.com/docs/developer-guides/scripting/) documents runtime errors, conditional blocks, arrays, JSON conversion and import handling. The older official [RouterOS scripting presentation](https://mum.mikrotik.com/presentations/US07/v3.pdf), page 23, describes unknown commands failing at runtime. The missing-IPv6-path probe is based on those semantics; parsing/import behavior on a device without the IPv6 command remains a required test.
|
||||
- [Fetch CLI](https://manual.mikrotik.com/docs/cli-reference/tool/fetch/) documents the silent 63 KiB `output=user` response limit, file output, HTTP headers and duration. Pending report/ACK bodies are small; policy downloads use file output instead.
|
||||
- [File read CLI](https://manual.mikrotik.com/docs/cli-reference/file/read/) documents chunked reads up to 32,768 bytes and returned `data`. The script checks file size before reading and detects missing progress.
|
||||
- The official [Files guide](https://help.mikrotik.com/docs/spaces/ROS/pages/2555971/Files) documents persistent `flash/` placement, including flash entries typed `disk`, and delayed NAND writes that can leave empty files after power loss. Corrupt state fails closed; this checkpoint does not claim crash-atomic native storage.
|
||||
- [Clock manual](https://manual.mikrotik.com/docs/system-information-and-utilities/clock/) documents date, time and GMT offset fields used for report timestamps. Version-specific timestamp formatting, large JSON/string allocation, import scope, matcher serialization and compensating rollback still need CHR/device execution.
|
||||
|
||||
## Verification and remaining device work
|
||||
|
||||
Focused validation:
|
||||
|
||||
```powershell
|
||||
node apps/api/node_modules/vitest/vitest.mjs run src/services/agent-hardening.test.ts src/services/agent-hardening-telemetry.test.ts src/services/agent-hardening-dualstack.test.ts src/services/agent-hardening-budget.test.ts src/services/agent-hardening-cache.test.ts src/services/agent-hardening-mikrotik.test.ts src/services/policy/mikrotik-rsc.test.ts src/services/sync-script.test.ts src/services/install-links.test.ts --root apps/api
|
||||
node apps/api/node_modules/typescript/bin/tsc --noEmit -p apps/api/tsconfig.json
|
||||
```
|
||||
|
||||
Current checkpoint: **100 focused tests passed** across the nine files above; API TypeScript and Bash syntax checks for the agent and installer passed. The suite executes embedded Python, real Git Bash control flow with mocked firewall commands, actual authenticated policy endpoints, the Bash parser, shared report schema validation, API ingestion, replay acknowledgement and queue removal. It covers queue overflow, mismatched ACK, timeout, failed persistence, final-snapshot ordering, mixed-family rollback, missing-family recovery, no-op revision ACK, canonical budget boundaries, 304 drift repair, failed-candidate cache retention and stale-sequence resynchronization. RouterOS checks are generated-code/static contracts plus actual API roundtrips; they do not emulate or execute native import.
|
||||
|
||||
Linux kernel nft/ipset/iptables commands were not executed in this Windows environment. nft syntax feature compatibility, kernel rollback behavior, Docker packet paths and counters require a Linux network-namespace or VM matrix. RouterOS scripts were rendered and checked statically; they were not executed on a RouterOS device or CHR. RouterOS syntax/version compatibility, clock offset formatting, file persistence, boot recovery and command-failure rollback remain device checks. No deployment is implied by these tests.
|
||||
|
||||
Follow-up RouterOS regression validation: **20 tests passed** across `agent-hardening-mikrotik.test.ts` and `policy/mikrotik-rsc.test.ts`, including both-family default matcher and shared per-rule reset detector contracts. A broader 102-test run in the restricted Windows sandbox produced 67 passes and 35 environment failures (Git Bash startup exit 3221225794 and Python atomic replace WinError 5 in sandbox temp storage); this does not replace the prior successful checkpoint run. These new checks are static native-script contracts, not RouterOS execution.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Agent labels and materialized assignment
|
||||
|
||||
Migration `019_agent_labels.sql` adds a bounded label catalog and agent-label join table. Label changes are metadata and do not advance policy generations. Names are unique after NFKC normalization and case folding. Up to 1000 labels may be stored, with at most 20 selected labels per agent.
|
||||
|
||||
All API paths use `/api/v1`:
|
||||
|
||||
- `GET /agent-labels?limit=20&offset=0`, `POST /agent-labels {name,color?}`, `PATCH /agent-labels/:id`, `DELETE /agent-labels/:id` manage the catalog. Maximum page size is 50. Deleting a label removes associations without changing policies.
|
||||
- `GET /agents/:id/labels`, `PUT /agents/:id/labels {label_ids}` read or replace agent labels. Replacement validates every label before writing.
|
||||
- `GET /agents?label_ids=id1,id2` filters by intersection (all specified labels). Empty label filters retain existing fleet behavior. Repeated query parameters are also accepted.
|
||||
- `POST /agent-labels/preview-assignment {selector,mode,set_ids}` resolves a concrete target list and returns `{mode,total,items:[{agent_id,generation,previous_set_ids,set_ids,changed}]}`. The selector must contain either `agent_ids` (1–100) or `label_ids` (1–20). Label selection uses intersection and rejects groups larger than 100 instead of truncating. Empty groups can be previewed.
|
||||
- `POST /agent-labels/assign-policy-sets` takes the preview request plus `expected:[{agent_id,generation}]`. The expected IDs must match the currently resolved selection exactly, and every generation must match. Empty assignments, invalid sets, missing agents, pin conflicts or concurrent membership/revision changes fail before any target is changed.
|
||||
|
||||
`mode:add` retains existing assignment order and appends missing selected sets. `mode:replace` uses the supplied exact set order, including an empty set list. Each final assignment has at most 100 sets through this bulk endpoint. Identical assignments retain the existing generation. The entire materialized operation shares one transaction.
|
||||
|
||||
Assignments are one-time changes. Agents that receive a label later do not inherit policies automatically. Label mutations require agents permissions; assignment preview requires policies read and assignment publication requires policies write.
|
||||
|
||||
Validation includes atomic invalid-label/set rejection, case-insensitive names, label-only revision preservation, add/replace ordering, unchanged revision behavior, exact membership/revision CAS, pin conflicts, 100/101-agent group boundaries, no subsequent automatic assignment and actual HTTP filtering/RBAC mapping. This does not establish live fleet rollout or production readiness.
|
||||
+5
-9
@@ -24,17 +24,13 @@ curl -fsSL https://<cp>/agent-install/<id> | bash
|
||||
|
||||
API (auth): `POST /api/v1/install-links` `{ "name": "web-01", "platform": "linux" | "mikrotik" }`.
|
||||
|
||||
## Linux (legacy one-liner)
|
||||
## Одноразовое подключение и совместимость
|
||||
|
||||
```bash
|
||||
curl -fsSL https://<cp>/v1/agent/install.sh | \
|
||||
EVOFW_CP_URL=https://<cp> \
|
||||
EVOFW_SEED=<seed> \
|
||||
EVOFW_CLIENT_NAME="web-01" \
|
||||
bash
|
||||
```
|
||||
Новый агент подключается только по созданной в UI install-ссылке. Скрипт содержит ограниченный credential конкретной ссылки; `POST /v1/agent/enroll` принимает его в `X-EvoFW-Enrollment-Credential`. Успешная регистрация потребляет credential атомарно; точный повтор того же запроса возвращает того же агента. Истёкшая, отозванная или использованная с другими данными ссылка не создаёт нового агента. Для чистой переустановки создайте новую ссылку.
|
||||
|
||||
Создаёт нового агента со статусом Pending (без Invited).
|
||||
Глобальный `EVOFW_SEED` / `X-EvoFW-Seed` больше не подходит для регистрации. `enroll_seed` хранится только на контроллере как HMAC master; UI/API возвращает маску, а не значение. Сохранение `********` оставляет ключ прежним. Явная смена master делает ещё не использованные старые install credentials недействительными; действующие токены агентов сохраняются.
|
||||
|
||||
Ротация токена поддерживается версиями `linux/10`, `rsc/10` и `install.sh/2`; более старый агент сначала обновляют. Администратор запрашивает ротацию для конкретного Approved агента; клиент сохраняет новый токен до обмена, получает подтверждение и атомарно обновляет конфигурацию. Старый токен после обмена принимается только для точного повтора завершённого обмена в течение 24 часов, а не для чтения политики. Отзыв агента запрещает и такие повторы.
|
||||
|
||||
Файлы: `/etc/evofw/agent.conf`, `/usr/local/sbin/evofw-firewall.sh`, timer `evofw-firewall.timer` (default 1min).
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
## Потоки
|
||||
|
||||
1. **Enroll** — `POST /v1/agent/enroll` + `X-EvoFW-Seed` → pending agent
|
||||
1. **Enroll** — `POST /v1/agent/enroll` + одноразовый `X-EvoFW-Enrollment-Credential` конкретной install-ссылки → pending agent; глобальный seed не передаётся клиенту
|
||||
2. **Approve** — UI/API → status approved
|
||||
3. **Policy** — `GET /v1/agent/policy` → deny/allow CIDRs + `default_action` + optional `port_rules` + hash (`apply_version: 3`) + `script_sha256` (Linux; не в `policy.hash`)
|
||||
4. **Linux self-update** — timer: `GET /v1/agent/sync-script` (`ETag` / `If-None-Match`) → при новой версии заменить `/usr/local/sbin/evofw-firewall.sh` и `exec` до policy
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Backend hardening checkpoint
|
||||
|
||||
Completed in the first implementation batch:
|
||||
|
||||
- Shared browser-compatible strict IP/CIDR parser: mask network bits, canonical IPv6 spelling, IPv4-mapped IPv6 support; ingress validation for rules, overrides, port ACL, manual entries and reported IP/network hits.
|
||||
- Manual list creation materializes and validates config entries through the same path as the entry editor.
|
||||
- EvoBGP autocomplete decrypts the stored API token; community resolution and prefix refresh collect all cursor pages and reject broken/repeated cursors.
|
||||
- Invalid JSON feed shapes, malformed prefixes and partial fetches retain the complete last successful entries. DNS/manual refresh failure retains the previous materialization.
|
||||
- Canonical list snapshots skip identical replacements; unchanged refresh skips agent generation updates. Effective policy hashes exclude generation. Hostname rules skip unchanged cache replacement. The full affected list closure is staged before one transaction publishes configs, materializations and generation changes; parent DNS failure cannot leave a partially updated child.
|
||||
- Replacement inserts use 200-entry SQL batches; regression covers 50,000 prefixes.
|
||||
- Referenced list deletion returns conflict while nested lists, policy rules or port ACL depend on it.
|
||||
- Self-cloning and cross-agent override deletion are rejected. Reordering validates an exact permutation and uses an available priority slot to avoid collisions with existing high priorities. Clone writes are transactional.
|
||||
- Each SQL migration and its applied stamp commit atomically. Foreign-key enforcement is disabled outside the transaction for referenced-table rebuilds, checked before commit, and restored after success/failure. Populated upgrade regression verifies preserved children.
|
||||
|
||||
Validation at this checkpoint:
|
||||
|
||||
- Shared build and 22 tests pass; DB build passes; API TypeScript check passes.
|
||||
- API focused backend suite: 29 tests pass, including eight new integrity regressions (encrypted autocomplete and manual config ingress included).
|
||||
- The broader API run also exercised concurrent agent work. Remaining failures at that time were native Bash startup in the sandbox (four shell tests) and the old MikroTik renderer fixture (owned by the agent implementation wave). The legacy IP-hit network compatibility failure found in that run was fixed and the IP statistics suite now passes.
|
||||
|
||||
Second checkpoint: telemetry and diagnostics
|
||||
|
||||
- `applyReportV2BodySchema` / `ApplyReportV2Body` are exported from shared. `/v1/agent/apply-report` validates the complete v2 envelope. Invalid v2 cannot silently fall back to legacy handling. Negative, fractional and unsafe integer counters are rejected; canonical duplicate hit keys are rejected.
|
||||
- Required v2 metadata: report ID, persistent sequence, boot ID, observation time, interval, independent traffic/IP/port epochs, capabilities and quality. Optional detail found/sent counts are validated against supplied arrays and persisted. Missing counts mark coverage partial; fewer sent elements mark truncation.
|
||||
- One synchronous transaction performs replay/order checks, baseline updates, traffic/detail accumulation, samples, capabilities/quality, apply diagnostics, and host snapshot updates. HTTP 200 v2 acknowledgements echo `report_id` and `sequence`; `accepted: false` reports duplicate or stale delivery without repeating accounting.
|
||||
- Durable agent replay/baseline state survives UI resets and raw-sample deletion. Once v2 is established, delayed legacy payloads cannot replace its state. Legacy-only telemetry remains accepted with explicit limited quality metadata.
|
||||
- Boot/domain epoch changes establish new baselines even when absolute counters increase. Missing values and initial/new-epoch traffic baselines store nullable deltas rather than observed zeroes. Backward observation clocks mark the report partial and leave interval coverage unknown. Unsafe accumulated totals retain their exact previous value and mark overflow unsupported/partial instead of rounding.
|
||||
- UI reset clears displayed traffic and detail totals while retaining device baselines, sequence watermark and history. Regression covers `100 -> reset -> 100 -> 105`, producing zero additional packets until the last report adds five.
|
||||
- Agent DTO adds `diagnostics` with desired/applied revision/hash, version, capabilities, contact/telemetry/apply times, structured errors and convergence state. The desired hash is known after policy delivery for the current generation; stale generations expose a null desired hash. Error-only reports preserve the last successful applied acknowledgement. Counter-only telemetry cannot clear a previous apply failure.
|
||||
- `/api/v1/ui/summary` returns the agreed compact counts. It authenticates `fw` access and independently omits each unauthorized dashboard/agents/lists/policies section. Shared `UiSummary` / `uiSummarySchema` are exported.
|
||||
- API build no longer copies agent scripts twice through tsup `--publicDir`; explicit `dist/agent-scripts` packaging remains.
|
||||
|
||||
Second-checkpoint validation:
|
||||
|
||||
- Shared and DB builds pass; shared tests: 22/22; API TypeScript check passes.
|
||||
- Dedicated telemetry suite: 10/10, summary/authentication/reset routes: 2/2.
|
||||
- Existing backend suite plus new tests: 66/68 in the concurrent source-hardening checkout. The two failures were source transport mocks still targeting global fetch after the source owner introduced `safeFetchJson`; source owner is adapting those fixtures. Shell/device tests remain owned by the agent implementation wave.
|
||||
|
||||
Third checkpoint: analytics, retention and import bounds
|
||||
|
||||
- Fixed both second-review findings: an acknowledgement with the same hash/revision recovers after `status: error` even when no optional error text was supplied; unknown/reset/decreased IP/port counter intervals mark quality partial. Both regressions pass through the shared ingress schema.
|
||||
- Migration 013 adds per-metric baseline timestamps, raw presence/interval metadata, an idempotent analytics-processed marker, and indexed per-agent UTC minute/hour/day buckets.
|
||||
- Every accepted report publishes its sample, replay state and additive rollups in the same transaction. Counters span their own last accepted observation; missing values preserve that timestamp. Skipped sequence deltas use the whole observation span rather than the producer's shorter current sampling interval.
|
||||
- Interval deltas are allocated proportionally across overlapping UTC buckets. Distribution is explicitly estimated when the observation interval differs from the bucket. Missing selected-agent metrics remain null; measured zero remains zero. Coverage is conservative across accepted/dropped metrics. Unknown first/new-epoch intervals are gaps. Future/backward clocks are partial and cannot create unbounded bucket loops.
|
||||
- `GET /api/v1/stats/timeseries` and `.csv` accept `from`, `to`, `resolution=1m|1h|1d`, and comma-separated `agent_ids` (maximum 100). Ranges are half-open `[from,to)` and returned bounds are rounded to UTC bucket boundaries. The 1440-point cap applies after rounding. SQL aggregates the bounded persisted buckets; requests do not scan raw history. Shared `TimeseriesResponse` / `TimeseriesPoint` / `StatsResolution` are exported.
|
||||
- `GET /api/v1/agents/:id/stats/raw` uses a stable `(recorded_at,id)` cursor, limit 1..500, `has_more` and `next_cursor`. It exposes nullable missing counters, deltas, interval starts, observation metadata, quality and detail counts. All analytics/CSV/raw reads require statistics permission.
|
||||
- Retention runs hourly and at startup: raw/minute 24 hours, hourly 30 days, daily 90 days, detail 7 days. Existing raw samples are backfilled once as explicitly estimated/partial before deletion; atomic processed claims prevent overlapping workers from double accounting. New samples are already rolled up transactionally. The worker yields between bounded deletion/backfill batches and accepts shutdown cancellation. The old `STATS_RETENTION_DAYS` config field remains for compatibility but does not override these fixed tier policies.
|
||||
- IP/port aggregates retain SQL top-N behavior and enforce per-agent storage budgets (2000 IP rows, 5000 port rows). Eviction marks truncated/partial quality. UI counter reset preserves rollups/history and device baselines.
|
||||
- Only list creation/entry import routes accept 8 MiB bodies; both plaintext token and structured item imports are capped at 100000 entries. Other routes retain their previous body limit.
|
||||
|
||||
Third-checkpoint validation:
|
||||
|
||||
- Shared/DB builds and API TypeScript check pass. Focused backend run: 19 files, 88 tests pass, excluding native agent/sandbox socket suites.
|
||||
- Dedicated analytics: 10 tests, telemetry: 12 tests, summary/routes/import: 4 tests. Coverage includes proportional multi-agent intervals, missing-metric spans, skipped sequences, epoch gaps, known zero, transactional rollback, huge intervals, retention tiers, estimated backfill, concurrent backfill, cardinality, CSV/RBAC/raw cursors, and an actual >1 MiB import of 100000 prefixes.
|
||||
- Broader sandbox execution also reached new native agent/socket tests; the remaining failures were Bash startup, Windows atomic-file replacement restrictions and sandbox socket errors. The full unrestricted integration run belongs to the final root verification checkpoint.
|
||||
|
||||
Still pending; do not treat the roadmap as completed:
|
||||
|
||||
- Final frontend analytics verification and updated OpenAPI/reference documentation for the new telemetry and statistics contracts.
|
||||
- Larger policy compilation/performance wave, source scheduling/retry observability and full target-load verification.
|
||||
- Real Linux kernel/firewall and RouterOS acceptance; mock/static checks cannot establish production device behavior.
|
||||
|
||||
The backend schema, persistence, analytics and retention above are implemented. Cache/load acceptance and production kernel/device validation remain separate work.
|
||||
|
||||
F1 checkpoint: explain and temporary overrides are implemented and described in `docs/policy-explain.md` (migration015). Analytics follow-up regressions cover explicit v2-null backfill, interval loss counts, traffic-only completeness, preserved quality reasons and exact minute-to-hour aggregation. The later ipset explanation correction follows its actual INPUT-only compiler rather than assuming nft stateful/forward behavior.
|
||||
|
||||
Policy bounds/cache checkpoint: implemented50000 effective/200000 raw memberships,1000 expanded Port ACL rules,8 MiB policy responses, last-good-preserving refusal, dualstack `all` sources, DB-scoped128-entry/32 MiB conservative LRU/intern accounting, expiry/dependency invalidation and full-response versioned ETags. Opt-in RouterOS budget envelopes and durable `sequence_floor` ACK recovery are documented in `docs/policy-budget-cache.md`. Focused verification:88 distinct backend tests pass, API TypeScript/shared/DB builds pass. Conditional304 client bundles remain the agent owner's follow-up; existing clients without a conditional header receive200.
|
||||
|
||||
Still pending: prepared/batched IP/port detail UPSERT optimization and target-load profiling; snapshots/canary workflows; enrollment rotation and labels/groups; final OpenAPI synchronization and real kernel/RouterOS runtime acceptance. No broader roadmap completion is claimed.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Frontend validation
|
||||
|
||||
## Baseline (2026-10-24)
|
||||
|
||||
- Existing portal appearance, ReUI Frame surfaces, density and theme are preserved.
|
||||
- Parent-task baseline: initial JavaScript approximately 214 KB gzip; CSS approximately 27 KB gzip.
|
||||
- Existing web suite: 4 files, 10 tests passing. Existing TypeScript check passes.
|
||||
|
||||
## Safety checkpoint
|
||||
|
||||
- Settings refreshes only hydrate a clean form. Successful saves update cache and accepted defaults together; edits are disabled while saving. A failed background refresh shows an error while keeping existing form values mounted.
|
||||
- Policy assignments cannot be changed until both assignments and catalog have loaded successfully. Failed reads no longer seed an empty assignment list. In-flight assignment reads are cancelled before writes; successful writes populate their query cache.
|
||||
- Dashboard, agent preview, fleet chart and observed host firewall show explicit error/retry states instead of reporting empty results.
|
||||
- Dashboard agent rows route directly to agent detail.
|
||||
- Command palette no longer claims to parse natural language. Combobox/listbox relationships and active options are explicit.
|
||||
- Sortable attributes now belong to the focusable drag handle. Table rows support Enter/Space and ignore interactive descendants.
|
||||
|
||||
## Verification
|
||||
|
||||
- Direct local TypeScript invocation: `node node_modules/typescript/bin/tsc --noEmit` from `apps/web`.
|
||||
- Direct local Vitest invocation: `node node_modules/vitest/vitest.mjs run` from `apps/web`.
|
||||
- Web suite now has 8 files and 20 passing tests. Coverage includes initial/stale query failures, strict ACL ports/CIDRs/ranges, bounded API retries and invalidation of derived agent policies after list mutations.
|
||||
- The development-only `/test/interaction.html` harness is outside the production entry graph. The parent task verified 15/15 mounted browser assertions with `#results[data-status=passed]` on isolated Vite `127.0.0.1:15178` and fixture-only fetches. It verifies QueryState DOM identity, focus, local edit state, mount count and recovery; failed assignment GET cannot issue PUT; the real settings page preserves dirty fields through data refresh/error, retains focus and saves once without reverting to stale cache.
|
||||
- `pnpm` self-bootstrap is restricted by sandbox symlink permissions; direct local binaries avoid dependency or lockfile changes. Registry CLI docs fetch encountered npm DNS failure; installed component APIs and ReUI MCP inline API were inspected instead.
|
||||
- No registry components were overwritten; no dependencies or lockfiles changed. No blanket style or layout rewrite.
|
||||
|
||||
## Second checkpoint
|
||||
|
||||
- Shell now uses the agreed `GET /api/v1/ui/summary` contract. Full agents/lists/policies catalogs are enabled only while the command palette is open and the matching permission is present, with 60-second catalog freshness. **Backend summary endpoint integration remains pending at this checkpoint.** A missing/failed endpoint displays an error and unknown/stale state; it never becomes healthy by falling back to empty catalogs.
|
||||
- Monitor derives counts from the server summary, uses the actual response timestamp and hides omitted unauthorized sections. Object search uses the shared five-minute online predicate. Healthy/warning trigger copy follows actual monitor data.
|
||||
- All central GET query functions pass TanStack cancellation signals into fetch. Optimistic agent/status and policy-set writes cancel pending reads first. Settings saves cancel pending reads before accepting their new cache value. Central mutation dependency invalidation covers shell counts, policy counts, assignments and derived agent previews.
|
||||
- HTTP errors preserve status. Authorization, validation and missing-resource errors do not retry; transient failures retry at most once; cancellations never retry.
|
||||
- Port ACL uses shared canonical validation with inline field errors and submit handling. Imports reject partial/reversed ranges, exponent notation, out-of-range ports and more than 50 entries. ACL/host/catalog failures show retry states. Read-only ACL actions are disabled.
|
||||
- Host firewall, ACL, blocked-IP and blocked-port detail components load through independent lazy boundaries. Agent-list clicks navigate directly to detail.
|
||||
- High contrast now changes theme-aware border/input/ring/muted-text tokens only when the existing user setting explicitly selects high contrast. Standard theme tokens are unchanged.
|
||||
- Astra review corrections preserve QueryState child identity across success/error; keyboard palette selection scrolls into view; disabled sortable handles retain `tabIndex=-1` after DnD attributes.
|
||||
|
||||
### Build evidence
|
||||
|
||||
Production Vite build passes (existing registry `use no memo`/source-map warnings remain). Compared with the first safety checkpoint, the agent detail route chunk fell from 54.10 KB (14.50 KB gzip) to 29.34 KB (8.82 KB gzip), with lazy ACL 19.91 KB / 6.35 KB gzip, host firewall 5.91 KB / 2.06 KB gzip, blocked IP 3.12 KB / 1.54 KB gzip and blocked ports 2.64 KB / 1.23 KB gzip. Shared main chunk is 334.94 KB / 104.04 KB gzip (prior checkpoint 332.67 KB / 103.31 KB gzip); CSS is 176.38 KB / 27.12 KB gzip (prior 175.57 KB / 27.01 KB gzip). These are chunk measurements, not a claim that the total initial network transfer decreased.
|
||||
|
||||
Summary integration was subsequently verified by the parent task against the isolated API: the shell loads the endpoint without 404 and reports zero problems for the empty fixture database. No production deployment has been performed.
|
||||
|
||||
## Analytics and diagnostics checkpoint
|
||||
|
||||
- The shell now imports the exported shared `UiSummary` type; the duplicate frontend DTO has been removed.
|
||||
- Fleet and statistics charts read the shared, runtime-validated `TimeseriesResponse` contract from `/api/v1/stats/timeseries`. Requests use completed UTC half-open buckets and cancellation signals. Missing packet metrics remain null and create gaps; unsupported presence/hit snapshots do not become packet counts. The UI labels accepted traffic as permissions granted by EvoFirewall rules, with a note that other host rules may still block it.
|
||||
- Statistics keeps the KPI → tabs → chart/table structure. Period presets are 1 hour at 1 minute, 24 hours at 1 hour, 7 days at 1 hour and 30 days at 1 day. Optional selections are capped at 100 agents. Packet KPIs sum known buckets only and say so. Coverage, estimated/partial/truncated buckets, lost reports and unsupported metrics are visible. Legacy raw samples remain a separately labeled table with no client-generated hourly peaks.
|
||||
- CSV uses the same bounded request, the existing bearer-header/401/error flow and a 2 MiB client response cap. Tokens never enter URLs or filenames. Agent-name selections apply immediately; downloads abort on page unmount.
|
||||
- The new Frame diagnostics section uses `agent.diagnostics` directly: authoritative state, separate contact/telemetry/application timestamps including seconds, desired/applied revision and hash, structured apply error, capabilities, quality and detail counts. Missing diagnostics or missing acknowledgments remain explicitly unknown. Agent detail refreshes every 30 seconds.
|
||||
- External list health shows the last **successful** `refreshed_at`, retained entry count and current failure. It no longer substitutes the general `updated_at` timestamp for a successful source refresh. Manual-list modification time remains separate.
|
||||
- The UI contract now records the user's approved current-layout/style exceptions, installed ReUI version and opt-in accessibility behavior. No registry components, dependencies or lockfiles were overwritten.
|
||||
|
||||
### Verification
|
||||
|
||||
- TypeScript passes. Web suite: **28 tests in 11 files**, including UTC boundaries/limits, null metrics and coverage, diagnostics truthfulness and authenticated CSV validation.
|
||||
- Parent browser run: **23/23 mounted harness checks**, `#results[data-status=passed]`. New assertions cover drift versus contact, absent telemetry/apply acknowledgment, partial/truncated/lost-report quality, null packet gap display and unchanged null values. A real PortalShell fixture with an agents-read-only token checks one summary request, no closed-palette full catalog requests, hidden unauthorized navigation and omitted summary metrics.
|
||||
- ReUI audit checklist retrieved. Targeted C1–C4 searches for new UI code returned no animation/raw palette/native table/select/overlay/Date.now/Radix-asChild issues. No new animation was added. No lint script is configured in `apps/web`; lint was not introduced. Mounted checks are behavioral verification; full responsive visual screenshots and interactive CSV download checks remain separate.
|
||||
- Production build passes with existing registry directive/source-map warnings. This checkpoint: stats route 9.46 KB / 4.14 KB gzip; shared stats-window/chart chunk 10.91 KB / 4.37 KB gzip; agent detail route 34.64 KB / 10.08 KB gzip; shared main chunk 336.94 KB / 104.68 KB gzip; CSS 176.42 KB / 27.13 KB gzip. These are chunk sizes, not total initial transfer measurements.
|
||||
- Live integration was subsequently verified by the parent task: minute view showed 600 dropped, 1800 allowed, 100% coverage and 60/60 measured intervals, with no unsupported/partial badges after the traffic-only quality fix. A direct agent URL showed distinct desired/applied/contact/telemetry fields. The CSV endpoint returned HTTP 200 `text/csv` with 60 data rows and the browser button was clicked without error; the downloaded file was not captured. The automated harness itself still uses controlled fixtures. No HAR, responsive baseline or performance improvement claim is implied.
|
||||
|
||||
## F1 Explain, temporary overrides and forms checkpoint
|
||||
|
||||
- Explain uses the shared request/response schemas in a FormSheet, with strict source IP and paired port/protocol validation, input/forward, DNAT and connection state controls. The result keeps desired policy, last acknowledged revision/hash and incomplete external-firewall evidence distinct. Null/external/unsupported/transport-unknown decisions remain unknown. Logical witnesses and bounded match counts are displayed, with localized capability/scope limits. Editing inputs leaves the previous result labeled as stale until explicitly checked again. It is a read operation.
|
||||
- Overrides now support an optional reason and either a bounded duration or an explicit future date within 30 days. Validation emits only the selected expiry field, uses shared canonical CIDR validation and rejects invalid TTLs. The server owns author identity. Author, reason, expiry and expired history are shown. Copy explicitly states that expiration changes desired policy and takes effect on the next policy sync, including delayed enforcement for offline agents. Failed writes retain dirty fields; successful writes reset them; closing the sheet keeps its in-memory draft in the current agent card.
|
||||
- Statistics replaces the raw UUID textbox with the installed Base UI name/hostname combobox and chips, keyboard behavior, visible selection count, a 100-agent limit, permission/error states and a reset-to-fleet action. IDs remain internal query values.
|
||||
- Added mean packets/second, peak **bucket-average** packets/second and managed-rule drop ratio. These require complete traffic coverage, non-null metrics and no partial-traffic flag. A zero denominator gives unknown drop ratio. Partial tooltip pps is unknown. Estimated rates are labeled. The previous period has the exact same duration, resolution and selected agents; percentage comparisons are withheld for incomplete coverage, changed response cohorts, unknown measurements or a zero baseline.
|
||||
- Port ACL edit and import forms now use real React Hook Form, zodResolver, Controller/register, FieldError and the existing FormSheet. The previous live manual field state/submit handling was removed. Schema validation still canonicalizes the request at mutation time. Pending writes lock form dismissal; dirty dismissal requires discarding the draft; catalog refreshes do not reset edits. Imports validate source selection and strict ranges before submitting.
|
||||
- Explain and override/clone forms load lazily, preloaded on button focus/pointer intent. A loading status is accessible. After first opening, components stay mounted when closed so draft state is preserved; forms are keyed to the current agent.
|
||||
|
||||
### F1 verification
|
||||
|
||||
- TypeScript and production build pass; existing registry `use no memo` and source-map warnings remain. **34 Vitest tests in 12 files** pass. New cases cover Explain transport pairing/unknown semantics, expiry bounds/field exclusivity, import validation, complete-coverage rate gating and zero/unknown comparison baselines.
|
||||
- Parent browser verified `/test/f1.html`: **16/16 PASS**, alongside the earlier 23-check harness. It exercises invalid/reversed Port ACL input, a valid submit, dirty catalog refresh, invalid/valid import, keyboard combobox opening and name selection, Explain invalid input/unknown result/dirty-result labeling, expiry metadata, invalid TTL and failed/successful override writes with draft preservation.
|
||||
- The development F1 harness exposes React Profiler and fixture request counts in `#metrics`. The parent-observed run reported 156 commits and 436.2000000476837 ms total `actualDuration`; fixture counts were GET agents 1, GET explain 1, GET overrides 2, POST overrides 2 (one rejected, one successful). These describe that current development fixture run only, with no before/after performance conclusion.
|
||||
- The parent also exercised Explain against the isolated real QA API, including after a 60-second TTL expired: the desired result and older applied acknowledgment remained separate and external uncertainty was visible. The form issued no writes.
|
||||
- Targeted new-code ReUI C1–C4 checks pass without new animation, native table/select, raw state colors or overlay markup. No dependencies, lockfiles, registry internals or backend/shared files were edited in this batch. Lint remains unavailable because no web lint script is configured. Responsive screenshots, HAR and browser-downloaded CSV file inspection remain unclaimed. Independent Astra review follows this bounded checkpoint.
|
||||
- Final F1 build measurement: agent detail route 31.04 KB / 9.20 KB gzip; lazy Explain 11.81 KB / 4.47 KB gzip; lazy override/clone settings 9.70 KB / 3.72 KB gzip; ACL chunk 21.94 KB / 7.10 KB gzip; stats route 21.92 KB / 8.50 KB gzip; CSS 176.47 KB / 27.13 KB gzip. Within this F1 batch, moving the rare forms out of the eager agent route reduced that route chunk from 49.39 KB / 14.18 KB gzip before the boundary change. This is a chunk-level observation, not an initial-page network result.
|
||||
- `node scripts/bench/bundle.mjs` on Node **v25.5.0** measured all built JS/CSS assets across routes as **1,855,385 bytes**, **569,103 bytes gzip**. It is not initial transfer, a production baseline, a HAR trace or a performance-improvement claim. Docker's Node 22 runtime remains separately unverified in this frontend checkpoint.
|
||||
|
||||
## F2/F3 snapshots, webhooks, scoped credentials and materialized groups
|
||||
|
||||
- Existing rules, settings and agent layouts remain. Snapshots/canary, webhook administration, credential lifecycle and labels/group-assignment forms load lazily and preload on focus/pointer intent. Table-heavy forms use the FormSheet wrapper's explicit wide size with min-width containment; the registry Sheet/DataGrid/Frame implementations are preserved.
|
||||
- Snapshot creation selects a named staging agent. Target and canary selectors show names, cap selections at 100 and restrict canaries to selected targets. A conflict-free diff for the exact current snapshot and complete target set is required before publication. Creation sends the exact preview generations; confirm/rollback use the displayed rollout version, and release uses the desired revision. A successful publication remains pending until fresh exact server ACK evidence appears. Confirmation is always manual. Rollback describes a newly published prior desired policy, without asserting kernel success. Pin release warns that current live policy is validated before resuming it. Snapshot/history selectors explicitly show the latest 50 records.
|
||||
- Webhooks require settings-admin permission, display endpoint state and pending/dead/dropped queue health, and provide create/edit/delete plus endpoint deliveries. New endpoints default disabled. Secrets use a password field, require 32–256 characters on creation and are omitted when unchanged on edit. Editing maps only editable fields into the strict RHF schema; the mounted harness caught and verified the fix for metadata accidentally blocking PATCH. Missing encryption configuration disables writes with an explanation. No real webhook was sent during QA.
|
||||
- Credential status uses the shared runtime schema. A rotation request is separate from agent exchange/completion, requires supported approved agents and explicit confirmation, and displays request expiry. Install-link lifecycle metadata shows expiry, consumption and revocation; the panel omits commands/URLs from its query cache and displays no tokens or credentials. New-agent copy explains single use and a 24-hour expiry. Settings render the signing master as a write-only password, preserve the mask and explain that replacement invalidates pending invitation credentials; the settings cache is masked after saving.
|
||||
- Label metadata supports create/edit/delete; agent label drafts survive refetch and rejected writes. Group policy assignment selects either explicit named agents or an AND label selection, with explicit add/replace modes and ordered set names. Preview resolves the materialized cohort; apply uses its exact agent IDs/generations and requires a separate confirmation. Changes to labels do not dynamically change set assignments. A rejected CAS invalidates the preview, requiring a new one.
|
||||
- Large list detail and its breadcrumb loader use the shared bounded page contract, query keys include page/search/filters and requests forward AbortSignal. Catalog reads request metadata-only summary view. ResourcePage manual pagination uses server totals and disables page-local filtering/sorting. Search/filtering covers the full SQL collection; a same-list previous page stays visible while updating to preserve typing focus. POST/DELETE entry mutations request and runtime-validate the bounded summary response. A mixed valid/invalid import is blocked inline before any request, preserving the draft; valid duplicates can be normalized. Truncated resolved-CIDR previews keep the full count and an explicit shortened-preview label.
|
||||
|
||||
### F2/F3 verification checkpoint
|
||||
|
||||
- **42 Vitest tests in 14 files** pass, including snapshot/diff identity and canary-ACK action gates, write-only secret omission, link lifecycle, all-or-nothing import validation, bounded query parameters/cancellation and a 100,000-row server-pagination rendering regression. TypeScript and the production build pass, with the existing registry directive/source-map warnings. No web lint script is configured.
|
||||
- Parent browser confirmed **21/21 mounted checks** at `/test/f2.html`, alongside the earlier 23-check and F1 16-check harnesses. F2 covers missing webhook key/admin permission, inline secret validation, failed/successful writes, blank-secret edit, dirty label refetch/rejected save, group preview/explicit CAS, diff-before-canary, pending vs fresh ACK, manual confirm/version, release warning and rotation request status. Every request is fixture-intercepted; no external webhook is delivered. Failed harness runs keep the mounted fixture and fake fetch for inspection.
|
||||
- Parent also exercised the real isolated QA API through the UI: captured a snapshot, previewed two targets, published only the canary, supplied a controlled telemetry ACK, manually confirmed the remaining target and rolled back. Applied revisions/unknown values remained separate from desired revisions and pending ACK. Desktop Sheet width was corrected from the inherited 384px constraint to the wrapper's wide size; subsequent mobile/responsive evidence is recorded separately by the parent.
|
||||
- ReUI audit checklist and focused C1–C4 checks were reviewed. New code uses installed Frame/ResourcePage/FormSheet/Combobox/Alert/Badge controls, semantic variants, no new animation, raw state palette, native table/select or custom overlay. Wall-clock reads are limited to credential/link expiry semantics and documented in that file. No dependency or lockfile changes. Node 22 Docker, production fleet apply behavior, HAR traces and performance improvements are not inferred from these local checks.
|
||||
- Final F2/F3 production measurement on Node **v25.5.0**: all built JS/CSS assets **1,908,807 bytes / 590,263 bytes gzip**. Compared with the parent's pristine `aa475848` measurement **1,785,684 / 539,471**, this is **+6.90% / +9.42%**; compared with F1 **1,855,385 / 569,103**, **+2.88% / +3.72%**. The document-referenced static bootstrap assets (entry script, modulepreloads, stylesheet; not a HAR or route-complete transfer) total **871,972 / 243,973**, versus pristine **858,774 / 240,019** (**+1.54% / +1.65%**) and F1 **867,475 / 242,654** (**+0.52% / +0.54%**). Agent detail is **32,300 / 9,519**; lazy new forms: labels **12,034 / 4,587**, rollouts **11,461 / 4,493**, webhooks **9,246 / 3,694**, credentials **5,550 / 2,546**. These are bundle-size observations; added features increased total assets.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Implementation checkpoint
|
||||
|
||||
Prerequisites unavailable here: Docker/Linux runtime, production-volume clone and RouterOS hardware. The user has no separate test stand; native checks are wired into CI and remain pending actual execution. Implementation and local verification cover F1/F2/F3, including a browser check of the 100k-entry paged list. Astra accepted the final feature/security package and benchmark CI wiring, with no open confirmed P1/P2 blockers. Local implementation is accepted; native release acceptance remains pending.
|
||||
|
||||
Root's final local suites passed: API 281 tests, shared 22, frontend 42, with 21 mounted UI checks recorded. The earlier 274-test checkpoint is superseded. The final fresh Windows in-memory run passed all 1611 requests, zero 429s, all raw/fleet/deduplication assertions and the bounded 100k import summary. Its separated steady profile observed new-ingestion p95 11.09ms and timeseries p95 8.07ms; the unconstrained host is not native target-host acceptance. Earlier mixed-profile results and full measurement limitations are documented in [runtime-validation.md](runtime-validation.md).
|
||||
|
||||
| Stage | Scope | Status | Acceptance still required |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | Baseline, preparation, ReUI | partial | ReUI skill updated to `68f0e6fa91`; root confirmed premium CLI `@reui/settings-2 --dry-run` passed (10 hypothetical files; no installation). Run reproducible baseline on Linux; collect initial-route/browser measurements and registry evidence |
|
||||
| 1 | Security and data preservation | implemented, local checks passed | Representative native DB upgrade, production recovery rehearsal and matching pre-upgrade backup procedure remain pending |
|
||||
| 2 | Reliable policy application | implemented, local checks passed | Linux CI and RouterOS application/failure/recovery against real packets and agents remain pending |
|
||||
| 3 | Sources and policy evaluation | implemented, local checks passed | Representative live-source and long-duration capacity checks remain pending |
|
||||
| 4 | Trustworthy telemetry | implemented, local checks passed | Counter resets, timestamps and mixed-agent versions against native traffic remain pending |
|
||||
| 5 | Analytics and storage | implemented, native acceptance pending | Local analytics/coverage/retry checks exist; long-duration retention, DB growth and Linux capacity measurements remain pending |
|
||||
| 6 | Frontend speed and ReUI | implemented, local/browser checks recorded | Native network/navigation measurements remain pending; asset growth is disclosed below |
|
||||
| 7 | Operations and Docker | implemented, CI wired | Actual built-image smoke, previous-image upgrade, recovery, digest provenance and constrained performance result remain pending CI/Linux |
|
||||
| 8 | New F1/F2/F3 features | implemented, local checks passed | F1 diagnostics/source health/explain IP-port/analytics CSV; F2 TTL overrides/snapshots/canary/rollback/webhooks/one-time enrollment/token rotation; F3 labels/preview and guarded group assignment/IPv6. Native agents still require CI/hardware |
|
||||
|
||||
Operations now include identity-scoped rate limits, durable bounded audit and webhook outboxes, secret masking, fail-closed encrypted webhook storage, DNS-pinned HTTPS delivery, online SQLite backup/restore tooling, generated contract drift checks, per-run Docker builders with effective GC verification and a digest-pinned previous-image release upgrade gate. Review fixed queued-webhook cancellation after endpoint changes and signing-master exposure through settings/install context. The quality workflow now invokes the real benchmark harness after smoke against the exact same immutable image ID, with a disposable 2-vCPU/4GB container and a 500ms steady ingestion/timeseries p95 gate. Relevant PRs and every CD quality invocation run this job; a failed native benchmark blocks release.
|
||||
|
||||
Root's browser check loaded the 100000-entry global list as a 100-row page, then searched the final entry `11.1.134.159/32`: exactly one result appeared and input focus remained intact. This confirms whole-list server filtering and bounded rendering for that scenario.
|
||||
|
||||
Final build artifacts: all JS/CSS assets 1908807 bytes raw / 590263 bytes gzip (+6.90% / +9.42% from the recorded baseline); bootstrap 871972 / 243973 bytes (+1.54% / +1.65%). Gzip figures are generated artifact sizes, not measured network transfer. Features increased total assets; no bundle-size reduction is claimed.
|
||||
|
||||
The Linux CI harness creates fresh network/mount/PID namespaces for nft and ipset independently, exercises IPv4/IPv6 traffic denial, drift repair, invalid-candidate preservation and unrelated rules. Its control-plane fixture now returns the exact report ID and sequence required by the durable telemetry sender; a pure acknowledgement-contract regression passes locally. Shell/Python validation is not native execution. Built-image startup/native SQLite, backup/upgrade, resource baselines, Docker-network traffic and RouterOS remain explicit pending gates.
|
||||
|
||||
Backend implementation/checks: [backend-hardening.md](backend-hardening.md). Frontend implementation/checks: [frontend-validation.md](frontend-validation.md). Runtime commands, actual constraints and pending gates: [runtime-validation.md](runtime-validation.md). Update this checkpoint only with observed integration evidence; a unit test alone does not complete a stage.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Bounded list import response
|
||||
|
||||
`POST /api/v1/lists/:id/entries?return=summary` opts into a bounded response:
|
||||
|
||||
```json
|
||||
{"count":100000,"content_hash":"sha256:...","changed":true}
|
||||
```
|
||||
|
||||
`count` is the total stored effective CIDR count after publication. `content_hash` is the stored content hash; `changed` records whether the root list's effective memberships changed. The response is derived from a SQL count and hash projection and does not serialize stored rows, source config or full list detail.
|
||||
|
||||
The opt-in path accepts a fully duplicate valid import as a successful no-op with `changed:false`, preserving agent generations. Mixed valid/invalid input still rejects the entire request before publication. With no query parameter the existing full-detail success response and duplicate-import error behavior remain unchanged. The existing 100000-entry and 8 MiB import bounds apply to both modes.
|
||||
|
||||
The frontend import/delete mutations use summary responses and invalidate list queries. `DELETE /lists/:id/entries?return=summary` returns the same summary after atomic deletion. Default delete responses remain full details.
|
||||
|
||||
`GET /lists/:id?view=page&limit=100&offset=0` opts into SQL pagination (limit 1–200, offset 0–10000000). Without query parameters the legacy full detail remains available. Invalid paging/filter parameters reject the request instead of falling back to the full response. Paged responses contain metadata, bounded `items` and preview `entries`, total stored `entry_count`, filtered logical-item `total`, `limit` and `offset`. `config_json` is `{}`; raw source config is not returned. Each item contains the true `resolved_count`, at most three `resolved_cidrs` previews and `resolved_truncated`.
|
||||
|
||||
Optional `q` searches value/name and kind. `filters` is JSON for at most ten filters: `{field:'value'|'kind',operator,values:string[]}`. Operators match the frontend's `is`, `is_not`, `is_any_of`, `is_not_any_of`, `contains`, `not_contains`, `starts_with`, `ends_with`, `empty` and `not_empty`. Filters combine with AND, values with OR. Filtering occurs over the entire list before pagination; nested-list value text includes its child name and ID. The deterministic SQLite `evofw_lower` function preserves JavaScript Unicode case folding for names.
|
||||
|
||||
Manual source items are projected through SQLite JSON1 and external CIDRs through `ip_list_entries`; full rows/config/resolution arrays are not materialized in JavaScript before slicing. `GET /lists?view=summary` also excludes source config from the metadata catalog query. Defaults for both endpoints preserve legacy behavior.
|
||||
|
||||
Observed Windows/in-memory endpoint test: 100000 unique IPv4 inputs returned 120 response bytes in approximately 1363 ms. A 200-item page from that list returned 31048 bytes; an exact filter for the 100000th item returned 524 bytes. These are local samples, not production or two-CPU capacity claims. Tests also cover full duplicate no-op without generation bumps, invalid mixed input preserving count/hash/revision, default legacy detail shape, bad paging/filter parameters, external-prefix pagination, bounded nested-list resolution previews and Unicode child-name filtering.
|
||||
File diff suppressed because it is too large.
Load diff
+214
-39
@@ -12,6 +12,98 @@ servers:
|
||||
description: Local API
|
||||
security: []
|
||||
paths:
|
||||
/v1/agent/credentials:
|
||||
$ref: './openapi.schemas.json#/paths/~1v1~1agent~1credentials'
|
||||
/v1/agent/token-rotation:
|
||||
$ref: './openapi.schemas.json#/paths/~1v1~1agent~1token-rotation'
|
||||
/api/v1/agents/{id}/token-rotation:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agents~1{id}~1token-rotation'
|
||||
/api/v1/policies/snapshots:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1snapshots'
|
||||
/api/v1/policies/snapshots/{id}:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1snapshots~1{id}'
|
||||
/api/v1/policies/snapshots/{id}/diff:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1snapshots~1{id}~1diff'
|
||||
/api/v1/policies/rollouts:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1rollouts'
|
||||
/api/v1/policies/rollouts/{id}:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1rollouts~1{id}'
|
||||
/api/v1/policies/rollouts/{id}/confirm:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1rollouts~1{id}~1confirm'
|
||||
/api/v1/policies/rollouts/{id}/rollback:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1policies~1rollouts~1{id}~1rollback'
|
||||
/api/v1/agents/{id}/policy-pin/release:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agents~1{id}~1policy-pin~1release'
|
||||
/api/v1/agent-labels:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agent-labels'
|
||||
/api/v1/agent-labels/{id}:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agent-labels~1{id}'
|
||||
/api/v1/agents/{id}/labels:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agents~1{id}~1labels'
|
||||
/api/v1/agent-labels/preview-assignment:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agent-labels~1preview-assignment'
|
||||
/api/v1/agent-labels/assign-policy-sets:
|
||||
$ref: './openapi.schemas.json#/paths/~1api~1v1~1agent-labels~1assign-policy-sets'
|
||||
/api/v1/webhooks:
|
||||
get:
|
||||
summary: List webhook endpoints and bounded queue counts (settings admin)
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: Endpoint metadata without secrets, configured flag, limits and queue counts
|
||||
'403':
|
||||
description: Settings administrator required
|
||||
post:
|
||||
summary: Create webhook endpoint (at most 20, settings admin)
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './openapi.schemas.json#/components/schemas/CreateWebhook'
|
||||
responses:
|
||||
'201':
|
||||
description: Endpoint metadata with secret_configured; secret never returned
|
||||
'503':
|
||||
description: EVOFW_SECRET_KEY missing or shorter than 32 characters
|
||||
/api/v1/webhooks/{id}:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Id'
|
||||
patch:
|
||||
summary: Update webhook endpoint; changed destination, signing secret or events cancel pending delivery
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './openapi.schemas.json#/components/schemas/PatchWebhook'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated endpoint metadata without secret
|
||||
'503':
|
||||
description: EVOFW_SECRET_KEY unavailable
|
||||
delete:
|
||||
summary: Delete endpoint and cancel queued deliveries (settings admin)
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: Deleted
|
||||
/api/v1/webhooks/{id}/deliveries:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Id'
|
||||
get:
|
||||
summary: Last 100 pending or terminal deliveries, with sanitized error codes (settings admin)
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: Items and total count, excluding payload bytes and signing secrets
|
||||
/health:
|
||||
get:
|
||||
summary: Liveness
|
||||
@@ -20,6 +112,42 @@ paths:
|
||||
'200':
|
||||
description: OK
|
||||
|
||||
/ready:
|
||||
get:
|
||||
summary: SQLite-backed readiness
|
||||
tags: [system]
|
||||
responses:
|
||||
'200':
|
||||
description: Ready
|
||||
'503':
|
||||
description: Database unavailable
|
||||
|
||||
/api/v1/diagnostics:
|
||||
get:
|
||||
summary: Bounded runtime and delivery queue diagnostics
|
||||
description: Requires fw:settings:admin. Returns DB/WAL sizes, filesystem capacity, audit outbox and limiter counters, RSS/heap and uptime. Memory databases and unavailable filesystem metrics return null; no credentials or queued payloads are returned.
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: Runtime diagnostics
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [database, audit_outbox, limiter, process]
|
||||
properties:
|
||||
database: { type: object, additionalProperties: true }
|
||||
audit_outbox: { type: object, additionalProperties: true }
|
||||
limiter: { type: object, additionalProperties: true }
|
||||
process: { type: object, additionalProperties: true }
|
||||
'401':
|
||||
description: Authentication required
|
||||
'403':
|
||||
description: Missing settings administrator permission
|
||||
'429':
|
||||
description: Authenticated user request limit exceeded
|
||||
|
||||
/api/v1/dashboard:
|
||||
get:
|
||||
summary: Dashboard KPI
|
||||
@@ -29,6 +157,37 @@ paths:
|
||||
'200':
|
||||
description: Stats
|
||||
|
||||
/api/v1/ui/summary:
|
||||
get:
|
||||
summary: Permission-filtered navigation and dashboard summary
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
responses:
|
||||
'200':
|
||||
description: Only sections visible to the authenticated user are included
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './openapi.schemas.json#/components/schemas/UiSummary'
|
||||
|
||||
/api/v1/stats/timeseries:
|
||||
get:
|
||||
summary: Coverage-aware fleet or agent packet series
|
||||
tags: [ops]
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: from, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: to, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: resolution, in: query, schema: { type: string, enum: [1m, 1h, 1d] } }
|
||||
- { name: agent_ids, in: query, description: Comma-separated agent IDs (at most 100), schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: At most 1440 points and 100 agents; unknown counters are null
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './openapi.schemas.json#/components/schemas/TimeseriesResponse'
|
||||
|
||||
/api/v1/install-context:
|
||||
get:
|
||||
summary: Public install base URL + enroll seed hint
|
||||
@@ -229,6 +388,10 @@ paths:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- name: view
|
||||
in: query
|
||||
description: Select summary to omit expanded source configuration and entries
|
||||
schema: { type: string, enum: [summary] }
|
||||
responses:
|
||||
'200':
|
||||
description: Lists (items + total)
|
||||
@@ -242,14 +405,30 @@ paths:
|
||||
|
||||
/api/v1/lists/{id}:
|
||||
get:
|
||||
summary: List detail + entries
|
||||
summary: List detail; use view=page for bounded entries and whole-list filtering
|
||||
tags: [lists]
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Id'
|
||||
- { name: view, in: query, schema: { type: string, enum: [page] } }
|
||||
- { name: limit, in: query, description: Requires view=page, schema: { type: integer, minimum: 1, maximum: 200, default: 100 } }
|
||||
- { name: offset, in: query, description: Requires view=page, schema: { type: integer, minimum: 0, maximum: 10000000, default: 0 } }
|
||||
- { name: q, in: query, description: Whole-list text search before pagination; requires view=page, schema: { type: string, maxLength: 256 } }
|
||||
- name: filters
|
||||
in: query
|
||||
description: >-
|
||||
JSON array (at most 10 filters), evaluated over the whole list before paging.
|
||||
Each filter has field value or kind, operator is/is_not/is_any_of/is_not_any_of/
|
||||
contains/not_contains/starts_with/ends_with/empty/not_empty and values array
|
||||
(at most 20 strings of at most 256 characters). Requires view=page.
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: Detail
|
||||
description: >-
|
||||
Default retains full legacy detail. view=page returns metadata plus total,
|
||||
limit, offset, entries and items (at most limit). Each item has kind,
|
||||
value, list_name, resolved_count, resolved_cidrs and resolved_truncated;
|
||||
nested resolved previews are bounded. Invalid page/filter input returns 400.
|
||||
delete:
|
||||
summary: Delete list
|
||||
tags: [lists]
|
||||
@@ -278,15 +457,23 @@ paths:
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Id'
|
||||
- name: return
|
||||
in: query
|
||||
schema: { type: string, enum: [summary] }
|
||||
description: Opt in to bounded count/content_hash/changed response instead of expanded detail
|
||||
responses:
|
||||
'200':
|
||||
description: Updated
|
||||
description: Updated; return=summary uses ListImportSummary from generated contracts
|
||||
delete:
|
||||
summary: Delete entry
|
||||
tags: [lists]
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Id'
|
||||
- name: return
|
||||
in: query
|
||||
schema: { type: string, enum: [summary] }
|
||||
description: Opt in to bounded count/content_hash/changed response instead of expanded detail
|
||||
responses:
|
||||
'200':
|
||||
description: Deleted
|
||||
@@ -748,11 +935,28 @@ paths:
|
||||
|
||||
/v1/agent/enroll:
|
||||
post:
|
||||
summary: Enroll agent (X-EvoFW-Seed)
|
||||
summary: Enroll with single-use install-link credential (legacy global seed refused)
|
||||
tags: [agent-public]
|
||||
parameters:
|
||||
- name: X-EvoFW-Enrollment-Credential
|
||||
in: header
|
||||
required: true
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './openapi.schemas.json#/components/schemas/EnrollWithCredential'
|
||||
responses:
|
||||
'200':
|
||||
description: Exact consumed enrollment retry; same client; no-store
|
||||
'201':
|
||||
description: Pending agent
|
||||
description: Newly enrolled pending agent; no-store
|
||||
'401':
|
||||
description: Scoped enrollment credential required
|
||||
'409':
|
||||
description: Credential already consumed with different enrollment
|
||||
|
||||
/v1/agent/policy:
|
||||
get:
|
||||
@@ -762,6 +966,10 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Policy (includes script_sha256 for Linux self-update; not part of policy.hash)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './openapi.schemas.json#/components/schemas/AgentPolicy'
|
||||
|
||||
/v1/agent/policy.rsc:
|
||||
get:
|
||||
@@ -791,40 +999,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status: { type: string }
|
||||
prefix_count: { type: integer }
|
||||
packets_dropped: { type: integer }
|
||||
packets_accepted: { type: integer }
|
||||
kernel_method: { type: string }
|
||||
error: { type: string }
|
||||
source: { type: string }
|
||||
ip_hits:
|
||||
type: array
|
||||
maxItems: 200
|
||||
description: Linux per-element drop counters (packets > 0)
|
||||
items:
|
||||
type: object
|
||||
required: [ip, packets]
|
||||
properties:
|
||||
ip: { type: string, maxLength: 64 }
|
||||
packets: { type: integer, minimum: 0 }
|
||||
port_hits:
|
||||
type: array
|
||||
maxItems: 500
|
||||
description: Linux nft deny_port_hits (ip × proto × dport, packets > 0)
|
||||
items:
|
||||
type: object
|
||||
required: [ip, port, protocol, packets]
|
||||
properties:
|
||||
ip: { type: string, maxLength: 64 }
|
||||
port: { type: integer, minimum: 1, maximum: 65535 }
|
||||
protocol: { type: string, enum: [tcp, udp] }
|
||||
packets: { type: integer, minimum: 0 }
|
||||
host_firewall:
|
||||
$ref: '#/components/schemas/HostFirewallPayload'
|
||||
$ref: './openapi.schemas.json#/components/schemas/ApplyReport'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Operational checkpoint
|
||||
|
||||
The deployment remains one Fastify/SPA/SQLite container. On 2026-10-26 Docker CLI is absent; `wsl.exe` exists but reports that WSL is not installed. Linux kernel execution and container resource measurements are pending CI. No throughput or memory improvement is claimed from request-injection tests.
|
||||
|
||||
## Authentication, permissions and limits
|
||||
|
||||
The operational plugin assigns route security metadata at registration: public, authenticated agent, protected user or the explicit `GET/HEAD /api/v1/ui/summary` section-filtered exception. A new `/api/*` route without a permission mapping or a new `/v1/*` agent route without an authentication mapping fails startup. The catalog coverage regression includes raw statistics, CSV permissions and agent explain. UI summary still filters each section by the caller's permissions. Diagnostics requires `fw:settings:admin`.
|
||||
|
||||
Successful agent requests are limited to 120/minute **per authenticated agent ID**, user requests to 600/minute per verified JWT subject, enrollment to 20/minute per source IP and remaining public API requests to 120/minute per IP. Limits are checked after authentication; users and agents behind one NAT do not share their successful-request bucket. Authentication failures are counted separately per IP: after 60 failures/minute, invalid requests return 429. A valid agent behind that same NAT remains usable. Verification still performs the indexed token lookup or JWT check before rejecting an invalid request, so this is not a volumetric network-DOS defense. Both maps cap identity buckets at 10,000 and reclaim expired windows before admitting more identities. Rejection includes `Retry-After: 60`.
|
||||
|
||||
Proxy headers are ignored by default. Configure only the actual reverse proxy IP/CIDRs, for example `TRUST_PROXY_CIDRS=127.0.0.1/32,10.20.0.10/32`; arbitrary truthy strings/hostnames are rejected. Audit uses Fastify's derived `request.ip`, never an unchecked first `X-Forwarded-For` value. Incorrectly trusting a whole client network permits spoofed IPs, so keep the list narrow.
|
||||
|
||||
Unknown `/api`, `/api/*`, `/v1` and `/v1/*` requests return JSON 404 even when SPA history fallback is enabled. Browser routes continue to serve the SPA.
|
||||
|
||||
## Durable audit delivery
|
||||
|
||||
Migration `014_audit_outbox.sql` creates a raw SQLite delivery table. Local audit append and outbox enqueue use one SQLite transaction. A single worker sends at most 25 events per batch every five seconds; it allows one active batch, uses an eight-second HTTP timeout, refuses redirects and records only a sanitized status/error. Success deletes the acknowledged batch. Failure retains event IDs and payload bytes and schedules exponential retry from five seconds up to one hour, without a retry-count deletion policy. Restart resumes persisted rows. Shutdown aborts the active HTTP request before DB closure and retains unacknowledged events.
|
||||
|
||||
Queue capacity is 10,000 events, with at most 16KiB per event and 8KiB bounded details. Settings audits contain only changed key names; values, tokens and passwords are excluded. Defensive key redaction also applies to other mutation details. An enabled full queue rejects protected writes with 503 before the handler. If disk failure or a race with an already-running asynchronous mutation prevents enqueue, local audit preservation is attempted, an error is logged and `enqueue_failures` increments; such an event needs operator recovery from the local audit log. Previously queued events are retained. This explicit failure mode does not promise delivery after a full disk.
|
||||
|
||||
Every enqueue validates the shared `ingestAuditEventSchema`: summaries are capped at 500 characters and invalid placeholder emails such as `dev@local` become null. Stored legacy events are normalized and validated again before delivery. Unrepairable JSON/contract/event-ID mismatches are quarantined individually, so a malformed row cannot poison a healthy batch. Quarantine retains at most 100 delivery rows and exposes only counts and a sanitized recovery instruction in diagnostics. Oldest excess delivery rows are removed while their original local audit records remain; operator repair uses that local log. Known legacy overlong summaries/invalid emails are repaired in place and retain the original event ID.
|
||||
|
||||
`GET /api/v1/diagnostics` reports DB/WAL byte sizes, available/total filesystem capacity, outbox volume/age/retry/capacity/enqueue failures, limiter counts, uptime and RSS/heap. It never returns queued payloads, secrets or complete filesystem paths. Unavailable metrics are null. This bounds the audit delivery queue and request buckets; Pino/stdout buffering is unchanged. Compose log rotation bounds stored container logs, and a stalled stdout consumer remains an operational limitation.
|
||||
|
||||
## Verification and Linux rehearsal
|
||||
|
||||
The in-process NAT regression sends 800 requests from 100 authenticated agents behind one source IP: three v2 reports with exact retries, policy reads and concurrent summary reads. It requires every response to be 200 and verifies retries do not double-count totals. The standalone Docker baseline runner separates a 1000-request cold/burst sweep from a warmed, paced 400-request steady profile. The steady phase adds a fourth unique report per agent with exact retry; final checks require four raw rows, 300/600 per-agent deltas and 30000/60000 fleet totals. It then imports 100k prefixes and requires a bounded summary response. The constrained Docker harness enforces the optional 500ms steady ingestion/timeseries p95 target; unconstrained Windows runs report observations without claiming target acceptance. Every HTTP error including 429 fails. It remains pending execution against a built Linux image.
|
||||
|
||||
## F2 webhooks implementation checkpoint
|
||||
|
||||
Migration `016_webhooks.sql` implements a separate outbox. Scope: settings administrators manage endpoints; consumers receive versioned event envelopes for agent offline/recovered, apply failure/recovery and source refresh failure/recovery. Payloads contain event ID, event type, occurrence time, agent/list ID and sanitized status codes, never tokens, source URLs, settings values or raw apply errors. Endpoint secrets are write-only, rotated explicitly and omitted from API responses, audit details, diagnostics and logs.
|
||||
|
||||
Use a separate durable outbox and single bounded worker polled every 30 seconds, preserving local controller writes when webhook delivery fails. Limits: 20 endpoints, 1000 pending rows globally, 16KiB encoded body, one active request, five deliveries per batch, eight-second deadline and 64KiB maximum response consumption. Persist the exact signed payload bytes and event ID for at-least-once retries; consumers deduplicate by ID. Failures retry with exponential backoff and jitter up to one hour, at most 12 attempts/24 hours before a bounded dead-letter state. Expose only counts, oldest age and sanitized error codes. Capacity exhaustion increments a dropped-notification counter and emits a local audit warning; it must not block agent telemetry or policy application. Retain at most 100 dead letters; no unbounded retry history.
|
||||
|
||||
Persist a five-minute cooldown per endpoint/event type/resource, with transition detection so repeated identical failures are suppressed and recovery is emitted once. Commit transition state and enqueue atomically to avoid duplicate storms after restart. A failed delivery does not restart cooldown. Endpoint deletion cancels queued deliveries; secret rotation defines whether old queued messages retain their original signing key (proposed: discard pending messages and audit the count, never retain retired secrets).
|
||||
|
||||
Sign `timestamp + '.' + raw_body` with HMAC-SHA256; send version, timestamp, event ID and signature in dedicated headers. Verify timestamps within five minutes and use constant-time comparison at receivers. Retry uses the stable event ID/body with a fresh timestamp/signature. Secrets require the existing `EVOFW_SECRET_KEY` (at least 32 characters), with AES-256-GCM encryption and endpoint-ID authenticated context; there is no plaintext fallback. Missing/weak key disables delivery and makes create/update return clear 503. Back up both the DB and current key securely before rotation. Existing ciphertext must be decrypted using the old key and re-encrypted with the new key during offline maintenance, or explicitly replace each endpoint signing secret under the new key; changing only the environment key prevents existing secrets from decrypting. Keep the old key with its matching backups for recovery.
|
||||
|
||||
POST transport must support DNS pinning specifically: HTTPS only, no userinfo/fragments, default port 443, no redirects, validate every resolved address against public ranges, select one validated address and pin the socket lookup while keeping TLS SNI/certificate verification for the hostname. Revalidate on every retry. The existing source GET transport must not be reused blindly: its configured internal-origin exception must never enable webhooks. No cross-origin forwarding of authorization, proxy inheritance or arbitrary request headers. Tests must cover DNS rebinding, mixed public/private answers, IPv4-mapped IPv6, redirects, timeouts, restart replay, cooldown and capacity pressure before enabling delivery.
|
||||
|
||||
Native RouterOS validation remains a separate release gate on an explicitly configured disposable router/runner: install the shipped script, apply IPv4 and IPv6 policies, verify counters, retry reports, drift repair and preservation of foreign rules. No RouterOS endpoint or credentials are configured here, so neither a static RSC check nor Linux CI counts as that gate passing. The existing `linux-agent-smoke` workflow is scheduled on API/Docker changes and runs the shipped agent in isolated namespaces; its result still requires an actual CI run.
|
||||
|
||||
```sh
|
||||
# From apps/api
|
||||
node node_modules/vitest/vitest.mjs run src/services/operational.test.ts src/services/audit.test.ts
|
||||
# From repository root, on a disposable Linux runner with namespace privileges
|
||||
sudo bash scripts/ci/linux-netns-smoke.sh
|
||||
sh scripts/bench/runtime.sh IMAGE runtime-artifacts/candidate
|
||||
```
|
||||
|
||||
The Linux smoke always creates network, mount and PID namespaces before any firewall mutation; it verifies the namespace IDs differ from the parent, makes mounts private and uses temporary `/etc`, state, log and `/run` mounts. A nested client namespace and veth pair exercise real packet denial. It runs the shipped agent against a local fixture, checks drift repair, invalid-candidate preservation and an unrelated nft table. It never applies policies to the host firewall. CI installs prerequisites and runs this script; unavailable namespace capabilities fail the job rather than reporting a skipped check as success. CI now executes separate isolated nft and ipset namespaces with both IPv4 and IPv6 veth HTTP traffic: allow-before-policy, deny-after-policy, drift repair, invalid-candidate preservation and foreign-table preservation. Actual execution is pending CI; Docker-network and RouterOS matrices remain separate. Local validation covers shell/Python syntax only; installation of WSL or host reboot is not part of this work.
|
||||
|
||||
Validation: 12 focused webhook tests passed (7 durable-service, 3 mocked transport, 2 route/RBAC tests). They cover encryption, missing-key 503, redacted DTOs, restart/retry byte identity, cooldown recovery, endpoint/outbox/dead-letter bounds, mixed/private DNS, socket pinning, redirect refusal and shutdown cancellation. No real external webhook was sent. Native Docker/Linux/RouterOS validation remains pending CI or an explicitly configured runner.
|
||||
|
||||
Webhook cancellation review: endpoint delivery changes increment a persistent generation and cancel queued rows. Each batch candidate is re-read immediately before sending and must still be pending, enabled and match that generation. A request already in progress may finish; canceled unsent bytes cannot be moved to a changed URL/secret. Two paused-transport regressions cover URL/secret change and disable while the first POST is in flight.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Policy publication bounds and conditional polling
|
||||
|
||||
The compiler and agent contract uses shared `POLICY_BUDGET`: 50000 effective network memberships, 200000 raw memberships, 1000 serialized Port ACL rules, and an 8 MiB JSON response. The error code is `POLICY_BUDGET_EXCEEDED`. Importing a 100000-entry source list is still permitted; assigning an oversized effective policy is refused without truncation. The last successfully delivered desired hash/revision and the last applied acknowledgement are preserved on refusal.
|
||||
|
||||
Effective memberships are canonical unique CIDR strings in each serialized `deny_cidrs`, `allow_cidrs`, and individual `port_rules[].src_cidrs` array, before optional kernel CIDR collapse. Adjacent or overlapping but distinct networks count separately. A network repeated in distinct source sets counts for each set. `protocol:both` produces two serialized rules with the same logical ID and different protocols, and its source memberships count twice. Exact deny/allow duplicate conflicts are removed before the effective L3 count, preserving deny-wins semantics. Raw membership counting precedes deduplication and includes repeated source references and transport expansion. Source prefetches have a combined 200000-entry bound; compilation stops before publishing an over-budget candidate.
|
||||
|
||||
`src_kind:all` now means both `0.0.0.0/0` and `::/0`. An explicit IPv4 CIDR `0.0.0.0/0` remains IPv4-only. Agents without the required IPv6/ACL support must refuse the entire candidate and retain their last-good rules; the UI explains missing capabilities as unsupported until upgraded. There is no hidden IPv6 ND/RA exception.
|
||||
|
||||
`GET /v1/agent/policy` normally returns HTTP422 with the standard bounded error envelope for a compilation-budget refusal. A RouterOS fetch can opt into `?transport=routeros`: only this known refusal returns HTTP200 `{error:{code:"POLICY_BUDGET_EXCEEDED",message}}`, with no policy hash/generation, `Cache-Control:no-store`, and no ETag. `policyBudgetErrorEnvelopeSchema` is the shared explicit parser contract. Authentication and unexpected failures retain their actual status. Unknown transport values return400. Clients must inspect the error envelope before normalizing or importing a policy.
|
||||
|
||||
Each database owns a cache through a WeakMap. LRU limits are128 agents and32 MiB of conservative charged retained structures. Identical content is interned by content hash across agent entries; each entry retains its own generation, chain, summary, sync metadata and expiry. Eviction releases unused content references. Charge includes UTF16/string allowance, object/array headers and slots, map/entry overhead and metadata; repeated references within a representation are conservatively charged repeatedly. This is a structural memory budget rather than an allocator measurement. Temporary compilation/serialization work and the database are outside that cache budget. No serialized JSON response is permanently retained.
|
||||
|
||||
The cache key includes agent generation, default action and sync interval setting. List, hostname, set, rule, Port ACL and assignment mutations must publish their generation bumps. Expiry is processed transactionally before every cache lookup, and the earliest pending override expiry also bounds an entry's lifetime. Tests exercise actual list publication for a Port ACL-only dependency and hostname refresh, plus settings, revision and TTL invalidation.
|
||||
|
||||
JSON polling returns a strong versioned ETag over the exact complete response JSON, including generation, sync interval and script SHA. RouterOS script polling hashes the actual rendered script. `If-None-Match` can return304 with an empty body; authenticated contact and delivery metadata are still recorded. Returning to an earlier content hash after TTL expiry still produces a new ETag/HTTP200 because the generation changed. Clients without a conditional header continue receiving200. A client enabling304 must atomically retain a validated policy body and its matching ETag and reuse that body on304; merely retaining a hash is insufficient. Agent conditional-cache integration remains a separate script checkpoint.
|
||||
|
||||
V2 apply-report acknowledgements echo the pending `report_id` and `sequence` and include `sequence_floor`, the persisted highest accepted sequence. A restored agent receiving a matching authenticated stale-sequence ACK may advance its next sequence beyond that floor while marking lost coverage and rotating counter epochs. It must never rewrite a queued report. A floor equal to `Number.MAX_SAFE_INTEGER` is exhausted and cannot be incremented safely.
|
||||
|
||||
Checkpoint validation: the focused61-test suite includes ten compiler/cache tests, nine explain tests and telemetry/analytics/routes/integrity regressions; another27 existing policy/stats/list tests pass (88 distinct tests total). Shared/DB builds and API TypeScript check pass. The script owner validates parser limits, durable ACK recovery and subsequent304 bundle support separately; production kernel/RouterOS runtime acceptance and maximum-load profiling remain pending.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Explain and temporary overrides
|
||||
|
||||
## Explain endpoint
|
||||
|
||||
`GET /api/v1/agents/:id/explain` requires `fw:agents:read` (write/admin include read).
|
||||
|
||||
Query fields:
|
||||
|
||||
- `ip`: strict IPv4/IPv6 address, canonicalized by shared network parsing.
|
||||
- `port` and `protocol=tcp|udp`: supplied together, otherwise omitted.
|
||||
- `chain=input|forward`, default `input`.
|
||||
- `dnat=true|false`, default `false`; forward Port ACL applies only when DNAT is explicitly selected.
|
||||
- `connection_state=new|established|related`, default `new`.
|
||||
|
||||
Shared exports: `ExplainQuery`, `ExplainMatch`, `ExplainResponse`, `explainQuerySchema`, `explainResponseSchema`.
|
||||
|
||||
The response contains `desired` (revision/hash, nullable decision, stage, one logical source witness), `applied` (last acknowledged revision/hash, comparison and diagnostics state), at most100 `matches`, `matches_total`, `truncated`, `limits`, and last observed external-firewall metadata. The external snapshot is always marked `complete:false`. An EvoFirewall accept does not establish that an external firewall or the destination service accepts the packet.
|
||||
|
||||
Desired Linux nft order mirrors the emitted rules: established/related acceptance, L3 deny membership, explicit matching Port ACL closes, matching opens, implicit close for unmatched sources of a configured open, L3 allow, default action. Input assumes non-loopback ingress. Ordinary non-DNAT forwarding skips Port ACL. With configured Port ACL and missing port/protocol, the result can be `transport_unknown` with a null decision. A known ipset/iptables backend has no owned conntrack bypass: its INPUT chain evaluates deny, allow, then optional default drop; accept-default falls through to external rules. Its FORWARD chain is external because the compiler installs no owned jump there. A regression asserts the real shell compiler's INPUT-only contract. Source witnesses reuse the compiler's list/hostname/active-override expansion; L3 attribution describes configured logical sources rather than a per-policy kernel packet counter.
|
||||
|
||||
A reported ipset/iptables backend with any Port ACL makes the entire candidate unsupported: the agent refuses that candidate rather than partially applying its L3 rules. Known IPv6 policy/ACL capabilities gate IPv6 explanation. RouterOS without a verified `policy-ipv6` capability remains IPv4-only; RouterOS rejects all Port ACL candidates. For supported RouterOS families, input owns deny only and otherwise falls through to external rules; forward owns deny, allow, and optional default drop. Accept-default fallthrough remains external. Structured `UNSUPPORTED_CAPABILITY` errors map to diagnostics `unsupported` before generic failed status.
|
||||
|
||||
## Override creation and expiry
|
||||
|
||||
`POST /api/v1/agents/:id/overrides` retains existing CIDR/action/comment input and adds:
|
||||
|
||||
- Optional `reason`, trimmed,1..1024 characters.
|
||||
- Either optional `ttl_seconds`60..2592000 or `expires_at` (ISO timestamp), mutually exclusive. Explicit expiry must be future and within30 days. Omit both or set `expires_at:null` for a permanent override.
|
||||
- Author identity comes from authenticated user context. Client-supplied author fields are ignored.
|
||||
|
||||
Read/create DTO adds `reason`, `created_by_user_id`, `created_by_name`, `expires_at`, `expired`, and `expiration_enforcement:"next_policy_sync"`. Legacy author/reason remain null when unknown. An allow override retains existing deny-wins semantics; it cannot bypass an applicable deny set.
|
||||
|
||||
Expiry changes desired policy when the next controller policy evaluation occurs. It is not a device-local timer and cannot remove an override from an offline agent. Expiration is recorded transactionally, bumps the agent generation once, and is excluded before hashing/compilation. Re-evaluation does not repeatedly bump generation and a backward clock does not reactivate an already expired row. An expired network can receive a new override without a unique-key conflict.
|
||||
|
||||
Limits:1000 active overrides per agent; exceeded creation or clone returns409. Clone validates its source before replacing target assignments/default/overrides and preserves absolute expiry/reason/provenance for active rows. Existing legacy over-capacity active configurations are preserved for operator reconciliation, while new additions and over-capacity clones are refused.
|
||||
|
||||
Expired display history is bounded to100 rows per agent and7 days, pruned during expiry/create lifecycle after generation handling. An inactive agent can temporarily retain up to its previous active bound until that lifecycle runs again. Local audit history is the durable source for creation author, reason and scheduled expiry after display rows are pruned. No global/shared policy set is mutated by override expiry.
|
||||
|
||||
Migration015 adds override metadata and an indexed expiry lookup. Migration016 is reserved for operational outbox work; further backend feature migrations start017.
|
||||
|
||||
## Validation checkpoint
|
||||
|
||||
Nine service regressions cover backend-specific rule order, IPv4/IPv6/DNAT/stateful behavior, unsupported candidates, source witnesses, desired/applied separation, once-only expiry, archive/cardinality limits and clone isolation. A route regression verifies RBAC, canonical CIDR, server-owned author, TTL conflicts, duplicate409, expiry on policy sync and replacement after expiry. API TypeScript and shared/DB builds pass.
|
||||
|
||||
Separately, analytics follow-ups preserve explicit unknown v2 backfill deltas, count interval losses once across proportional buckets/retries, keep traffic-only coverage complete, preserve actual unsupported reasons, and retain exact hourly totals when exact minute intervals fit inside an hour.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Policy snapshots and staged rollout
|
||||
|
||||
Migration `017_policy_snapshots.sql` adds immutable compiled effective-policy snapshots, rollouts, materialized targets and per-agent pins. These are control-plane desired policies. Applied hashes and revisions still come exclusively from telemetry acknowledgements.
|
||||
|
||||
## API
|
||||
|
||||
All paths below have the `/api/v1` prefix. Snapshot/rollout routes require policies read/write permissions; pin release follows agents write permissions.
|
||||
|
||||
- `POST /policies/snapshots {source_agent_id,name}` captures a staging/source agent's effective compiled policy. It rejects pinned sources and active temporary overrides. Permanent overrides are included. Duplicate complete payloads return the existing snapshot and original metadata.
|
||||
- `GET /policies/snapshots?limit=20&offset=0` returns metadata only; maximum page size 50. `GET /policies/snapshots/:id` returns one payload. `DELETE` rejects referenced snapshots.
|
||||
- `POST /policies/snapshots/:id/diff {agent_ids}` compares at most 100 unique targets, with counts, default/port-rule changes, generation preconditions, admission conflicts and at most 100 membership examples **across the whole response**.
|
||||
- `POST /policies/rollouts {snapshot_id,targets:[{agent_id,expected_generation}],canary_ids}` validates every target, freezes each previous desired policy, then activates only canaries in one transaction. Canary IDs must be a nonempty subset of targets. Unselected agents retain their previous behavior.
|
||||
- `GET /policies/rollouts` lists bounded metadata; `GET /policies/rollouts/:id` includes at most 100 target states and current acknowledgement evidence.
|
||||
- `POST /policies/rollouts/:id/confirm {expected_version}` requires fresh successful exact hash/revision acknowledgements from every canary received strictly after activation. The freshness window is three sync intervals, minimum 180 seconds, maximum one hour. Failures and changed pending generations block confirmation. All remaining pins publish atomically.
|
||||
- `POST /policies/rollouts/:id/rollback {expected_version}` restores still-owned activated targets' captured previous desired policies at **new, higher revisions**. Pending and manually released targets remain unchanged. Even when every activated target was already released, rollback terminates the rollout and permits history cleanup. The rollback result is not an applied-success claim. Rollback pins stay active until explicitly released.
|
||||
- `POST /agents/:id/policy-pin/release {rollout_id,expected_generation}` validates and recompiles the current live policy before removing the pin; invalid/unsupported live policies retain the pin transactionally. Release can abandon a canary; subsequent confirmation fails, while rollback preserves the manual release and terminates the remaining rollout safely.
|
||||
- `DELETE /policies/rollouts/:id` removes terminal history only after every pin is released. This lets operators subsequently delete unreferenced snapshots and reclaim capacity.
|
||||
|
||||
## Safety and bounds
|
||||
|
||||
Every rollout validates all targets' temporary overrides, approval status, telemetry-v2 support and reported policy capabilities. RouterOS Port ACL, missing IPv6 support and legacy/unknown capabilities reject admission. A rollout freezes full effective source policy; target-specific permanent overrides are replaced, and the diff exposes that replacement.
|
||||
|
||||
While pinned, direct default-action, override, port-rule, set-assignment and clone mutations return `409 POLICY_PINNED`. Shared source/list/DNS edits can proceed, but their generation bumps skip pinned agents. Pin reads use the existing bounded policy cache; source witnesses for explain derive decisions from the frozen memberships and explicitly mark original attribution unavailable.
|
||||
|
||||
Snapshots have an 8 MiB payload limit, a global 256 MiB stored-payload byte limit, and a 200-row limit. Capacity sums actual UTF-8 payload bytes in SQL. Content deduplication includes frozen explanatory metadata. Rollouts have at most 100 targets, 20 active canary rollouts and 200 retained history rows. Capacity/admission failures roll back previous-snapshot capture and every pin. Rollout lists and target hashes do not load payload JSON.
|
||||
|
||||
Captured previous desired snapshots, prior delivered metadata and prior reported applied metadata are distinct fields. Pin changes update desired hash/revision and clear the delivery timestamp; only a subsequent policy delivery marks delivery, and only telemetry changes applied state. Mutation CAS checks and state changes share SQLite transactions.
|
||||
|
||||
## Observed validation
|
||||
|
||||
Service/HTTP lifecycle tests cover source mutation after capture, canary isolation, TTL rejection for source and all target baselines, unsupported targets, exact/fresh/revision/error ACK gates, pending-target CAS conflict, monotonic rollback, explicit release failure preservation, global-capacity admission, bounded diff examples, source explain after pinning and endpoint schema/pin-conflict handling. Windows tests do not establish real Linux/RouterOS apply behavior or production-volume readiness.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Runtime validation checkpoint
|
||||
|
||||
Docker/Linux runtime and RouterOS hardware are unavailable in this Windows workspace. No image smoke, load result, memory reduction or performance improvement is claimed. Docker Hub digest resolution was attempted on 2026-10-24 but DNS lookup failed. A checked-in base-image digest is still an unresolved release gate.
|
||||
|
||||
Local verification: `node --test scripts/ci/sqlite-backup.test.mjs` passed on Windows / Node 25.5.0 (1 test): committed WAL contents, refusal to overwrite a backup, offline restore requirement, removal of stale sidecars and preservation of the old destination when the input is invalid. Windows sandbox denied atomic rename; the identical test passed after an approved sandbox escalation. Bash syntax and workflow YAML parsing passed. These checks do not verify Alpine, Node 22 or the container image.
|
||||
|
||||
## Build and publication
|
||||
|
||||
2026-10-26 checkpoint: Docker CLI is still absent and `wsl --status` reports WSL is not installed. Local checks passed for the full-size fresh fixture/reuse refusal, selected-platform manifest validation and generated-schema drift check. These are not native container results. The load runner requires zero telemetry history/watermarks/totals, checks three unique raw reports and 200/400 deltas per agent plus 20000/40000 fleet totals, and reports report-ingestion and timeseries p95 independently. The first v2 counter establishes a baseline, so it contributes no fabricated traffic.
|
||||
|
||||
Both run-scoped CI builders now load `deploy/docker/buildkitd.toml`: GC targets 12GB maximum cache usage with 2GB reserved and 5GB free-space target; transient local/cachemount/git data has a 48-hour/1GB policy. These are garbage-collection targets rather than hard filesystem quotas. Configuration fields follow the [BuildKit daemon reference](https://docs.docker.com/build/buildkit/toml-configuration/). The real runner must still prove support and effective cleanup. Existing mirror tags are revalidated by comparing their selected platform manifest digest with the immutable upstream descriptor before use; a mismatched or missing platform fails publication.
|
||||
|
||||
The deployment remains one Alpine container: Fastify, SPA and SQLite. The Dockerfile has independent `api-build` and `web-build` branches sharing the dependency stage. `pnpm deploy --prod` creates the API dependency closure; the API root uses an allowlist (`dist`, `node_modules`, `package.json`). DB package `files` preserves migrations and shared runtime exports. Agent scripts come only from the API's `dist/agent-scripts` build output. Build tools stay in the dependency stages. Native module loading and migrations are checked by image smoke, not inferred from a successful TypeScript build.
|
||||
|
||||
Bake exports one registry cache from the final target with `mode=max`, covering both reachable branches. Pull requests import the release cache and explicitly disable export; forks build without registry credentials (private cache reads may miss). Image smoke runs on relevant PR and main/master changes. CD publication is serialized using workflow concurrency; the same registry cache has one writer. Gitea runner support for concurrency must be confirmed on the first run.
|
||||
|
||||
The mirror script resolves Docker Hub's manifest digest, gives each source digest its own mirror tag, then resolves and uses the mirror by digest. Resolution/copy errors fail CD instead of silently selecting a stale mirror. `BASE_NODE_SOURCE` can supply an approved `node:22-alpine@sha256:...` once independently verified. Defaults in Dockerfile/bake remain a mutable tag until that reviewed digest is available. Record and review the `BASE_NODE` line from CI.
|
||||
|
||||
```sh
|
||||
cd deploy/docker
|
||||
CACHE_REF_NODE=git.shx.one/denozord/evofw-buildcache:node-buildcache \
|
||||
docker buildx bake -f docker-bake.hcl --set evofw.cache-to= \
|
||||
--set evofw.tags=evofw-smoke:local --load default
|
||||
cd ../..
|
||||
sh scripts/ci/image-smoke.sh evofw-smoke:local
|
||||
REQUIRE_UPGRADE=true sh scripts/ci/image-smoke.sh evofw-smoke:local \
|
||||
git.shx.one/denozord/evofw@sha256:PREVIOUS_DIGEST
|
||||
```
|
||||
|
||||
Smoke uses disposable volumes and verifies migration/native SQLite startup, `/health`, DB-backed `/ready`, API JSON, SPA deep links, agent script delivery, runtime file placement, clean SIGTERM, restart persistence, an online backup and restoration into another volume. Supply Gitea variable `EVOFW_PREVIOUS_IMAGE` with the last deployed version/digest to exercise upgrade migrations. Every CD quality invocation now builds and runs smoke, including docs-only releases, before semantic-release can publish. `EVOFW_PREVIOUS_IMAGE` must end in an immutable `@sha256:...` digest. Missing or mutable previous images fail release validation. Only an explicitly approved first deployment may set Gitea variable `EVOFW_FIRST_RELEASE=true`; unset it immediately after that first deployment. PR/development smoke can run without upgrade validation and reports NOT RUN. The image healthcheck uses `/ready` with 40s startup grace; Compose allows 30s shutdown and rotates JSON logs (3 x 10MB).
|
||||
|
||||
## SQLite backup and recovery
|
||||
|
||||
```sh
|
||||
sh deploy/docker/backup.sh evofirewall /secure-backups/evofw-2026-10-24.db
|
||||
docker compose stop evofirewall
|
||||
sh deploy/docker/restore.sh git.shx.one/denozord/evofw:vVERSION \
|
||||
PROJECT_evofw-data /secure-backups/evofw-2026-10-24.db
|
||||
docker compose up -d evofirewall
|
||||
```
|
||||
|
||||
Backup uses `better-sqlite3`'s SQLite online backup API and verifies the output, including foreign keys. It includes committed WAL data while the API is running; copying only `app.db` from a live WAL database is not a backup. The host output must be new. Keep backups on a separate durable filesystem and protect them like credentials.
|
||||
|
||||
Restore supports a named Docker volume with `/data/app.db`. Stop **all** writers and prevent supervisors/other operators from starting them for the entire restore. The wrapper rejects a volume currently mounted by any running container, mounts the backup read-only, verifies it, prepares a temporary database, removes old sidecars and replaces the offline DB. This exclusion check is a guard, not a cross-host lock. Take a backup before replacing an existing DB. Rehearse restoration and upgrade on a cloned volume before production. Downgrading application code after new migrations requires restoring the matching pre-upgrade backup; down migrations are not supplied.
|
||||
|
||||
## Reproducible baseline
|
||||
|
||||
Use the same Linux host, pinned image digest, Docker version and fixture for each candidate. The harness limits the disposable container to the target 2 vCPU / 4GB and records host/Docker/image metadata. It does not add production CPU/heap caps based on guesses.
|
||||
|
||||
```sh
|
||||
sh scripts/bench/runtime.sh evofw-smoke:local runtime-artifacts/before
|
||||
node scripts/bench/bundle.mjs apps/web/dist > runtime-artifacts/bundle.json
|
||||
```
|
||||
|
||||
`fixture.mjs` seeds 100 approved agents assigned to one 50,000-prefix policy on a fresh migrated DB. The load runner fetches each agent's policy and heartbeat at configurable concurrency (default 10), then imports 100,000 prefixes through HTTP into a second list. It records statuses, payload bytes, per-request duration, p50/p95/max, raw Docker CPU/memory/block IO samples, process RSS/IO, DB/WAL sizes, cold readiness seconds and a verified backup. Every HTTP error fails the harness; a payload limit failure is evidence to resolve, not a passed capacity test. Collection runs once per disposable fixture; retain JSON/logs and backup according to the test environment's data policy.
|
||||
|
||||
The bundle tool records every JS/CSS output file's raw and gzip size; totals include lazy chunks and are not the initial route transfer size. Browser navigation/network traces remain required for route-level performance. Repeat at least three times per candidate and compare median runs, plus cold/warm policy behavior, while holding environment constant. Current measurement table: startup, RSS, CPU, IO, WAL, 100-agent policy latency and 100k import are **pending Docker execution**; bundle output may be collected locally after a successful web build. No gain percentage is available yet.
|
||||
|
||||
The migrated fixture itself passed a local test at full 100-agent / 50k-prefix scale, including its valid priority constraint, foreign keys and all 100k unique import CIDRs. This does not execute the image or HTTP capacity profile. The load runner separates 1000 cold/burst requests from a paced 400-request steady profile after warmup; verification and import add separate requests. Every HTTP error including 429 fails the runner. A separate in-process 800-request regression passed; the full Linux resource run is still pending. See [operational-hardening.md](operational-hardening.md) for the isolated namespace CI and guard semantics. Root separately verified the ReUI premium CLI `@reui/settings-2 --dry-run` successfully (10 hypothetical files, no installation changes), with the skill at `68f0e6fa91`.
|
||||
|
||||
Steady defaults are two workers, one agent cycle per worker every 1200ms (approximately 100 agents/minute), with a new sequence-4 report, its exact retry, a summary read and a timeseries read per agent. It measures accepted ingestion separately from duplicate retries. Both phases assert response ACK identity and duplicate flags. Post-burst verification requires three raw rows and 200/400 dropped/accepted per agent; post-steady verification requires four raw rows and 300/600 per agent, fleet totals 30000/60000. Fresh complete runs now issue 1611 requests including warmup, verification, import and diagnostics.
|
||||
|
||||
`BENCH_STEADY_CONCURRENCY` and `BENCH_STEADY_INTERVAL_MS` expose the steady profile. `BENCH_P95_LIMIT_MS` is optional for direct local runs; when set it fails on steady **new ingestion** or timeseries p95 above the limit. The constrained 2-vCPU/4GB Docker harness sets a default 500ms limit. Cold/burst p95 is reported independently and does not contaminate that acceptance check. Shorter pacing or extra requests from the same user can deliberately trigger UI limits; any 429 still fails the run. The target p95 acceptance remains pending the actual constrained CI run.
|
||||
|
||||
The quality workflow now actually invokes `scripts/bench/runtime.sh` after built-image smoke. Bake loads a run-unique image tag; CI captures its immutable local image ID and passes that exact ID to both smoke and the benchmark. The host load driver uses Node 22; the harness creates its own disposable 2-vCPU/4GB volume/container. The step explicitly sets the 500ms limit, concurrency 10 for burst and two workers/1200ms pacing for steady, with a 15-minute timeout. There are no production credentials in the fixture or request measurements. Relevant PRs and every CD invocation run the Docker job, so a failed constrained benchmark blocks release. This is CI wiring evidence; no native result is claimed before that workflow executes.
|
||||
|
||||
## Remaining gates
|
||||
|
||||
- Build and run the image on Linux/amd64, including the native binding, production-only dependency closure and the actual Gitea runner.
|
||||
- Pin a reviewed and resolved Alpine Node digest in source; verify mirror provenance and registry cache behavior.
|
||||
- Run previous-production-image upgrade and backup recovery with a representative database; preserve the immutable pre-upgrade backup.
|
||||
- Run the fixture baseline and inspect failed HTTP responses, event-loop behavior and DB growth. Long-duration retention/capacity testing and Linux/MikroTik packet/application validation remain separate.
|
||||
|
||||
## Local functional load observation (2026-10-26)
|
||||
|
||||
Root executed the benchmark HTTP load against a Windows Node 25.5 in-memory DB process, without CPU/RAM quotas. At concurrency 10, the fixture had 100 same-NAT agents and a 50k-prefix policy. All 1107 measured requests succeeded with zero HTTP failures and zero 429s. Each agent submitted three unique v2 reports, each retried once (six report POSTs), and retained exactly three raw rows. Assertions passed for per-agent 200 dropped/400 accepted deltas, fleet totals 20000/40000 and a 100k-prefix import.
|
||||
|
||||
Observed p95: report ingestion 30.08ms, timeseries 371.10ms, summary 16.94ms, initial policy reads 922.63ms and heartbeat 784.70ms. After the run at 122s uptime, the process reported RSS 177385472 bytes and heap-used 38631240 bytes. These are a final snapshot, not peak memory. This Windows functional run is neither a Linux/Alpine image result nor a 2-vCPU/4GB target-host baseline; no performance improvement or comparison is established.
|
||||
|
||||
The initial import response was 21903744 bytes and took 2.74s, exposing response amplification. Backend follow-up changes it to a bounded summary; this observation predates that change and does not validate the corrected response size. Preserve a fresh-run result for the revised endpoint before comparing payload or timing. Native CI still must execute image startup, SQLite binding/backup/upgrade, resource sampling and both isolated firewall backends.
|
||||
|
||||
A second fresh Windows in-memory run of the original mixed profile passed all 1107 requests with zero 429s and all correctness checks, including the corrected 120-byte import summary. Its p95 was reports 60.54ms, timeseries 647.73ms, cold policy 2221ms and summary 38.17ms. Concurrent development tools were active and CPU/RAM were unbounded. The timeseries result exceeds 500ms; it is not evidence that the target passes. These two runs mixed cold policy bursts and reads; neither measures the newly separated paced steady profile or establishes a before/after performance comparison.
|
||||
|
||||
The final fresh run used the separated profiles on Windows Node 25.5 with an in-memory DB and no CPU/RAM quota. All 1611 requests passed with zero rate limits, exact report-ID/sequence and duplicate ACK checks, four raw rows per agent, final per-agent 300 dropped/600 accepted and fleet totals 30000/60000. The 100k import summary remained below 256 bytes. Burst profile: 1000 requests at concurrency 10, new-ingestion p95 372.753ms (300 samples), timeseries p95 782.623ms and cold-policy p95 3451.412ms. Steady profile: 400 requests, two workers paced at 1200ms; new ingestion 100 samples p95 11.0855ms/max 14.4917ms, timeseries 100 samples p95 8.0675ms/max 10.7258ms, duplicate retries 100 samples p95 7.5481ms and summaries 100 samples p95 3.6905ms. This verifies the revised harness and local correctness. It does not prove native 2-vCPU/4GB capacity or establish a performance improvement. The actual constrained CI threshold remains pending execution.
|
||||
|
||||
Builder concurrency correction: each quality/publication job uses its own run-ID/attempt builder, with setup-buildx cleanup enabled. The registry cache is durable; local builder state is disposable. The verification script inspects the actual container configuration before use. Jobs never remove a shared builder, so concurrent PRs cannot tear down another build. The old persistent `evofw` builder is no longer selected; remove its stale container/state only during an idle-runner maintenance window after confirming no legacy job is using it.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Source refresh checkpoint
|
||||
|
||||
JSON URL sources and EvoBGP now share a bounded Node HTTP(S) transport. It resolves DNS inside the connection's `lookup` callback and returns the vetted address/addresses directly to the socket. It does not perform a preflight lookup followed by an independent connection lookup. Every resolved address must be public; mixed public/private DNS answers fail closed. IP literals receive the same policy check before connecting. Loopback, private/link-local, reserved, mapped and IPv6 transition destinations require an explicit operator allowlist. HTTP(S) URLs with embedded credentials and other protocols are rejected.
|
||||
|
||||
Private source origins are permitted only through the server environment, for example:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
EVOFW_SOURCE_ALLOWED_ORIGINS: "https://bgp.internal.example:8443,http://10.20.0.5:8080"
|
||||
```
|
||||
|
||||
Entries are comma-separated **exact origins** (scheme, hostname/IP and port). No wildcard, path, query, suffix or list-config bypass is supported. `https://bgp.internal.example:8443` does not authorize port 443, another hostname or its redirects. Keep this operator configuration narrow; an allowed internal service is intentionally trusted to resolve to private addresses. Existing internal EvoBGP deployments must add their origin before enabling these changes.
|
||||
|
||||
Each response has an 8MiB body limit, 16KiB header limit and a 45s wall-clock deadline covering DNS, headers and body. Both declared and streamed body sizes are checked. Requests ask for identity encoding and reject compressed responses, preventing an unbounded decompression path. JSON URL data is limited to 100,000 input elements, including duplicates. Redirects are processed manually with a maximum of three hops and a shared deadline; each destination is revalidated. HTTPS-to-HTTP redirects fail. Secret headers remain available only for same-origin redirects; after an origin change they are removed permanently, including a subsequent redirect back to the original origin. TLS certificate checks and the original hostname/SNI remain enabled.
|
||||
|
||||
EvoBGP pagination accepts at most 1,000 pages, 100,000 elements and 32MiB total, with a 45s deadline across the complete collection. Missing/repeated/invalid cursors, invalid continuation metadata and malformed page items fail the snapshot. Pages are collected completely before publishing any prefix. Community-label healing may first collect the communities endpoint and then the prefixes endpoint; each collection has its own bounded deadline. The autocomplete proxy uses the same transport and pagination and rejects malformed community records with an upstream error.
|
||||
|
||||
Refreshes are coalesced per database/list and limited to two concurrently across the process. A refresh captures the source list revision before network access and checks it before cascade staging; the existing transaction guard rechecks the staged dependency snapshot before commit. Results from a changed list cannot overwrite its newer configuration. EvoBGP integration settings are checked again before publishing. Parent DNS/materialization failures retain the whole previous cascade.
|
||||
|
||||
An empty upstream snapshot is quarantined unless the list configuration explicitly contains `"allow_empty": true`. Failed/partial/oversized/empty/stale results do not replace the last successful prefix data. Identical CIDR sets preserve entry IDs and agent generations. Diagnostics use existing fields: `last_error` records the bounded error, `entry_count` records the published volume, and `refreshed_at` now represents the last **successful** refresh. Failure no longer changes that timestamp. Stale requests do not write an error onto a newly edited list. No secrets or complete request URLs are included in transport errors.
|
||||
|
||||
Hostname materialization queries A and AAAA concurrently using one resolver and a shared five-second deadline; timeout cancels both queries. Only `ENODATA` permits publishing the successful other family, because it indicates authoritative absence for that record type. `ENOTFOUND`/NXDOMAIN concerns the whole hostname; contradictory partial answers and transient failures such as SERVFAIL/TIMEOUT reject the complete snapshot. An all-empty DNS result also preserves the previous cache. List cascade generation updates now include agents whose port ACL references the list directly, and deduplicate agent IDs across all changed lists/parents/sets in that publication. A policy affected through multiple dependencies receives one generation increment, while unchanged snapshots receive none.
|
||||
|
||||
Verification commands from `apps/api`:
|
||||
|
||||
```sh
|
||||
node node_modules/typescript/bin/tsc --noEmit
|
||||
node node_modules/vitest/vitest.mjs run src/services/source-hardening.test.ts src/services/source-dns.test.ts src/services/backend-hardening.test.ts
|
||||
```
|
||||
|
||||
Tests cover actual local HTTP sockets (explicitly allowed origins), DNS-address handoff, mixed/private DNS rejection, malformed allowlist handling, redirects and credential removal, streamed/declared limits, headers, timeouts, format/encoding rejection, pagination budgets, stale edits, empty quarantine, refresh coalescing/concurrency and previous cascade regressions. Windows sandbox blocked localhost connections; the same focused tests passed with an approved sandbox escalation. Linux container execution, real EvoBGP integration and load/resource measurements remain pending. Policy ETags/caching, event-loop telemetry and the expanded NAT-agent report/retry benchmark are separate pending work; the current ops benchmark only measures policy/heartbeat sweeps and one import.
|
||||
@@ -4,6 +4,22 @@
|
||||
Surface: **ReUI Frame**. Kit: `apps/web/src/components/reui-kit/`.
|
||||
Иерархия: **ReUI PRO > shadcn primitives**.
|
||||
|
||||
## EvoFirewall: согласованное сохранение текущего интерфейса (2026-10-24)
|
||||
|
||||
В текущем roadmap пользователь явно выбрал сохранение существующего внешнего вида. Это приоритетнее прежних рекомендаций этого документа о принудительном выравнивании всей оболочки. Не выполнять массовую замену registry-компонентов, нормализацию классов, типографики, размеров, плотности, KPI/Quick Actions или расположения header/sidebar ради соответствия историческому эталону.
|
||||
|
||||
Текущие chrome, локальные адаптации Frame/KPI, кнопка палитры на мобильных экранах, freshness/monitor и формы сохраняются. Поведение, состояния загрузки/ошибки, подписи метрик и доступность исправляются на существующих компонентах. Новые аналитика и диагностика используют Frame, действующие Chart/DataGrid и те же theme tokens без перестройки страницы. Registry `--overwrite` и повторная установка существующих компонентов не разрешены этим этапом.
|
||||
|
||||
Высокий контраст — явная пользовательская настройка, меняющая theme-aware токены только при `html[data-contrast='high']`. Стандартная тема сохраняется. Палитра имеет combobox/listbox/active-option связь и прокрутку выбранного результата; таблицы поддерживают Enter/Space, drag handles доступны клавиатурой. Для диагностических статусов и качества измерений используются Badge/Alert варианты, неизвестные данные не заменяются нулём или признаком успешности.
|
||||
|
||||
F1 использует те же FormSheet/RHF/Zod/FieldError для Explain и форм Port ACL, а имеющийся Sheet-паттерн overrides сохраняет черновик и добавляет срок/причину. Выбор агентов в аналитике — установленный Base UI Combobox с именами и chips, без требования вводить UUID. Редкие формы загружаются лениво и предзагружаются по фокусу/намерению; существующее расположение основных страниц сохраняется.
|
||||
|
||||
F2/F3 добавляют ленивые формы снимков/canary, webhook, ротации credentials и меток, сохраняя Frame/base-nova. Широкие таблицы используют `FormSheet size="lg"|"xl"`: ширина задана точными `data-[side=right]` вариантами, на мобильном — viewport, внутренний grid имеет `minmax(0,1fr)`, таблица прокручивается внутри DataGrid. Исходные registry-компоненты не переписываются. Предпросмотры не запускают изменения; ACK применения и публикация desired policy различаются. Снятие pin требует явного предупреждения о проверке и возобновлении live policy. Ключ подписи install-ссылок и webhook secrets не раскрываются при чтении.
|
||||
|
||||
Большие списки используют серверную пагинацию `ResourcePage.serverPagination`: поиск и все фильтры применяются ко всему списку до страницы; локальная сортировка отключена, чтобы не выдавать сортировку страницы за сортировку всего набора. Между запросами сохраняются предыдущая страница и фокус поля, показывается обновление. Неполный CIDR preview помечен сокращённым, количество остаётся полным. Импорт с хотя бы одной ошибочной строкой блокируется целиком; уникальные валидные дубликаты нормализуются.
|
||||
|
||||
Локальный ReUI skill обновлён до `68f0e6fa91`. PRO dry-run подтверждён для `settings-2` с `frame`/`base-nova`; файлы блока не установлены. Эти проверки не означают редизайн существующего интерфейса.
|
||||
|
||||
Карта: [docs](https://reui.io/docs) · [llms.txt](https://reui.io/llms.txt) · [Get Started](https://reui.io/docs/get-started) · [Styling](https://reui.io/docs/styling) · [Registry](https://reui.io/docs/registry) · [License](https://reui.io/docs/license-setup) · [Blocks](https://reui.io/blocks) · [MCP](https://reui.io/docs/mcp) · [Agent Skills](https://reui.io/docs/agent-skills)
|
||||
|
||||
## Surface
|
||||
|
||||
Reference in new issue
Block a user