feat(bell): add alert acknowledgement vertical slice
Harness governance / validate (pull_request) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
-- Bell v6 append-only rule evaluation, Alert identity and state transitions.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.rule_versions (
|
||||
id text PRIMARY KEY,
|
||||
tenant_id bigint NOT NULL,
|
||||
site_id bigint,
|
||||
rule_key text NOT NULL,
|
||||
version integer NOT NULL,
|
||||
display_name text NOT NULL,
|
||||
event_kind text NOT NULL,
|
||||
minimum_severity text NOT NULL,
|
||||
enabled boolean NOT NULL,
|
||||
effective_from timestamptz NOT NULL,
|
||||
config_hash bytea NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT bell_rule_version_id CHECK (id ~ '^ruv_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||||
CONSTRAINT bell_rule_version_scope CHECK (tenant_id >= 1 AND (site_id IS NULL OR site_id >= 1)),
|
||||
CONSTRAINT bell_rule_version_key CHECK (rule_key ~ '^[a-z][a-z0-9_-]{2,63}$'),
|
||||
CONSTRAINT bell_rule_version_number CHECK (version >= 1),
|
||||
CONSTRAINT bell_rule_version_name CHECK (char_length(btrim(display_name)) BETWEEN 1 AND 120),
|
||||
CONSTRAINT bell_rule_version_kind CHECK (event_kind ~ '^[a-z][a-z0-9_]{2,63}$'),
|
||||
CONSTRAINT bell_rule_version_severity CHECK (minimum_severity IN ('low','medium','high','critical')),
|
||||
CONSTRAINT bell_rule_version_hash CHECK (octet_length(config_hash) = 32),
|
||||
UNIQUE (tenant_id, rule_key, version),
|
||||
UNIQUE (tenant_id, rule_key, config_hash)
|
||||
);
|
||||
ALTER TABLE bell.rule_versions OWNER TO bell_app;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.event_rule_sweeps (
|
||||
event_id text PRIMARY KEY REFERENCES bell.events(id),
|
||||
swept_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
ALTER TABLE bell.event_rule_sweeps OWNER TO bell_app;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.rule_evaluations (
|
||||
id text PRIMARY KEY,
|
||||
event_id text NOT NULL REFERENCES bell.events(id),
|
||||
rule_version_id text NOT NULL REFERENCES bell.rule_versions(id),
|
||||
result text NOT NULL,
|
||||
reason text NOT NULL,
|
||||
evaluated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT bell_rule_evaluation_id CHECK (id ~ '^eva_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||||
CONSTRAINT bell_rule_evaluation_result CHECK (result IN ('matched','no_match')),
|
||||
CONSTRAINT bell_rule_evaluation_reason CHECK (reason IN ('matched','disabled','event_kind','severity')),
|
||||
UNIQUE (event_id, rule_version_id)
|
||||
);
|
||||
ALTER TABLE bell.rule_evaluations OWNER TO bell_app;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.alerts (
|
||||
id text PRIMARY KEY,
|
||||
tenant_id bigint NOT NULL,
|
||||
site_id bigint NOT NULL,
|
||||
rule_evaluation_id text NOT NULL UNIQUE REFERENCES bell.rule_evaluations(id),
|
||||
rule_version_id text NOT NULL REFERENCES bell.rule_versions(id),
|
||||
severity text NOT NULL,
|
||||
title text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT bell_alert_id CHECK (id ~ '^alt_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||||
CONSTRAINT bell_alert_scope CHECK (tenant_id >= 1 AND site_id >= 1),
|
||||
CONSTRAINT bell_alert_severity CHECK (severity IN ('low','medium','high','critical')),
|
||||
CONSTRAINT bell_alert_title CHECK (char_length(btrim(title)) BETWEEN 1 AND 120)
|
||||
);
|
||||
ALTER TABLE bell.alerts OWNER TO bell_app;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.alert_events (
|
||||
alert_id text NOT NULL REFERENCES bell.alerts(id),
|
||||
event_id text NOT NULL REFERENCES bell.events(id),
|
||||
linked_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (alert_id, event_id),
|
||||
UNIQUE (event_id, alert_id)
|
||||
);
|
||||
ALTER TABLE bell.alert_events OWNER TO bell_app;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.alert_transitions (
|
||||
id text PRIMARY KEY,
|
||||
alert_id text NOT NULL REFERENCES bell.alerts(id),
|
||||
sequence integer NOT NULL,
|
||||
from_state text,
|
||||
to_state text NOT NULL,
|
||||
actor_ref text NOT NULL,
|
||||
note text,
|
||||
occurred_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
CONSTRAINT bell_alert_transition_id CHECK (id ~ '^trn_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||||
CONSTRAINT bell_alert_transition_sequence CHECK (sequence >= 1),
|
||||
CONSTRAINT bell_alert_transition_from CHECK (from_state IS NULL OR from_state IN ('open','acknowledged')),
|
||||
CONSTRAINT bell_alert_transition_to CHECK (to_state IN ('open','acknowledged','closed')),
|
||||
CONSTRAINT bell_alert_transition_edge CHECK (
|
||||
(sequence = 1 AND from_state IS NULL AND to_state = 'open') OR
|
||||
(sequence > 1 AND from_state = 'open' AND to_state = 'acknowledged') OR
|
||||
(sequence > 1 AND from_state = 'acknowledged' AND to_state = 'closed')
|
||||
),
|
||||
CONSTRAINT bell_alert_transition_actor CHECK (
|
||||
char_length(actor_ref) BETWEEN 1 AND 80 AND actor_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:@/-]*$'
|
||||
),
|
||||
CONSTRAINT bell_alert_transition_note CHECK (note IS NULL OR char_length(note) <= 500),
|
||||
UNIQUE (alert_id, sequence)
|
||||
);
|
||||
ALTER TABLE bell.alert_transitions OWNER TO bell_app;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.alert_command_receipts (
|
||||
tenant_id bigint NOT NULL,
|
||||
idempotency_key text NOT NULL,
|
||||
command_hash bytea NOT NULL,
|
||||
response_status integer NOT NULL,
|
||||
response_body jsonb NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (tenant_id, idempotency_key),
|
||||
CONSTRAINT bell_alert_command_tenant CHECK (tenant_id >= 1),
|
||||
CONSTRAINT bell_alert_command_key CHECK (
|
||||
char_length(idempotency_key) BETWEEN 8 AND 128
|
||||
AND idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
|
||||
),
|
||||
CONSTRAINT bell_alert_command_hash CHECK (octet_length(command_hash) = 32),
|
||||
CONSTRAINT bell_alert_command_status CHECK (response_status IN (200,409)),
|
||||
CONSTRAINT bell_alert_command_body CHECK (jsonb_typeof(response_body) = 'object')
|
||||
);
|
||||
ALTER TABLE bell.alert_command_receipts OWNER TO bell_app;
|
||||
|
||||
DO $immutable$
|
||||
DECLARE
|
||||
table_name text;
|
||||
BEGIN
|
||||
FOREACH table_name IN ARRAY ARRAY[
|
||||
'rule_versions','event_rule_sweeps','rule_evaluations','alerts',
|
||||
'alert_events','alert_transitions','alert_command_receipts'
|
||||
] LOOP
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON bell.%I', 'bell_' || table_name || '_immutable', table_name);
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE UPDATE OR DELETE ON bell.%I FOR EACH ROW EXECUTE FUNCTION bell.reject_immutable_change()',
|
||||
'bell_' || table_name || '_immutable', table_name
|
||||
);
|
||||
END LOOP;
|
||||
END
|
||||
$immutable$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS bell_rule_versions_effective_idx
|
||||
ON bell.rule_versions(tenant_id, rule_key, effective_from DESC, version DESC);
|
||||
CREATE INDEX IF NOT EXISTS bell_alerts_scope_time_idx
|
||||
ON bell.alerts(tenant_id, site_id, created_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS bell_alert_events_event_idx ON bell.alert_events(event_id, alert_id);
|
||||
CREATE INDEX IF NOT EXISTS bell_alert_transitions_latest_idx
|
||||
ON bell.alert_transitions(alert_id, sequence DESC);
|
||||
|
||||
INSERT INTO bell.schema_migrations(version) VALUES (6)
|
||||
ON CONFLICT (version) DO NOTHING;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Bell runtime may only append and read rule/Alert history.
|
||||
|
||||
REVOKE ALL ON TABLE bell.rule_versions, bell.event_rule_sweeps,
|
||||
bell.rule_evaluations, bell.alerts, bell.alert_events,
|
||||
bell.alert_transitions, bell.alert_command_receipts FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE bell.rule_versions, bell.event_rule_sweeps,
|
||||
bell.rule_evaluations, bell.alerts, bell.alert_events,
|
||||
bell.alert_transitions, bell.alert_command_receipts FROM bell_runtime;
|
||||
|
||||
GRANT SELECT, INSERT ON TABLE bell.rule_versions, bell.event_rule_sweeps,
|
||||
bell.rule_evaluations, bell.alerts, bell.alert_events,
|
||||
bell.alert_transitions, bell.alert_command_receipts TO bell_runtime;
|
||||
|
||||
REVOKE ALL ON TABLE bell.rule_versions, bell.event_rule_sweeps,
|
||||
bell.rule_evaluations, bell.alerts, bell.alert_events,
|
||||
bell.alert_transitions, bell.alert_command_receipts FROM sense_app;
|
||||
@@ -1,6 +1,6 @@
|
||||
# YoVision PostgreSQL 初始化
|
||||
|
||||
本目录实现 T-009~T-012、T-015~T-016、T-019 的 PostgreSQL `17.10` schema。SQL 必须按文件名前缀顺序执行:`001`~`004` 创建 NOLOGIN 权限角色、Bell/Sense 初始对象和配额权限;`005`~`007` 增量增加 Area 与本地审计;`008`~`009` 增加 Control API 状态;`010`~`011` 增加调和 fencing、MediaMTX Path 历史归属、孤儿报告/受控处置结果;`012`~`013` 增加 Bell 不可变事件、append-only outcome 和独立 `bell_runtime` 最小权限;`014`~`015` 增加审计 relay;`016`~`017` 增加 Brain 事件身份绑定、永久来源收据、短期 nonce 和最小权限。全部 SQL 可重放。对象 owner/迁移角色为 `bell_app`/`sense_app`;应用登录角色及密码由部署环境或密钥系统创建,Sense 登录加入 `sense_app`,Bell 运行登录只加入 `bell_runtime`,仓库不保存登录凭据。
|
||||
本目录实现 T-009~T-012、T-015~T-016、T-019~T-020 的 PostgreSQL `17.10` schema。SQL 必须按文件名前缀顺序执行:`001`~`004` 创建 NOLOGIN 权限角色、Bell/Sense 初始对象和配额权限;`005`~`007` 增量增加 Area 与本地审计;`008`~`009` 增加 Control API 状态;`010`~`011` 增加调和 fencing、MediaMTX Path 历史归属、孤儿报告/受控处置结果;`012`~`013` 增加 Bell 不可变事件、append-only outcome 和独立 `bell_runtime` 最小权限;`014`~`015` 增加审计 relay;`016`~`017` 增加 Brain 事件身份绑定、永久来源收据、短期 nonce 和最小权限;`018`~`019` 增加不可变规则版本、durable evaluation、Alert/Event 关联、append-only 状态迁移、永久命令收据和最小权限。全部 SQL 可重放。对象 owner/迁移角色为 `bell_app`/`sense_app`;应用登录角色及密码由部署环境或密钥系统创建,Sense 登录加入 `sense_app`,Bell 运行登录只加入 `bell_runtime`,仓库不保存登录凭据。
|
||||
|
||||
生产/共享实例必须由管理员先备份并在 YoVision 专用数据库中执行。Sense 进程不会用高权限自动建库或建角色。示例只使用私有环境变量,不把实际 DSN 写入脚本或日志:
|
||||
|
||||
@@ -24,6 +24,7 @@ Get-ChildItem deploy/postgres/[0-9][0-9][0-9]_*.sql |
|
||||
- `bell.audit_events` 与事件事实一样不可更新/删除;`bell.audit_relay_receipts` 只为 10 分钟 nonce 幂等窗口保留,Bell runtime 仅可在这张限定表中查询、插入和清理过期记录。
|
||||
- `bell.events` 与 `bell.event_outcomes` 由 `bell_app` 拥有;`bell_runtime` 只获得 `SELECT/INSERT`,没有 owner、`UPDATE`、`DELETE` 或 `TRUNCATE` 权限,数据库 trigger 再拒绝 owner 路径的意外事实改写。
|
||||
- `bell.event_ingress_bindings` 由管理员维护并引用当前 Bell Site/Area 和 Sense Device;Bell runtime 只读绑定及 Sense 设备的逻辑 ID/Area/modality 列,不可读取 endpoint、credential、profile token 或 MediaMTX path。`event_ingress_receipts` 永久只追加;只有 `event_ingress_nonces` 可按 10 分钟 TTL 清理。
|
||||
- `bell.rule_versions`、sweep/evaluation、Alert/Event 关联、transition 和命令收据均由 `bell_app` 拥有;`bell_runtime` 只有 `SELECT/INSERT`,数据库 trigger 拒绝 UPDATE/DELETE。当前状态从最新 transition 推导,同一 evaluation 由唯一约束最多创建一个 Alert;命令收据永久保留以支持跨重启幂等。
|
||||
- `sense.control_idempotency_receipts` 不保存原始 Idempotency-Key,只保存 scope/request SHA-256 和脱敏响应快照;`batch_operations`/items 只保存逻辑 ID、状态和稳定错误,不保存连接秘密。
|
||||
- 调和与孤儿租约使用 PostgreSQL `clock_timestamp()` 和 fencing token;过期 worker 不能提交完成/失败或扫描报告。`media_path_ownership`、扫描和处置表不保存 endpoint、credential 或 source URI;数据库约束禁止为 `unowned` finding 写删除结果。
|
||||
- PUBLIC 对两个业务 schema 的表和函数没有权限。
|
||||
|
||||
@@ -71,7 +71,7 @@ BEGIN
|
||||
OR NOT has_table_privilege('sense_app', 'sense.device_operation_outbox', 'DELETE') THEN
|
||||
RAISE EXCEPTION 'sense_app lacks access to its local audit Outbox';
|
||||
END IF;
|
||||
IF (SELECT max(version) FROM bell.schema_migrations) <> 5
|
||||
IF (SELECT max(version) FROM bell.schema_migrations) <> 6
|
||||
OR (SELECT max(version) FROM sense.schema_migrations) <> 6 THEN
|
||||
RAISE EXCEPTION 'schema migration version drift';
|
||||
END IF;
|
||||
@@ -254,3 +254,24 @@ BEGIN
|
||||
END IF;
|
||||
END
|
||||
$event_ingress$;
|
||||
|
||||
DO $bell_alerts$
|
||||
DECLARE
|
||||
target text;
|
||||
BEGIN
|
||||
FOREACH target IN ARRAY ARRAY[
|
||||
'bell.rule_versions', 'bell.event_rule_sweeps', 'bell.rule_evaluations',
|
||||
'bell.alerts', 'bell.alert_events', 'bell.alert_transitions',
|
||||
'bell.alert_command_receipts'
|
||||
] LOOP
|
||||
IF NOT has_table_privilege('yovision_t015_bell', target, 'SELECT,INSERT')
|
||||
OR has_table_privilege('yovision_t015_bell', target, 'UPDATE,DELETE,TRUNCATE') THEN
|
||||
RAISE EXCEPTION 'Bell runtime violates append-only Alert privilege on %', target;
|
||||
END IF;
|
||||
IF has_table_privilege('public', target, 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE')
|
||||
OR has_table_privilege('sense_app', target, 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE') THEN
|
||||
RAISE EXCEPTION 'Bell Alert history leaked on %', target;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
$bell_alerts$;
|
||||
|
||||
Reference in New Issue
Block a user