@taopao/memoir 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +8 -2
- package/assets/migrations-pg/0100_baseline.sql +1115 -0
- package/assets/migrations-pg/0101_functions.sql +93 -0
- package/assets/migrations-pg/0102_views.sql +2425 -0
- package/assets/migrations-pg/0103_constraints.sql +147 -0
- package/assets/migrations-pg/0104_protection.sql +33 -0
- package/assets/migrations-pg/0105_bocom_credit_card.sql +43 -0
- package/assets/migrations-pg/0106_psbc_debit_card.sql +87 -0
- package/assets/migrations-pg/0107_tailong_debit_card.sql +87 -0
- package/assets/migrations-pg/0108_ccb_debit_card.sql +87 -0
- package/assets/migrations-pg/0109_ccb_credit_card.sql +63 -0
- package/assets/migrations-pg/0110_icbc_debit_card.sql +87 -0
- package/assets/migrations-pg/0111_icbc_credit_card.sql +66 -0
- package/assets/migrations-pg/0112_boc_debit_card.sql +88 -0
- package/assets/migrations-pg/0113_jsb_debit_card.sql +88 -0
- package/assets/migrations-pg/0114_cmbc_debit_card.sql +88 -0
- package/assets/migrations-pg/0115_hzbank_debit_card.sql +88 -0
- package/assets/migrations-pg/0116_unified_bank_documents.sql +197 -0
- package/assets/migrations-pg/0117_agent_submissions.sql +50 -0
- package/assets/migrations-pg/0118_agent_reversions_analysis.sql +34 -0
- package/assets/migrations-pg/0119_agent_extension_registry.sql +72 -0
- package/assets/migrations-pg/0120_agent_record_sources.sql +22 -0
- package/assets/migrations-pg/0121_agent_extension_deprecation.sql +30 -0
- package/assets/migrations-pg/0122_agent_extension_controlled_revert.sql +107 -0
- package/assets/migrations-pg/0123_agent_extension_versions.sql +54 -0
- package/assets/migrations-pg/0124_agent_extension_retirement_groups.sql +26 -0
- package/assets/migrations-pg/0125_agent_candidate_reviews.sql +30 -0
- package/assets/migrations-pg/0126_agent_module_lifecycle.sql +93 -0
- package/assets/migrations-pg/0127_agent_tasks.sql +64 -0
- package/assets/migrations-pg/0128_agent_task_leases.sql +80 -0
- package/assets/migrations-pg/0129_agent_task_plans.sql +66 -0
- package/assets/migrations-pg/0130_agent_task_submissions.sql +72 -0
- package/assets/migrations-pg/0131_agent_task_completions.sql +93 -0
- package/assets/migrations-pg/0132_agent_task_changes.sql +113 -0
- package/assets/migrations-pg/0133_agent_imported_source_snapshots.sql +152 -0
- package/assets/migrations-pg/0134_agent_analysis_datasets.sql +6 -0
- package/assets/migrations-pg/0135_agent_maintenance_tasks.sql +139 -0
- package/assets/migrations-pg/0136_agent_task_structures.sql +66 -0
- package/assets/migrations-pg/0137_agent_personal_identity.sql +149 -0
- package/assets/migrations-pg/0138_agent_work_records.sql +24 -0
- package/assets/migrations-pg/0139_shared_files.sql +43 -0
- package/assets/migrations-pg/0140_shared_script_runs.sql +36 -0
- package/assets/migrations-pg/0141_payment_bank_links.sql +96 -0
- package/assets/migrations-pg/0142_consumption_order_types_and_places.sql +287 -0
- package/assets/migrations-pg/0143_skill_direct_write.sql +330 -0
- package/assets/migrations-pg/0144_skill_schema_archive.sql +73 -0
- package/assets/migrations-pg/0145_skill_ingest_wiki.sql +91 -0
- package/assets/migrations-pg/0146_skill_load.sql +17 -0
- package/assets/migrations-pg/0147_drop_agent_schema.sql +147 -0
- package/assets/migrations-pg/0148_ingest_lanes.sql +19 -0
- package/assets/migrations-pg/0149_account_ownership.sql +233 -0
- package/assets/migrations-pg/0150_bank_card_exact_link.sql +24 -0
- package/assets/migrations-pg/0151_account_ownership_enforce.sql +35 -0
- package/assets/migrations-pg/0152_account_ownership_views.sql +750 -0
- package/assets/migrations-pg/0153_web_frontend.sql +34 -0
- package/assets/migrations-pg/0154_audio_record_owners.sql +103 -0
- package/assets/migrations-pg/0155_ingest_knowledge_tables.sql +171 -0
- package/assets/migrations-pg/0156_batch_playbook_note.sql +17 -0
- package/assets/migrations-pg/0157_last_contact.sql +166 -0
- package/assets/migrations-pg/0158_refund_match_hash_join.sql +91 -0
- package/assets/migrations-pg/0159_fact_occurred_time.sql +88 -0
- package/assets/migrations-pg/0160_agent_notes.sql +118 -0
- package/assets/migrations-pg/0161_life_history.sql +314 -0
- package/assets/migrations-pg/0162_person_card.sql +427 -0
- package/assets/migrations-pg/0163_parking_drop_personal_defaults.sql +12 -0
- package/assets/native/darwin-arm64.node +0 -0
- package/assets/playbooks/audio.md +83 -0
- package/assets/playbooks/chat.md +37 -0
- package/assets/playbooks/common.md +66 -0
- package/assets/playbooks/intimate.md +19 -0
- package/assets/playbooks/journal.md +17 -0
- package/assets/playbooks/orders.md +15 -0
- package/assets/playbooks/payments.md +16 -0
- package/assets/playbooks/self.md +29 -0
- package/bin/install-codex-skill.mjs +77 -0
- package/bin/memoir.mjs +5 -0
- package/dist/cli.js +34062 -0
- package/package.json +40 -4
- package/release-manifest.json +432 -0
- package/skill/SKILL.md +120 -0
- package/skill/references//345/256/211/350/243/205/344/275/215/347/275/256.md +21 -0
- package/skill/references//346/225/260/346/215/256/345/217/243/345/276/204.md +36 -0
- package/skill/scripts/memoir.mjs +82 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- 图退役的整组回执只增保存;逐表搬迁和归档依赖更新仍追加到已有生命周期历史。
|
|
2
|
+
-- 不搬迁业务资料、不创建备份,不修改已发布迁移。
|
|
3
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
4
|
+
CREATE TABLE agent.extension_retirement_groups (
|
|
5
|
+
id text PRIMARY KEY,actor text NOT NULL,idempotency_key text NOT NULL,
|
|
6
|
+
fingerprint text NOT NULL,request_json text NOT NULL CHECK(public.vault_json_valid(request_json)),
|
|
7
|
+
anchor_event_id text NOT NULL UNIQUE REFERENCES agent.extension_lifecycle_events(id),
|
|
8
|
+
result_json text NOT NULL CHECK(public.vault_json_valid(result_json)),created_at text NOT NULL,
|
|
9
|
+
UNIQUE(actor,idempotency_key)
|
|
10
|
+
);
|
|
11
|
+
-- 健康检查和幂等重放按组核对全部逐表事件,索引避免反复扫描无关生命周期历史。
|
|
12
|
+
CREATE INDEX idx_extension_lifecycle_retirement_group ON agent.extension_lifecycle_events ((result_json::jsonb->>'retirement_group_id'));
|
|
13
|
+
CREATE TRIGGER trg_extension_retirement_groups_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.extension_retirement_groups FOR EACH ROW EXECUTE FUNCTION public.vault_guard('append');
|
|
14
|
+
CREATE TRIGGER trg_extension_retirement_groups_truncate BEFORE TRUNCATE ON agent.extension_retirement_groups FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard('append');
|
|
15
|
+
CREATE TRIGGER trg_extension_retirement_groups_audit AFTER INSERT OR UPDATE OR DELETE ON agent.extension_retirement_groups FOR EACH ROW EXECUTE FUNCTION public.vault_audit('id');
|
|
16
|
+
CREATE TRIGGER trg_extension_retirement_groups_version AFTER INSERT OR UPDATE OR DELETE ON agent.extension_retirement_groups FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch();
|
|
17
|
+
DO $$ DECLARE g record; BEGIN
|
|
18
|
+
REVOKE ALL ON agent.extension_retirement_groups FROM PUBLIC;
|
|
19
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='agent.extension_retirement_groups'::regclass AND a.grantee<>c.relowner LOOP
|
|
20
|
+
EXECUTE format('REVOKE ALL ON agent.extension_retirement_groups FROM %I',g.rolname);
|
|
21
|
+
END LOOP;
|
|
22
|
+
-- app/readonly 仅按现有 persons 读取角色映射 SELECT,不能改写回执或扩大结构权限。
|
|
23
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type='SELECT' LOOP
|
|
24
|
+
EXECUTE format('GRANT SELECT ON agent.extension_retirement_groups TO %I',g.rolname);
|
|
25
|
+
END LOOP;
|
|
26
|
+
END $$;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- 候选处理与纠正结果分开:不自动写事实、不改原文;仅新增可追溯处理记录,不创建备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
CREATE TABLE agent.candidate_reviews (
|
|
4
|
+
id text PRIMARY KEY,candidate_id text NOT NULL UNIQUE REFERENCES agent.candidates(id),
|
|
5
|
+
actor text NOT NULL,idempotency_key text NOT NULL,fingerprint text NOT NULL,
|
|
6
|
+
outcome text NOT NULL CHECK(outcome IN ('resolved','dismissed')),reason text NOT NULL,
|
|
7
|
+
sensitivity text NOT NULL CHECK(sensitivity IN ('normal','private','intimate')),
|
|
8
|
+
resolution_submission_id text REFERENCES agent.submissions(id),resolution_operation_key text,
|
|
9
|
+
resolution_json text CHECK(resolution_json IS NULL OR public.vault_json_valid(resolution_json)),
|
|
10
|
+
request_json text NOT NULL CHECK(public.vault_json_valid(request_json)),plan_revision text NOT NULL,
|
|
11
|
+
result_json text NOT NULL CHECK(public.vault_json_valid(result_json)),change_set text NOT NULL,created_at text NOT NULL,
|
|
12
|
+
UNIQUE(actor,idempotency_key),
|
|
13
|
+
CHECK((outcome='resolved' AND resolution_submission_id IS NOT NULL AND resolution_operation_key IS NOT NULL AND resolution_json IS NOT NULL)
|
|
14
|
+
OR (outcome='dismissed' AND resolution_submission_id IS NULL AND resolution_operation_key IS NULL AND resolution_json IS NULL))
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX ix_candidate_review_resolution ON agent.candidate_reviews(resolution_submission_id) WHERE resolution_submission_id IS NOT NULL;
|
|
17
|
+
CREATE TRIGGER trg_candidate_reviews_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.candidate_reviews FOR EACH ROW EXECUTE FUNCTION public.vault_guard('append');
|
|
18
|
+
CREATE TRIGGER trg_candidate_reviews_truncate BEFORE TRUNCATE ON agent.candidate_reviews FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard('append');
|
|
19
|
+
CREATE TRIGGER trg_candidate_reviews_audit AFTER INSERT OR UPDATE OR DELETE ON agent.candidate_reviews FOR EACH ROW EXECUTE FUNCTION public.vault_audit('id');
|
|
20
|
+
CREATE TRIGGER trg_candidate_reviews_version AFTER INSERT OR UPDATE OR DELETE ON agent.candidate_reviews FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch();
|
|
21
|
+
DO $$ DECLARE g record; BEGIN
|
|
22
|
+
REVOKE ALL ON agent.candidate_reviews FROM PUBLIC;
|
|
23
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='agent.candidate_reviews'::regclass AND a.grantee<>c.relowner LOOP
|
|
24
|
+
EXECUTE format('REVOKE ALL ON agent.candidate_reviews FROM %I',g.rolname);
|
|
25
|
+
END LOOP;
|
|
26
|
+
-- 仅映射既有读取/业务插入角色;不给 UPDATE、DELETE、TRUNCATE 或结构权限。
|
|
27
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
28
|
+
EXECUTE format('GRANT %s ON agent.candidate_reviews TO %I',g.privilege_type,g.rolname);
|
|
29
|
+
END LOOP;
|
|
30
|
+
END $$;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
-- 执行停用保留资料:核心元数据存全局开关,不改写扩展生命周期,不创建备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
ALTER TABLE public.vault_meta ADD COLUMN agent_writes_enabled boolean NOT NULL DEFAULT true;
|
|
4
|
+
ALTER TABLE public.vault_meta ADD COLUMN agent_runtime_revision integer NOT NULL DEFAULT 1 CHECK(agent_runtime_revision>0);
|
|
5
|
+
CREATE FUNCTION public.vault_agent_writes_enabled() RETURNS boolean LANGUAGE sql STABLE SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
6
|
+
SELECT coalesce((SELECT agent_writes_enabled FROM public.vault_meta WHERE id=1),false)
|
|
7
|
+
$$;
|
|
8
|
+
-- 该函数仅返回开关,可供 app/readonly/触发器读取;不包含凭据、不提升身份。
|
|
9
|
+
GRANT EXECUTE ON FUNCTION public.vault_agent_writes_enabled() TO PUBLIC;
|
|
10
|
+
CREATE FUNCTION public.vault_agent_state_guard() RETURNS trigger LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
11
|
+
DECLARE owner_call boolean; actor text:=nullif(current_setting('vault.actor',true),'');
|
|
12
|
+
BEGIN
|
|
13
|
+
IF TG_OP IN ('DELETE','TRUNCATE') THEN RAISE EXCEPTION '核心元数据不能删除或清空'; END IF;
|
|
14
|
+
IF NEW.id IS DISTINCT FROM OLD.id THEN RAISE EXCEPTION '核心元数据身份不能修改'; END IF;
|
|
15
|
+
IF NEW.agent_writes_enabled IS DISTINCT FROM OLD.agent_writes_enabled OR NEW.agent_runtime_revision IS DISTINCT FROM OLD.agent_runtime_revision THEN
|
|
16
|
+
SELECT c.relowner=r.oid INTO owner_call FROM pg_class c JOIN pg_roles r ON r.rolname=current_user WHERE c.oid=TG_RELID;
|
|
17
|
+
IF NOT coalesce(owner_call,false) OR actor IS NULL OR (actor NOT LIKE 'maintenance:%' AND actor<>'migration') THEN RAISE EXCEPTION 'Agent 开关仅由属主维护入口修改'; END IF;
|
|
18
|
+
IF NEW.agent_runtime_revision<>OLD.agent_runtime_revision+1 OR NEW.agent_writes_enabled=OLD.agent_writes_enabled THEN RAISE EXCEPTION 'Agent 开关修订必须连续切换'; END IF;
|
|
19
|
+
END IF;
|
|
20
|
+
RETURN NEW;
|
|
21
|
+
END $$;
|
|
22
|
+
CREATE TRIGGER trg_vault_meta_agent_state BEFORE UPDATE OR DELETE ON public.vault_meta FOR EACH ROW EXECUTE FUNCTION public.vault_agent_state_guard();
|
|
23
|
+
CREATE TRIGGER trg_vault_meta_agent_truncate BEFORE TRUNCATE ON public.vault_meta FOR EACH STATEMENT EXECUTE FUNCTION public.vault_agent_state_guard();
|
|
24
|
+
CREATE TABLE agent.module_events (
|
|
25
|
+
id text PRIMARY KEY,actor text NOT NULL,idempotency_key text NOT NULL,fingerprint text NOT NULL,
|
|
26
|
+
previous_enabled boolean NOT NULL,enabled boolean NOT NULL,revision integer NOT NULL UNIQUE CHECK(revision>1),
|
|
27
|
+
reason text NOT NULL,plan_revision text NOT NULL,request_json text NOT NULL CHECK(public.vault_json_valid(request_json)),
|
|
28
|
+
grants_json text NOT NULL CHECK(public.vault_json_valid(grants_json)),catalog_json text NOT NULL CHECK(public.vault_json_valid(catalog_json)),
|
|
29
|
+
result_json text NOT NULL CHECK(public.vault_json_valid(result_json)),created_at text NOT NULL,
|
|
30
|
+
UNIQUE(actor,idempotency_key),CHECK(previous_enabled<>enabled)
|
|
31
|
+
);
|
|
32
|
+
CREATE TRIGGER trg_module_events_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.module_events FOR EACH ROW EXECUTE FUNCTION public.vault_guard('append');
|
|
33
|
+
CREATE TRIGGER trg_module_events_truncate BEFORE TRUNCATE ON agent.module_events FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard('append');
|
|
34
|
+
CREATE TRIGGER trg_module_events_audit AFTER INSERT OR UPDATE OR DELETE ON agent.module_events FOR EACH ROW EXECUTE FUNCTION public.vault_audit('id');
|
|
35
|
+
CREATE TRIGGER trg_module_events_version AFTER INSERT OR UPDATE OR DELETE ON agent.module_events FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch();
|
|
36
|
+
DO $$ DECLARE g record; BEGIN
|
|
37
|
+
REVOKE ALL ON agent.module_events FROM PUBLIC;
|
|
38
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='agent.module_events'::regclass AND a.grantee<>c.relowner LOOP
|
|
39
|
+
EXECUTE format('REVOKE ALL ON agent.module_events FROM %I',g.rolname);
|
|
40
|
+
END LOOP;
|
|
41
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type='SELECT' LOOP
|
|
42
|
+
EXECUTE format('GRANT SELECT ON agent.module_events TO %I',g.rolname);
|
|
43
|
+
END LOOP;
|
|
44
|
+
END $$;
|
|
45
|
+
-- 保护规则由共享表清单安装触发器,此文件定义稳定的数据库函数。
|
|
46
|
+
CREATE OR REPLACE FUNCTION public.vault_guard() RETURNS trigger LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
47
|
+
DECLARE actor text := nullif(current_setting('vault.actor',true),'');
|
|
48
|
+
BEGIN
|
|
49
|
+
IF TG_OP='TRUNCATE' THEN RAISE EXCEPTION '禁止 TRUNCATE'; END IF;
|
|
50
|
+
IF actor IS NULL THEN RAISE EXCEPTION '未设置写入会话'; END IF;
|
|
51
|
+
-- 正常人工/导入写入 public 不读 Agent 管理表;仅 Agent 身份或管理表写入核对稳定的 vault_meta 开关。
|
|
52
|
+
IF actor LIKE 'agent:%' OR TG_TABLE_SCHEMA='agent' THEN
|
|
53
|
+
IF actor<>'migration' AND NOT public.vault_agent_writes_enabled() THEN
|
|
54
|
+
IF NOT (TG_TABLE_SCHEMA='agent' AND TG_TABLE_NAME='module_events' AND actor LIKE 'maintenance:%'
|
|
55
|
+
AND EXISTS(SELECT 1 FROM pg_class c JOIN pg_roles r ON r.rolname=current_user WHERE c.oid=TG_RELID AND c.relowner=r.oid))
|
|
56
|
+
THEN RAISE EXCEPTION 'Agent 写入已停用,资料保留只读'; END IF;
|
|
57
|
+
END IF;
|
|
58
|
+
END IF;
|
|
59
|
+
|
|
60
|
+
IF TG_ARGV[0]='append' AND TG_OP IN('UPDATE','DELETE') THEN RAISE EXCEPTION '只增表禁止修改或删除'; END IF;
|
|
61
|
+
IF TG_ARGV[0]='fact' AND actor NOT IN('importer','migration') THEN RAISE EXCEPTION '事实表只允许导入程序修改'; END IF;
|
|
62
|
+
IF TG_TABLE_NAME='backup_log' AND TG_OP='DELETE' THEN RAISE EXCEPTION '备份记录不能删除'; END IF;
|
|
63
|
+
IF TG_TABLE_NAME='import_batches' THEN
|
|
64
|
+
IF TG_OP='DELETE' THEN RAISE EXCEPTION '导入批次不能删除'; END IF;
|
|
65
|
+
IF TG_OP='UPDATE' AND OLD.status<>'running' AND actor<>'migration' THEN RAISE EXCEPTION '已完成的导入批次不能修改'; END IF;
|
|
66
|
+
END IF;
|
|
67
|
+
IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW;
|
|
68
|
+
END $$;
|
|
69
|
+
CREATE OR REPLACE FUNCTION public.vault_extension_guard() RETURNS trigger
|
|
70
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
71
|
+
DECLARE actor text:=nullif(current_setting('vault.actor',true),''); item record; col text; controlled boolean;
|
|
72
|
+
BEGIN
|
|
73
|
+
IF TG_OP IN ('DELETE','TRUNCATE') THEN RAISE EXCEPTION '扩展只允许软删除'; END IF;
|
|
74
|
+
IF actor IS NULL THEN RAISE EXCEPTION '未设置写入会话'; END IF;
|
|
75
|
+
IF actor<>'migration' AND NOT public.vault_agent_writes_enabled() THEN RAISE EXCEPTION 'Agent 写入已停用,资料保留只读'; END IF;
|
|
76
|
+
|
|
77
|
+
SELECT schema_name,table_name,state INTO item FROM agent.extensions WHERE id=TG_ARGV[0];
|
|
78
|
+
IF NOT FOUND OR item.schema_name<>TG_TABLE_SCHEMA OR item.table_name<>TG_TABLE_NAME THEN RAISE EXCEPTION '扩展注册与物理对象不一致'; END IF;
|
|
79
|
+
IF actor='migration' THEN RETURN NEW; END IF;
|
|
80
|
+
-- 标志本身不是权限:必须处于限定恢复函数的 owner 执行身份,app 即使自行 SET 也不能写。
|
|
81
|
+
SELECT TG_OP='UPDATE' AND nullif(current_setting('vault.extension_revert',true),'')=TG_ARGV[0]
|
|
82
|
+
AND c.relowner=r.oid INTO controlled FROM pg_class c JOIN pg_roles r ON r.rolname=current_user WHERE c.oid=TG_RELID;
|
|
83
|
+
IF item.state<>'active' AND NOT coalesce(controlled,false) THEN RAISE EXCEPTION '扩展已停止写入'; END IF;
|
|
84
|
+
IF nullif(current_setting('vault.change_set',true),'') IS NULL THEN RAISE EXCEPTION '扩展写入需要批次'; END IF;
|
|
85
|
+
IF NEW.updated_by<>actor OR (TG_OP='INSERT' AND NEW.created_by<>actor) THEN RAISE EXCEPTION '扩展审计身份不一致'; END IF;
|
|
86
|
+
IF TG_OP='UPDATE' THEN
|
|
87
|
+
IF NEW.created_at IS DISTINCT FROM OLD.created_at OR NEW.created_by IS DISTINCT FROM OLD.created_by THEN RAISE EXCEPTION '扩展创建信息不可修改'; END IF;
|
|
88
|
+
FOREACH col IN ARRAY TG_ARGV[1:array_length(TG_ARGV,1)] LOOP
|
|
89
|
+
IF (to_jsonb(NEW)->col) IS DISTINCT FROM (to_jsonb(OLD)->col) THEN RAISE EXCEPTION '扩展主键不可修改'; END IF;
|
|
90
|
+
END LOOP;
|
|
91
|
+
END IF;
|
|
92
|
+
RETURN NEW;
|
|
93
|
+
END $$;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- 单来源任务定义与登记回执只追加;此版本不领取任务、不执行模型、不自动备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
-- 停用时的 ACL/目录快照必须保持可恢复,不能在未知旧快照上创建新写入口或隐式重新启用。
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
IF NOT public.vault_agent_writes_enabled() THEN
|
|
6
|
+
RAISE EXCEPTION 'Agent 写入已停用,请在明确的模块维护中核对后再升级任务结构';
|
|
7
|
+
END IF;
|
|
8
|
+
END $$;
|
|
9
|
+
CREATE TABLE agent.tasks (
|
|
10
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
11
|
+
actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),
|
|
12
|
+
content_key text NOT NULL CHECK(content_key ~ '^[a-f0-9]{64}$'),
|
|
13
|
+
descriptor_json text NOT NULL CHECK(public.vault_json_valid(descriptor_json)),
|
|
14
|
+
max_sensitivity text NOT NULL CHECK(max_sensitivity IN ('normal','private','intimate')),
|
|
15
|
+
created_at text NOT NULL,
|
|
16
|
+
UNIQUE(actor,content_key),UNIQUE(actor,id),
|
|
17
|
+
CHECK(jsonb_typeof(descriptor_json::jsonb) IS NOT DISTINCT FROM 'object'),
|
|
18
|
+
CHECK((descriptor_json::jsonb->>'format_version') IS NOT DISTINCT FROM '1'),
|
|
19
|
+
CHECK((descriptor_json::jsonb->>'kind') IS NOT DISTINCT FROM 'extract_source'),
|
|
20
|
+
CHECK((descriptor_json::jsonb->>'max_sensitivity') IS NOT DISTINCT FROM max_sensitivity)
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX ix_tasks_actor_created ON agent.tasks(actor,created_at DESC,id DESC);
|
|
23
|
+
CREATE TABLE agent.task_enqueues (
|
|
24
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
25
|
+
actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),
|
|
26
|
+
idempotency_key text NOT NULL CHECK(length(idempotency_key) BETWEEN 1 AND 200),
|
|
27
|
+
fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
|
|
28
|
+
task_id text NOT NULL,task_reused boolean NOT NULL,
|
|
29
|
+
reason text NOT NULL CHECK(length(reason) BETWEEN 1 AND 2000),created_at text NOT NULL,
|
|
30
|
+
UNIQUE(actor,idempotency_key),
|
|
31
|
+
FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id)
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX ix_task_enqueues_task ON agent.task_enqueues(task_id);
|
|
34
|
+
CREATE FUNCTION public.vault_task_registration_guard() RETURNS trigger
|
|
35
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
36
|
+
DECLARE actor text:=nullif(current_setting('vault.actor',true),'');
|
|
37
|
+
BEGIN
|
|
38
|
+
-- 恢复/迁移保留原始身份和时间;普通登记身份与写会话一致,时间统一由服务器生成。
|
|
39
|
+
IF actor='migration' THEN RETURN NEW; END IF;
|
|
40
|
+
IF actor IS NULL OR actor !~ '^agent:local:[0-9]+$' OR NEW.actor IS DISTINCT FROM actor THEN
|
|
41
|
+
RAISE EXCEPTION '任务登记身份与可信写会话不一致';
|
|
42
|
+
END IF;
|
|
43
|
+
NEW.created_at:=to_char(clock_timestamp() AT TIME ZONE 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"');
|
|
44
|
+
RETURN NEW;
|
|
45
|
+
END $$;
|
|
46
|
+
REVOKE ALL ON FUNCTION public.vault_task_registration_guard() FROM PUBLIC;
|
|
47
|
+
DO $$ DECLARE name text; g record; BEGIN
|
|
48
|
+
FOREACH name IN ARRAY ARRAY['tasks','task_enqueues'] LOOP
|
|
49
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
50
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
51
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_identity BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_registration_guard()',name);
|
|
52
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(''id'')',name);
|
|
53
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
54
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
55
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid=to_regclass('agent.'||name) AND a.grantee<>c.relowner LOOP
|
|
56
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
|
|
57
|
+
END LOOP;
|
|
58
|
+
-- 沿用现有部署角色映射;不授 UPDATE/DELETE/TRUNCATE、结构权限或转授权限。
|
|
59
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
60
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
61
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
62
|
+
END LOOP;
|
|
63
|
+
END LOOP;
|
|
64
|
+
END $$;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
-- 首批租约只支持首次领取/续期,不自动抢占过期任务,不运行模型或创建备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
IF NOT public.vault_agent_writes_enabled() THEN
|
|
5
|
+
RAISE EXCEPTION 'Agent 写入已停用,请在明确的模块维护中核对后再升级租约结构';
|
|
6
|
+
END IF;
|
|
7
|
+
END $$;
|
|
8
|
+
CREATE TABLE agent.task_lease_events (
|
|
9
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
10
|
+
actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),task_id text NOT NULL,
|
|
11
|
+
sequence integer NOT NULL CHECK(sequence>0),attempt integer NOT NULL CHECK(attempt>0),
|
|
12
|
+
kind text NOT NULL CHECK(kind IN ('claim','heartbeat')),
|
|
13
|
+
executor text NOT NULL CHECK(executor ~ '^[a-z][a-z0-9_-]{0,99}$'),
|
|
14
|
+
token_hash text NOT NULL CHECK(token_hash ~ '^[a-f0-9]{64}$'),
|
|
15
|
+
max_sensitivity text NOT NULL CHECK(max_sensitivity IN ('normal','private','intimate')),
|
|
16
|
+
lease_until text NOT NULL CHECK(lease_until ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z$'),
|
|
17
|
+
created_at text NOT NULL,
|
|
18
|
+
UNIQUE(actor,task_id,sequence),UNIQUE(actor,id,kind),
|
|
19
|
+
FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
|
|
20
|
+
CHECK((kind='claim' AND sequence=1 AND attempt=1) OR (kind='heartbeat' AND sequence>1))
|
|
21
|
+
);
|
|
22
|
+
CREATE TABLE agent.task_claims (
|
|
23
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),
|
|
24
|
+
idempotency_key text NOT NULL CHECK(length(idempotency_key) BETWEEN 1 AND 200),
|
|
25
|
+
fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
|
|
26
|
+
event_id text,event_kind text NOT NULL DEFAULT 'claim' CHECK(event_kind='claim'),
|
|
27
|
+
scanned integer NOT NULL CHECK(scanned BETWEEN 0 AND 100),scan_truncated boolean NOT NULL,
|
|
28
|
+
cursor_json text CHECK(cursor_json IS NULL OR public.vault_json_valid(cursor_json)),created_at text NOT NULL,
|
|
29
|
+
UNIQUE(actor,idempotency_key),
|
|
30
|
+
FOREIGN KEY(actor,event_id,event_kind) REFERENCES agent.task_lease_events(actor,id,kind),
|
|
31
|
+
CHECK(event_id IS NULL OR (scanned>0 AND cursor_json IS NULL)),
|
|
32
|
+
CHECK(event_id IS NOT NULL OR scan_truncated=(cursor_json IS NOT NULL)),
|
|
33
|
+
CHECK(cursor_json IS NULL OR scanned=100)
|
|
34
|
+
);
|
|
35
|
+
CREATE FUNCTION public.vault_task_lease_guard() RETURNS trigger
|
|
36
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
37
|
+
DECLARE previous agent.task_lease_events%ROWTYPE; maximum text;
|
|
38
|
+
BEGIN
|
|
39
|
+
IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
|
|
40
|
+
-- 原始令牌永不入库;即使不经 SDK 的合作入口写入,也必须保持租约历史连续且不换归属。
|
|
41
|
+
PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
42
|
+
SELECT max_sensitivity INTO maximum FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
|
|
43
|
+
IF maximum IS NULL OR array_position(ARRAY['normal','private','intimate'],NEW.max_sensitivity)>array_position(ARRAY['normal','private','intimate'],maximum) THEN
|
|
44
|
+
RAISE EXCEPTION '任务租约不能提高登记的读取范围';
|
|
45
|
+
END IF;
|
|
46
|
+
IF NEW.lease_until::timestamptz<=clock_timestamp() OR NEW.lease_until::timestamptz>clock_timestamp()+interval '300 seconds' THEN
|
|
47
|
+
RAISE EXCEPTION '任务租约时限无效';
|
|
48
|
+
END IF;
|
|
49
|
+
SELECT * INTO previous FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
|
|
50
|
+
IF NEW.kind='claim' THEN
|
|
51
|
+
IF FOUND OR NEW.sequence<>1 OR NEW.attempt<>1 THEN RAISE EXCEPTION '任务已经领取,不能自动重新领取'; END IF;
|
|
52
|
+
ELSE
|
|
53
|
+
IF NOT FOUND OR NEW.sequence<>previous.sequence+1 OR NEW.attempt<>previous.attempt
|
|
54
|
+
OR NEW.token_hash IS DISTINCT FROM previous.token_hash OR NEW.executor IS DISTINCT FROM previous.executor
|
|
55
|
+
OR NEW.max_sensitivity IS DISTINCT FROM previous.max_sensitivity OR previous.lease_until::timestamptz<=clock_timestamp()
|
|
56
|
+
OR NEW.lease_until<previous.lease_until THEN
|
|
57
|
+
RAISE EXCEPTION '任务续期租约已失效或历史不连续';
|
|
58
|
+
END IF;
|
|
59
|
+
END IF;
|
|
60
|
+
RETURN NEW;
|
|
61
|
+
END $$;
|
|
62
|
+
REVOKE ALL ON FUNCTION public.vault_task_lease_guard() FROM PUBLIC;
|
|
63
|
+
DO $$ DECLARE name text; g record; BEGIN
|
|
64
|
+
FOREACH name IN ARRAY ARRAY['task_lease_events','task_claims'] LOOP
|
|
65
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
66
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
67
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_identity BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_registration_guard()',name);
|
|
68
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(''id'')',name);
|
|
69
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
70
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
71
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid=to_regclass('agent.'||name) AND a.grantee<>c.relowner LOOP
|
|
72
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
|
|
73
|
+
END LOOP;
|
|
74
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
75
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
76
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
77
|
+
END LOOP;
|
|
78
|
+
END LOOP;
|
|
79
|
+
END $$;
|
|
80
|
+
CREATE TRIGGER trg_task_lease_events_lease BEFORE INSERT ON agent.task_lease_events FOR EACH ROW EXECUTE FUNCTION public.vault_task_lease_guard();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- 稳定提交计划先于业务执行持久化;本迁移不执行计划,不调用模型或创建备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
IF NOT public.vault_agent_writes_enabled() THEN
|
|
5
|
+
RAISE EXCEPTION 'Agent 写入已停用,请在明确的模块维护中核对后再升级任务计划结构';
|
|
6
|
+
END IF;
|
|
7
|
+
END $$;
|
|
8
|
+
CREATE TABLE agent.task_plans (
|
|
9
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
10
|
+
actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),task_id text NOT NULL,
|
|
11
|
+
lease_event_id text NOT NULL,lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat')),
|
|
12
|
+
fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
|
|
13
|
+
source_revision text NOT NULL CHECK(source_revision ~ '^[a-f0-9]{64}$'),
|
|
14
|
+
max_sensitivity text NOT NULL CHECK(max_sensitivity IN ('normal','private','intimate')),
|
|
15
|
+
submission_key text NOT NULL CHECK(submission_key='task:'||task_id||':submit:v1'),
|
|
16
|
+
payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),created_at text NOT NULL,
|
|
17
|
+
UNIQUE(actor,task_id),UNIQUE(actor,id),UNIQUE(actor,submission_key),
|
|
18
|
+
FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
|
|
19
|
+
FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind),
|
|
20
|
+
CHECK((payload_json::jsonb->>'format_version') IS NOT DISTINCT FROM '1'),
|
|
21
|
+
CHECK((payload_json::jsonb->>'kind') IS NOT DISTINCT FROM 'submit'),
|
|
22
|
+
CHECK((payload_json::jsonb->>'task_id') IS NOT DISTINCT FROM task_id),
|
|
23
|
+
CHECK((payload_json::jsonb->>'source_revision') IS NOT DISTINCT FROM source_revision),
|
|
24
|
+
CHECK((payload_json::jsonb->'request'->>'idempotency_key') IS NOT DISTINCT FROM submission_key),
|
|
25
|
+
CHECK((payload_json::jsonb->'request'->'source'->>'sensitivity') IS NOT DISTINCT FROM max_sensitivity)
|
|
26
|
+
);
|
|
27
|
+
CREATE FUNCTION public.vault_task_plan_guard() RETURNS trigger
|
|
28
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
29
|
+
DECLARE lease agent.task_lease_events%ROWTYPE; task agent.tasks%ROWTYPE;
|
|
30
|
+
BEGIN
|
|
31
|
+
IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
|
|
32
|
+
-- 同一任务不能换租约另存计划;只追加约束和唯一键共同保留首个明确请求。
|
|
33
|
+
PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
34
|
+
SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
|
|
35
|
+
IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind
|
|
36
|
+
OR lease.lease_until::timestamptz<=clock_timestamp() THEN
|
|
37
|
+
RAISE EXCEPTION '任务计划需要当前有效租约';
|
|
38
|
+
END IF;
|
|
39
|
+
SELECT * INTO task FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
|
|
40
|
+
IF NOT FOUND OR task.descriptor_json::jsonb->'source'->>'object' IS DISTINCT FROM 'public.journal_entries'
|
|
41
|
+
OR task.descriptor_json::jsonb->'source'->>'revision' IS DISTINCT FROM NEW.source_revision
|
|
42
|
+
OR task.max_sensitivity IS DISTINCT FROM NEW.max_sensitivity OR lease.max_sensitivity IS DISTINCT FROM NEW.max_sensitivity
|
|
43
|
+
OR current_setting('vault.reason',true) IS DISTINCT FROM NEW.payload_json::jsonb->'request'->>'reason' THEN
|
|
44
|
+
RAISE EXCEPTION '任务计划与来源快照、处理范围或审计原因不一致';
|
|
45
|
+
END IF;
|
|
46
|
+
RETURN NEW;
|
|
47
|
+
END $$;
|
|
48
|
+
REVOKE ALL ON FUNCTION public.vault_task_plan_guard() FROM PUBLIC;
|
|
49
|
+
DO $$ DECLARE name text; g record; BEGIN
|
|
50
|
+
FOREACH name IN ARRAY ARRAY['task_plans'] LOOP
|
|
51
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
52
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
53
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_identity BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_registration_guard()',name);
|
|
54
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(''id'')',name);
|
|
55
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
56
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
57
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid=to_regclass('agent.'||name) AND a.grantee<>c.relowner LOOP
|
|
58
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
|
|
59
|
+
END LOOP;
|
|
60
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
61
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
62
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
63
|
+
END LOOP;
|
|
64
|
+
END LOOP;
|
|
65
|
+
END $$;
|
|
66
|
+
CREATE TRIGGER trg_task_plans_plan BEFORE INSERT ON agent.task_plans FOR EACH ROW EXECUTE FUNCTION public.vault_task_plan_guard();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- 受控任务业务提交与真实结果绑定;只新增结构,不执行计划或创建备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
IF NOT public.vault_agent_writes_enabled() THEN
|
|
5
|
+
RAISE EXCEPTION 'Agent 写入已停用,请在明确的模块维护中核对后再升级任务提交结构';
|
|
6
|
+
END IF;
|
|
7
|
+
END $$;
|
|
8
|
+
-- 全局主键已唯一,补身份复合键供绑定外键核对,不改写任何既有提交。
|
|
9
|
+
ALTER TABLE agent.submissions ADD CONSTRAINT submissions_actor_id_key UNIQUE(actor,id);
|
|
10
|
+
CREATE TABLE agent.task_submissions (
|
|
11
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
12
|
+
actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),task_id text NOT NULL,
|
|
13
|
+
plan_revision text NOT NULL CHECK(plan_revision ~ '^[a-f0-9]{64}$'),
|
|
14
|
+
lease_event_id text NOT NULL,lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat')),created_at text NOT NULL,
|
|
15
|
+
UNIQUE(actor,task_id),UNIQUE(actor,id),
|
|
16
|
+
FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
|
|
17
|
+
CONSTRAINT task_submissions_plan_fkey FOREIGN KEY(actor,id) REFERENCES agent.task_plans(actor,id),
|
|
18
|
+
CONSTRAINT task_submissions_submission_fkey FOREIGN KEY(actor,id) REFERENCES agent.submissions(actor,id),
|
|
19
|
+
FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind)
|
|
20
|
+
);
|
|
21
|
+
CREATE FUNCTION public.vault_task_submission_guard() RETURNS trigger
|
|
22
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
23
|
+
DECLARE lease agent.task_lease_events%ROWTYPE; plan agent.task_plans%ROWTYPE; submission agent.submissions%ROWTYPE; source public.journal_entries%ROWTYPE; request jsonb;
|
|
24
|
+
BEGIN
|
|
25
|
+
IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
|
|
26
|
+
PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
27
|
+
SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
|
|
28
|
+
IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind
|
|
29
|
+
OR lease.lease_until::timestamptz<=clock_timestamp() THEN RAISE EXCEPTION '任务提交需要当前有效租约'; END IF;
|
|
30
|
+
SELECT * INTO plan FROM agent.task_plans WHERE actor=NEW.actor AND id=NEW.id;
|
|
31
|
+
IF NOT FOUND OR plan.task_id IS DISTINCT FROM NEW.task_id OR plan.fingerprint IS DISTINCT FROM NEW.plan_revision
|
|
32
|
+
OR current_setting('vault.change_set',true) IS DISTINCT FROM NEW.id THEN RAISE EXCEPTION '任务提交必须绑定原计划与审计批次'; END IF;
|
|
33
|
+
request:=plan.payload_json::jsonb->'request';
|
|
34
|
+
SELECT * INTO submission FROM agent.submissions WHERE actor=NEW.actor AND id=NEW.id;
|
|
35
|
+
IF NOT FOUND OR submission.idempotency_key IS DISTINCT FROM plan.submission_key
|
|
36
|
+
OR submission.result_json::jsonb->>'change_set' IS DISTINCT FROM NEW.id
|
|
37
|
+
OR submission.result_json::jsonb->>'source_id' IS DISTINCT FROM submission.source_id
|
|
38
|
+
OR submission.result_json::jsonb->>'state' IS DISTINCT FROM 'committed'
|
|
39
|
+
OR current_setting('vault.reason',true) IS DISTINCT FROM request->>'reason'
|
|
40
|
+
OR EXISTS(SELECT 1 FROM agent.reversions WHERE submission_id=NEW.id) THEN
|
|
41
|
+
RAISE EXCEPTION '任务提交缺少一致且未撤销的真实业务结果';
|
|
42
|
+
END IF;
|
|
43
|
+
-- 本版仍复用普通 submit 的原文快照;核对实际快照,不能只凭回调返回的 ID 记作成功。
|
|
44
|
+
SELECT * INTO source FROM public.journal_entries WHERE id=submission.source_id;
|
|
45
|
+
IF NOT FOUND OR source.deleted_at IS NOT NULL OR source.author_id IS DISTINCT FROM request->'source'->>'author_id'
|
|
46
|
+
OR source.about_person_id IS DISTINCT FROM request->'source'->>'about_person_id'
|
|
47
|
+
OR source.written_at IS DISTINCT FROM request->'source'->>'written_at'
|
|
48
|
+
OR source.title IS DISTINCT FROM request->'source'->>'title' OR source.content IS DISTINCT FROM request->'source'->>'content'
|
|
49
|
+
OR source.sensitivity IS DISTINCT FROM plan.max_sensitivity OR lease.max_sensitivity IS DISTINCT FROM plan.max_sensitivity THEN
|
|
50
|
+
RAISE EXCEPTION '任务提交的真实原文快照或处理范围不一致';
|
|
51
|
+
END IF;
|
|
52
|
+
RETURN NEW;
|
|
53
|
+
END $$;
|
|
54
|
+
REVOKE ALL ON FUNCTION public.vault_task_submission_guard() FROM PUBLIC;
|
|
55
|
+
DO $$ DECLARE name text; g record; BEGIN
|
|
56
|
+
FOREACH name IN ARRAY ARRAY['task_submissions'] LOOP
|
|
57
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
58
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
59
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_identity BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_registration_guard()',name);
|
|
60
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(''id'')',name);
|
|
61
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
62
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
63
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid=to_regclass('agent.'||name) AND a.grantee<>c.relowner LOOP
|
|
64
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
|
|
65
|
+
END LOOP;
|
|
66
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
67
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
68
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
69
|
+
END LOOP;
|
|
70
|
+
END LOOP;
|
|
71
|
+
END $$;
|
|
72
|
+
CREATE TRIGGER trg_task_submissions_submission BEFORE INSERT ON agent.task_submissions FOR EACH ROW EXECUTE FUNCTION public.vault_task_submission_guard();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
-- 任务尝试完成历史只追加;成功必须关联真实结果,失败不改写已提交业务,也不创建备份。
|
|
2
|
+
SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
IF NOT public.vault_agent_writes_enabled() THEN RAISE EXCEPTION 'Agent 写入已停用,请在明确的模块维护中核对后再升级任务完成结构'; END IF;
|
|
5
|
+
END $$;
|
|
6
|
+
CREATE TABLE agent.task_completions (
|
|
7
|
+
id text PRIMARY KEY CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
|
|
8
|
+
actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),task_id text NOT NULL,
|
|
9
|
+
attempt integer NOT NULL CHECK(attempt>0),lease_event_id text NOT NULL,
|
|
10
|
+
lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat')),
|
|
11
|
+
outcome text NOT NULL CHECK(outcome IN ('succeeded','failed')),
|
|
12
|
+
fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
|
|
13
|
+
payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),created_at text NOT NULL,
|
|
14
|
+
UNIQUE(actor,task_id,attempt),UNIQUE(actor,id),
|
|
15
|
+
FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
|
|
16
|
+
FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind),
|
|
17
|
+
CHECK((payload_json::jsonb->>'format_version') IS NOT DISTINCT FROM '1'),
|
|
18
|
+
CHECK((payload_json::jsonb->>'task_id') IS NOT DISTINCT FROM task_id),
|
|
19
|
+
CHECK((payload_json::jsonb->>'attempt') IS NOT DISTINCT FROM attempt::text),
|
|
20
|
+
CHECK((payload_json::jsonb->>'outcome') IS NOT DISTINCT FROM outcome),
|
|
21
|
+
CHECK(NOT (payload_json::jsonb ? 'lease_token') AND NOT (payload_json::jsonb ? 'id'))
|
|
22
|
+
);
|
|
23
|
+
CREATE FUNCTION public.vault_task_completion_guard() RETURNS trigger
|
|
24
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
25
|
+
DECLARE lease agent.task_lease_events%ROWTYPE; task agent.tasks%ROWTYPE; bound agent.task_submissions%ROWTYPE; body jsonb:=NEW.payload_json::jsonb;
|
|
26
|
+
BEGIN
|
|
27
|
+
IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
|
|
28
|
+
PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
29
|
+
SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
|
|
30
|
+
IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind OR lease.attempt IS DISTINCT FROM NEW.attempt
|
|
31
|
+
OR lease.lease_until::timestamptz<=clock_timestamp() THEN RAISE EXCEPTION '任务完成需要当前有效租约'; END IF;
|
|
32
|
+
SELECT * INTO task FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
|
|
33
|
+
IF NOT FOUND OR task.max_sensitivity IS DISTINCT FROM lease.max_sensitivity THEN RAISE EXCEPTION '任务完成范围与登记不一致'; END IF;
|
|
34
|
+
IF NEW.outcome='succeeded' THEN
|
|
35
|
+
IF body->>'source_revision' IS DISTINCT FROM task.descriptor_json::jsonb->'source'->>'revision'
|
|
36
|
+
OR jsonb_typeof(body->'submission_ids') IS DISTINCT FROM 'array' OR jsonb_typeof(body->'analysis_ids') IS DISTINCT FROM 'array'
|
|
37
|
+
OR jsonb_array_length(body->'analysis_ids')<>0 THEN RAISE EXCEPTION '任务成功缺少明确来源版本或包含未支持分析结果'; END IF;
|
|
38
|
+
SELECT * INTO bound FROM agent.task_submissions WHERE actor=NEW.actor AND task_id=NEW.task_id;
|
|
39
|
+
IF FOUND THEN
|
|
40
|
+
IF jsonb_array_length(body->'submission_ids')<>1 OR body->'submission_ids'->>0 IS DISTINCT FROM bound.id
|
|
41
|
+
OR body ? 'no_change_reason' OR EXISTS(SELECT 1 FROM agent.reversions WHERE submission_id=bound.id) THEN
|
|
42
|
+
RAISE EXCEPTION '任务成功结果与真实绑定不一致或已经撤销'; END IF;
|
|
43
|
+
ELSE
|
|
44
|
+
IF jsonb_array_length(body->'submission_ids')<>0 OR length(btrim(coalesce(body->>'no_change_reason',''))) NOT BETWEEN 1 AND 2000
|
|
45
|
+
OR EXISTS(SELECT 1 FROM agent.task_plans WHERE actor=NEW.actor AND task_id=NEW.task_id)
|
|
46
|
+
OR EXISTS(SELECT 1 FROM agent.submissions WHERE actor=NEW.actor AND idempotency_key='task:'||NEW.task_id||':submit:v1') THEN
|
|
47
|
+
RAISE EXCEPTION '任务成功不能跳过原计划或未知业务结果'; END IF;
|
|
48
|
+
END IF;
|
|
49
|
+
ELSE
|
|
50
|
+
-- 固定分类是执行器报告,不作为来源变化已核实或业务撤销的证明。
|
|
51
|
+
IF body->>'error_code' IS NULL OR body->>'error_code' NOT IN ('EXECUTOR_UNAVAILABLE','SOURCE_CHANGED','BUSINESS_CONFLICT','EXECUTION_FAILED') THEN
|
|
52
|
+
RAISE EXCEPTION '任务失败分类无效'; END IF;
|
|
53
|
+
END IF;
|
|
54
|
+
RETURN NEW;
|
|
55
|
+
END $$;
|
|
56
|
+
REVOKE ALL ON FUNCTION public.vault_task_completion_guard() FROM PUBLIC;
|
|
57
|
+
CREATE FUNCTION public.vault_task_open_guard() RETURNS trigger
|
|
58
|
+
LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
|
|
59
|
+
DECLARE attempt_no integer;
|
|
60
|
+
BEGIN
|
|
61
|
+
IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
|
|
62
|
+
PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
|
|
63
|
+
IF TG_TABLE_NAME='task_lease_events' THEN attempt_no:=NEW.attempt;
|
|
64
|
+
ELSE SELECT e.attempt INTO attempt_no FROM agent.task_lease_events e WHERE e.actor=NEW.actor AND e.task_id=NEW.task_id ORDER BY e.sequence DESC LIMIT 1; END IF;
|
|
65
|
+
IF EXISTS(SELECT 1 FROM agent.task_completions WHERE actor=NEW.actor AND task_id=NEW.task_id AND attempt=attempt_no) THEN
|
|
66
|
+
RAISE EXCEPTION '任务当前尝试已完成,不能继续租约工作';
|
|
67
|
+
END IF;
|
|
68
|
+
RETURN NEW;
|
|
69
|
+
END $$;
|
|
70
|
+
REVOKE ALL ON FUNCTION public.vault_task_open_guard() FROM PUBLIC;
|
|
71
|
+
DO $$ DECLARE name text; g record; BEGIN
|
|
72
|
+
FOREACH name IN ARRAY ARRAY['task_completions'] LOOP
|
|
73
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_guard BEFORE INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
74
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
|
|
75
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_identity BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_registration_guard()',name);
|
|
76
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_audit AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_audit(''id'')',name);
|
|
77
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_version AFTER INSERT OR UPDATE OR DELETE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_touch()',name);
|
|
78
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
|
|
79
|
+
FOR g IN SELECT DISTINCT r.rolname FROM pg_class c CROSS JOIN LATERAL aclexplode(c.relacl) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid=to_regclass('agent.'||name) AND a.grantee<>c.relowner LOOP
|
|
80
|
+
EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
|
|
81
|
+
END LOOP;
|
|
82
|
+
FOR g IN SELECT DISTINCT r.rolname,a.privilege_type FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) a JOIN pg_roles r ON r.oid=a.grantee WHERE c.oid='public.persons'::regclass AND a.privilege_type IN ('SELECT','INSERT') LOOP
|
|
83
|
+
EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
|
|
84
|
+
EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
|
|
85
|
+
END LOOP;
|
|
86
|
+
END LOOP;
|
|
87
|
+
END $$;
|
|
88
|
+
CREATE TRIGGER trg_task_completions_completion BEFORE INSERT ON agent.task_completions FOR EACH ROW EXECUTE FUNCTION public.vault_task_completion_guard();
|
|
89
|
+
DO $$ DECLARE name text; BEGIN
|
|
90
|
+
FOREACH name IN ARRAY ARRAY['task_lease_events','task_plans','task_submissions'] LOOP
|
|
91
|
+
EXECUTE format('CREATE TRIGGER trg_%1$s_open BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_open_guard()',name);
|
|
92
|
+
END LOOP;
|
|
93
|
+
END $$;
|