@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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/assets/migrations-pg/0100_baseline.sql +1115 -0
  4. package/assets/migrations-pg/0101_functions.sql +93 -0
  5. package/assets/migrations-pg/0102_views.sql +2425 -0
  6. package/assets/migrations-pg/0103_constraints.sql +147 -0
  7. package/assets/migrations-pg/0104_protection.sql +33 -0
  8. package/assets/migrations-pg/0105_bocom_credit_card.sql +43 -0
  9. package/assets/migrations-pg/0106_psbc_debit_card.sql +87 -0
  10. package/assets/migrations-pg/0107_tailong_debit_card.sql +87 -0
  11. package/assets/migrations-pg/0108_ccb_debit_card.sql +87 -0
  12. package/assets/migrations-pg/0109_ccb_credit_card.sql +63 -0
  13. package/assets/migrations-pg/0110_icbc_debit_card.sql +87 -0
  14. package/assets/migrations-pg/0111_icbc_credit_card.sql +66 -0
  15. package/assets/migrations-pg/0112_boc_debit_card.sql +88 -0
  16. package/assets/migrations-pg/0113_jsb_debit_card.sql +88 -0
  17. package/assets/migrations-pg/0114_cmbc_debit_card.sql +88 -0
  18. package/assets/migrations-pg/0115_hzbank_debit_card.sql +88 -0
  19. package/assets/migrations-pg/0116_unified_bank_documents.sql +197 -0
  20. package/assets/migrations-pg/0117_agent_submissions.sql +50 -0
  21. package/assets/migrations-pg/0118_agent_reversions_analysis.sql +34 -0
  22. package/assets/migrations-pg/0119_agent_extension_registry.sql +72 -0
  23. package/assets/migrations-pg/0120_agent_record_sources.sql +22 -0
  24. package/assets/migrations-pg/0121_agent_extension_deprecation.sql +30 -0
  25. package/assets/migrations-pg/0122_agent_extension_controlled_revert.sql +107 -0
  26. package/assets/migrations-pg/0123_agent_extension_versions.sql +54 -0
  27. package/assets/migrations-pg/0124_agent_extension_retirement_groups.sql +26 -0
  28. package/assets/migrations-pg/0125_agent_candidate_reviews.sql +30 -0
  29. package/assets/migrations-pg/0126_agent_module_lifecycle.sql +93 -0
  30. package/assets/migrations-pg/0127_agent_tasks.sql +64 -0
  31. package/assets/migrations-pg/0128_agent_task_leases.sql +80 -0
  32. package/assets/migrations-pg/0129_agent_task_plans.sql +66 -0
  33. package/assets/migrations-pg/0130_agent_task_submissions.sql +72 -0
  34. package/assets/migrations-pg/0131_agent_task_completions.sql +93 -0
  35. package/assets/migrations-pg/0132_agent_task_changes.sql +113 -0
  36. package/assets/migrations-pg/0133_agent_imported_source_snapshots.sql +152 -0
  37. package/assets/migrations-pg/0134_agent_analysis_datasets.sql +6 -0
  38. package/assets/migrations-pg/0135_agent_maintenance_tasks.sql +139 -0
  39. package/assets/migrations-pg/0136_agent_task_structures.sql +66 -0
  40. package/assets/migrations-pg/0137_agent_personal_identity.sql +149 -0
  41. package/assets/migrations-pg/0138_agent_work_records.sql +24 -0
  42. package/assets/migrations-pg/0139_shared_files.sql +43 -0
  43. package/assets/migrations-pg/0140_shared_script_runs.sql +36 -0
  44. package/assets/migrations-pg/0141_payment_bank_links.sql +96 -0
  45. package/assets/migrations-pg/0142_consumption_order_types_and_places.sql +287 -0
  46. package/assets/migrations-pg/0143_skill_direct_write.sql +330 -0
  47. package/assets/migrations-pg/0144_skill_schema_archive.sql +73 -0
  48. package/assets/migrations-pg/0145_skill_ingest_wiki.sql +91 -0
  49. package/assets/migrations-pg/0146_skill_load.sql +17 -0
  50. package/assets/migrations-pg/0147_drop_agent_schema.sql +147 -0
  51. package/assets/migrations-pg/0148_ingest_lanes.sql +19 -0
  52. package/assets/migrations-pg/0149_account_ownership.sql +233 -0
  53. package/assets/migrations-pg/0150_bank_card_exact_link.sql +24 -0
  54. package/assets/migrations-pg/0151_account_ownership_enforce.sql +35 -0
  55. package/assets/migrations-pg/0152_account_ownership_views.sql +750 -0
  56. package/assets/migrations-pg/0153_web_frontend.sql +34 -0
  57. package/assets/migrations-pg/0154_audio_record_owners.sql +103 -0
  58. package/assets/migrations-pg/0155_ingest_knowledge_tables.sql +171 -0
  59. package/assets/migrations-pg/0156_batch_playbook_note.sql +17 -0
  60. package/assets/migrations-pg/0157_last_contact.sql +166 -0
  61. package/assets/migrations-pg/0158_refund_match_hash_join.sql +91 -0
  62. package/assets/migrations-pg/0159_fact_occurred_time.sql +88 -0
  63. package/assets/migrations-pg/0160_agent_notes.sql +118 -0
  64. package/assets/migrations-pg/0161_life_history.sql +314 -0
  65. package/assets/migrations-pg/0162_person_card.sql +427 -0
  66. package/assets/migrations-pg/0163_parking_drop_personal_defaults.sql +12 -0
  67. package/assets/native/darwin-arm64.node +0 -0
  68. package/assets/playbooks/audio.md +83 -0
  69. package/assets/playbooks/chat.md +37 -0
  70. package/assets/playbooks/common.md +66 -0
  71. package/assets/playbooks/intimate.md +19 -0
  72. package/assets/playbooks/journal.md +17 -0
  73. package/assets/playbooks/orders.md +15 -0
  74. package/assets/playbooks/payments.md +16 -0
  75. package/assets/playbooks/self.md +29 -0
  76. package/bin/install-codex-skill.mjs +77 -0
  77. package/bin/memoir.mjs +5 -0
  78. package/dist/cli.js +34062 -0
  79. package/package.json +40 -4
  80. package/release-manifest.json +432 -0
  81. package/skill/SKILL.md +120 -0
  82. package/skill/references//345/256/211/350/243/205/344/275/215/347/275/256.md +21 -0
  83. package/skill/references//346/225/260/346/215/256/345/217/243/345/276/204.md +36 -0
  84. package/skill/scripts/memoir.mjs +82 -0
@@ -0,0 +1,113 @@
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_changes (
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
+ sequence integer NOT NULL CHECK(sequence>0),attempt integer NOT NULL CHECK(attempt>=0),
10
+ action text NOT NULL CHECK(action IN ('cancel','retry')),
11
+ idempotency_key text NOT NULL CHECK(length(idempotency_key) BETWEEN 1 AND 200),
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,id),UNIQUE(actor,task_id,sequence),UNIQUE(actor,idempotency_key),
15
+ FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
16
+ CHECK((payload_json::jsonb->>'id') IS NOT DISTINCT FROM task_id),
17
+ CHECK((payload_json::jsonb->>'action') IS NOT DISTINCT FROM action),
18
+ CHECK((payload_json::jsonb->>'idempotency_key') IS NOT DISTINCT FROM idempotency_key),
19
+ CHECK(action<>'retry' OR attempt>0)
20
+ );
21
+ ALTER TABLE agent.task_lease_events ADD COLUMN control_id text;
22
+ ALTER TABLE agent.task_lease_events ADD CONSTRAINT task_lease_events_actor_control_id_key UNIQUE(actor,control_id);
23
+ ALTER TABLE agent.task_lease_events ADD CONSTRAINT task_lease_events_control_fkey FOREIGN KEY(actor,control_id) REFERENCES agent.task_changes(actor,id);
24
+ ALTER TABLE agent.task_lease_events DROP CONSTRAINT task_lease_events_kind_check;
25
+ ALTER TABLE agent.task_lease_events ADD CONSTRAINT task_lease_events_kind_check CHECK(kind IN ('claim','heartbeat','resume'));
26
+ ALTER TABLE agent.task_lease_events DROP CONSTRAINT task_lease_events_check;
27
+ ALTER TABLE agent.task_lease_events ADD CONSTRAINT task_lease_events_check CHECK((kind='claim' AND sequence=1 AND attempt=1) OR (kind='heartbeat' AND sequence>1 AND control_id IS NULL) OR (kind='resume' AND sequence>1 AND attempt>1 AND control_id IS NOT NULL));
28
+ ALTER TABLE agent.task_plans DROP CONSTRAINT task_plans_lease_kind_check;
29
+ ALTER TABLE agent.task_plans ADD CONSTRAINT task_plans_lease_kind_check CHECK(lease_kind IN ('claim','heartbeat','resume'));
30
+ ALTER TABLE agent.task_submissions DROP CONSTRAINT task_submissions_lease_kind_check;
31
+ ALTER TABLE agent.task_submissions ADD CONSTRAINT task_submissions_lease_kind_check CHECK(lease_kind IN ('claim','heartbeat','resume'));
32
+ ALTER TABLE agent.task_completions DROP CONSTRAINT task_completions_lease_kind_check;
33
+ ALTER TABLE agent.task_completions ADD CONSTRAINT task_completions_lease_kind_check CHECK(lease_kind IN ('claim','heartbeat','resume'));
34
+ CREATE FUNCTION public.vault_task_change_guard() RETURNS trigger
35
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
36
+ DECLARE previous agent.task_changes%ROWTYPE; lease agent.task_lease_events%ROWTYPE; completed text; has_lease boolean; body jsonb:=NEW.payload_json::jsonb;
37
+ BEGIN
38
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
39
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
40
+ SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
41
+ has_lease:=FOUND;
42
+ SELECT * INTO previous FROM agent.task_changes WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
43
+ IF NEW.sequence<>coalesce(previous.sequence,0)+1 THEN RAISE EXCEPTION '任务取消恢复序号不连续'; END IF;
44
+ SELECT outcome INTO completed FROM agent.task_completions WHERE actor=NEW.actor AND task_id=NEW.task_id AND attempt=coalesce(lease.attempt,0);
45
+ IF completed='succeeded' THEN RAISE EXCEPTION '任务已经成功完成,不能取消或重新执行'; END IF;
46
+ IF current_setting('vault.reason',true) IS DISTINCT FROM body->>'reason' THEN RAISE EXCEPTION '任务取消恢复与审计原因不一致'; END IF;
47
+ IF NEW.action='cancel' THEN
48
+ IF NEW.attempt<>coalesce(lease.attempt,0) OR previous.action='cancel' THEN RAISE EXCEPTION '任务已取消或尝试不一致'; END IF;
49
+ ELSE
50
+ IF NEW.attempt<>coalesce(lease.attempt,0)+1 OR NOT (coalesce(previous.action='cancel',false) OR coalesce(completed='failed',false) OR (has_lease AND lease.lease_until::timestamptz<=clock_timestamp())) THEN
51
+ RAISE EXCEPTION '任务仍有有效尝试或尚未需要恢复'; END IF;
52
+ END IF;
53
+ RETURN NEW;
54
+ END $$;
55
+ REVOKE ALL ON FUNCTION public.vault_task_change_guard() FROM PUBLIC;
56
+ CREATE OR REPLACE FUNCTION public.vault_task_lease_guard() RETURNS trigger
57
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
58
+ DECLARE previous agent.task_lease_events%ROWTYPE; maximum text; control agent.task_changes%ROWTYPE; has_previous boolean;
59
+ BEGIN
60
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
61
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
62
+ SELECT max_sensitivity INTO maximum FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
63
+ IF maximum IS NULL OR NEW.max_sensitivity IS DISTINCT FROM maximum THEN RAISE EXCEPTION '任务租约范围与登记不一致'; END IF;
64
+ IF NEW.lease_until::timestamptz<=clock_timestamp() OR NEW.lease_until::timestamptz>clock_timestamp()+interval '300 seconds' THEN RAISE EXCEPTION '任务租约时限无效'; END IF;
65
+ SELECT * INTO previous FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
66
+ has_previous:=FOUND;
67
+ SELECT * INTO control FROM agent.task_changes WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
68
+ IF NEW.control_id IS NOT NULL AND (control.id IS DISTINCT FROM NEW.control_id OR control.action IS DISTINCT FROM 'retry' OR control.attempt IS DISTINCT FROM NEW.attempt) THEN RAISE EXCEPTION '任务恢复租约缺少显式恢复历史'; END IF;
69
+ IF NEW.kind='claim' THEN
70
+ IF has_previous OR NEW.sequence<>1 OR NEW.attempt<>1 OR (control.id IS NOT NULL AND NEW.control_id IS NULL) THEN RAISE EXCEPTION '任务已经领取或取消,不能重新领取'; END IF;
71
+ ELSIF NEW.kind='resume' THEN
72
+ IF NOT has_previous OR NEW.sequence<>previous.sequence+1 OR NEW.attempt<>previous.attempt+1 OR NEW.control_id IS NULL OR NEW.token_hash=previous.token_hash THEN RAISE EXCEPTION '任务恢复尝试、证明或序号无效'; END IF;
73
+ ELSE
74
+ IF NOT has_previous OR NEW.sequence<>previous.sequence+1 OR NEW.attempt<>previous.attempt OR NEW.control_id IS NOT NULL
75
+ OR NEW.token_hash IS DISTINCT FROM previous.token_hash OR NEW.executor IS DISTINCT FROM previous.executor OR NEW.max_sensitivity IS DISTINCT FROM previous.max_sensitivity
76
+ OR previous.lease_until::timestamptz<=clock_timestamp() OR NEW.lease_until<previous.lease_until THEN RAISE EXCEPTION '任务续期租约已失效或历史不连续'; END IF;
77
+ END IF;
78
+ RETURN NEW;
79
+ END $$;
80
+ REVOKE ALL ON FUNCTION public.vault_task_lease_guard() FROM PUBLIC;
81
+ CREATE OR REPLACE FUNCTION public.vault_task_open_guard() RETURNS trigger
82
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
83
+ DECLARE attempt_no integer; control agent.task_changes%ROWTYPE;
84
+ BEGIN
85
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
86
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
87
+ IF TG_TABLE_NAME='task_lease_events' THEN attempt_no:=NEW.attempt;
88
+ 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;
89
+ SELECT * INTO control FROM agent.task_changes WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
90
+ IF control.attempt>attempt_no OR (control.action='cancel' AND control.attempt>=attempt_no) THEN RAISE EXCEPTION '任务已取消或旧尝试已被恢复'; END IF;
91
+ IF EXISTS(SELECT 1 FROM agent.task_completions WHERE actor=NEW.actor AND task_id=NEW.task_id AND attempt=attempt_no) THEN RAISE EXCEPTION '任务当前尝试已完成,不能继续租约工作'; END IF;
92
+ RETURN NEW;
93
+ END $$;
94
+ REVOKE ALL ON FUNCTION public.vault_task_open_guard() FROM PUBLIC;
95
+ DO $$ DECLARE name text; g record; BEGIN
96
+ FOREACH name IN ARRAY ARRAY['task_changes'] LOOP
97
+ 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);
98
+ EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
99
+ 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);
100
+ 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);
101
+ 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);
102
+ EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
103
+ 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
104
+ EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
105
+ END LOOP;
106
+ 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
107
+ EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
108
+ EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
109
+ END LOOP;
110
+ END LOOP;
111
+ END $$;
112
+ CREATE TRIGGER trg_task_changes_change BEFORE INSERT ON agent.task_changes FOR EACH ROW EXECUTE FUNCTION public.vault_task_change_guard();
113
+ CREATE TRIGGER trg_task_completions_open BEFORE INSERT ON agent.task_completions FOR EACH ROW EXECUTE FUNCTION public.vault_task_open_guard();
@@ -0,0 +1,152 @@
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.source_snapshots (
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]+$'),
9
+ payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),created_at text NOT NULL,
10
+ UNIQUE(actor,id), CHECK(payload_json::jsonb->>'kind' IS NOT DISTINCT FROM 'imported_source')
11
+ );
12
+ -- 默认值明确保留旧随笔引用;生成列和外键保证两类来源真实存在,而非只信任文本类型。
13
+ ALTER TABLE agent.submissions ADD COLUMN source_object text NOT NULL DEFAULT 'public.journal_entries'
14
+ CHECK(source_object IN ('public.journal_entries','agent.source_snapshots'));
15
+ ALTER TABLE agent.submissions DROP CONSTRAINT submissions_source_id_fkey;
16
+ ALTER TABLE agent.submissions ADD COLUMN journal_source_id text GENERATED ALWAYS AS (CASE WHEN source_object='public.journal_entries' THEN source_id END) STORED;
17
+ ALTER TABLE agent.submissions ADD COLUMN snapshot_source_id text GENERATED ALWAYS AS (CASE WHEN source_object='agent.source_snapshots' THEN source_id END) STORED;
18
+ ALTER TABLE agent.submissions ADD CONSTRAINT submissions_journal_source_fkey FOREIGN KEY(journal_source_id) REFERENCES public.journal_entries(id);
19
+ ALTER TABLE agent.submissions ADD CONSTRAINT submissions_snapshot_source_fkey FOREIGN KEY(actor,snapshot_source_id) REFERENCES agent.source_snapshots(actor,id);
20
+ ALTER TABLE agent.submissions ADD CONSTRAINT submissions_source_key UNIQUE(id,source_object,source_id);
21
+ ALTER TABLE agent.candidates ADD COLUMN source_object text NOT NULL DEFAULT 'public.journal_entries'
22
+ CHECK(source_object IN ('public.journal_entries','agent.source_snapshots'));
23
+ ALTER TABLE agent.candidates DROP CONSTRAINT candidates_source_id_fkey;
24
+ ALTER TABLE agent.candidates ADD CONSTRAINT candidates_submission_source_fkey FOREIGN KEY(submission_id,source_object,source_id) REFERENCES agent.submissions(id,source_object,source_id);
25
+ CREATE FUNCTION public.vault_task_source_guard() RETURNS trigger
26
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
27
+ BEGIN
28
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
29
+ -- 更新结果 JSON/候选流程不能悄悄改换原文定位;旧证据和原提交的归属必须保持不变。
30
+ IF TG_OP='UPDATE' AND (NEW.source_id IS DISTINCT FROM OLD.source_id OR NEW.source_object IS DISTINCT FROM OLD.source_object) THEN
31
+ RAISE EXCEPTION 'Agent 原文定位不可覆盖';
32
+ END IF;
33
+ RETURN NEW;
34
+ END $$;
35
+ CREATE TRIGGER trg_submissions_source BEFORE UPDATE ON agent.submissions FOR EACH ROW EXECUTE FUNCTION public.vault_task_source_guard();
36
+ CREATE TRIGGER trg_candidates_source BEFORE UPDATE ON agent.candidates FOR EACH ROW EXECUTE FUNCTION public.vault_task_source_guard();
37
+ CREATE FUNCTION public.vault_task_snapshot_guard() RETURNS trigger
38
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
39
+ DECLARE request jsonb; descriptor jsonb; object text; actual jsonb; keys jsonb; supplied jsonb; source_task_id text; lease agent.task_lease_events%ROWTYPE;
40
+ BEGIN
41
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
42
+ SELECT p.payload_json::jsonb->'request'->'source',t.descriptor_json::jsonb,p.task_id INTO request,descriptor,source_task_id
43
+ FROM agent.task_plans p JOIN agent.tasks t ON t.actor=p.actor AND t.id=p.task_id
44
+ WHERE p.actor=NEW.actor AND p.id=current_setting('vault.change_set',true);
45
+ IF NOT FOUND OR request->>'kind' IS DISTINCT FROM 'imported_source' OR request IS DISTINCT FROM NEW.payload_json::jsonb
46
+ OR request->'source' IS DISTINCT FROM descriptor->'source' OR request->>'sensitivity' IS DISTINCT FROM descriptor->>'max_sensitivity' THEN
47
+ RAISE EXCEPTION '来源快照必须对应冻结任务的真实原文与处理范围';
48
+ END IF;
49
+ SELECT * INTO lease FROM agent.task_lease_events l WHERE l.actor=NEW.actor AND l.task_id=source_task_id ORDER BY sequence DESC LIMIT 1;
50
+ IF NOT FOUND OR lease.lease_until::timestamptz<=clock_timestamp()
51
+ OR EXISTS(SELECT 1 FROM agent.task_completions c WHERE c.actor=NEW.actor AND c.task_id=source_task_id AND c.attempt=lease.attempt)
52
+ OR (SELECT ch.action FROM agent.task_changes ch WHERE ch.actor=NEW.actor AND ch.task_id=source_task_id ORDER BY sequence DESC LIMIT 1)='cancel' THEN
53
+ RAISE EXCEPTION '来源快照需要当前尚未关闭的有效租约';
54
+ END IF;
55
+ object:=request->'source'->>'object';
56
+ IF object NOT IN ('public.wechat_messages','public.audio_records','public.audio_sentences','public.audio_summaries','public.orders','public.order_items','public.payments','public.train_tickets','public.bank_card_statements','public.bank_card_transactions','public.credit_card_statements','public.credit_card_transactions','public.bank_document_records') THEN
57
+ RAISE EXCEPTION '任务来源对象不支持';
58
+ END IF;
59
+ SELECT jsonb_agg(a.attname ORDER BY a.attname) INTO keys FROM pg_index i JOIN pg_attribute a ON a.attrelid=i.indrelid AND a.attnum=ANY(i.indkey) WHERE i.indrelid=to_regclass(object) AND i.indisprimary;
60
+ SELECT jsonb_agg(k ORDER BY k) INTO supplied FROM jsonb_object_keys(request->'source'->'primary_key') k;
61
+ IF keys IS NULL OR keys IS DISTINCT FROM supplied THEN RAISE EXCEPTION '任务来源主键不完整'; END IF;
62
+ -- object 已经封闭白名单,主键值使用 USING 绑定;保留原始表全部字段,不套用虚构作者/日期。
63
+ EXECUTE format('SELECT to_jsonb(s) FROM %s s WHERE to_jsonb(s) @> $1',object) INTO actual USING request->'source'->'primary_key';
64
+ IF actual IS NULL OR actual IS DISTINCT FROM request->'row' OR actual->>'deleted_at' IS NOT NULL THEN
65
+ RAISE EXCEPTION '任务来源原始行在提交前变化';
66
+ END IF;
67
+ RETURN NEW;
68
+ END $$;
69
+
70
+ CREATE OR REPLACE FUNCTION public.vault_task_plan_guard() RETURNS trigger
71
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
72
+ DECLARE lease agent.task_lease_events%ROWTYPE; task agent.tasks%ROWTYPE;
73
+ BEGIN
74
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
75
+ -- 同一任务不能换租约另存计划;只追加约束和唯一键共同保留首个明确请求。
76
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
77
+ SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
78
+ IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind
79
+ OR lease.lease_until::timestamptz<=clock_timestamp() THEN
80
+ RAISE EXCEPTION '任务计划需要当前有效租约';
81
+ END IF;
82
+ SELECT * INTO task FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
83
+ IF NOT FOUND
84
+ OR task.descriptor_json::jsonb->'source'->>'revision' IS DISTINCT FROM NEW.source_revision
85
+ OR task.max_sensitivity IS DISTINCT FROM NEW.max_sensitivity OR lease.max_sensitivity IS DISTINCT FROM NEW.max_sensitivity
86
+ OR current_setting('vault.reason',true) IS DISTINCT FROM NEW.payload_json::jsonb->'request'->>'reason' THEN
87
+ RAISE EXCEPTION '任务计划与来源快照、处理范围或审计原因不一致';
88
+ END IF;
89
+ IF task.descriptor_json::jsonb->'source'->>'object' IS DISTINCT FROM 'public.journal_entries' AND
90
+ (NEW.payload_json::jsonb->'request'->'source'->>'kind' IS DISTINCT FROM 'imported_source'
91
+ OR NEW.payload_json::jsonb->'request'->'source'->'source' IS DISTINCT FROM task.descriptor_json::jsonb->'source') THEN
92
+ RAISE EXCEPTION '导入任务计划必须保存真实来源定位';
93
+ END IF;
94
+ RETURN NEW;
95
+ END $$;
96
+ CREATE OR REPLACE FUNCTION public.vault_task_submission_guard() RETURNS trigger
97
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
98
+ DECLARE lease agent.task_lease_events%ROWTYPE; plan agent.task_plans%ROWTYPE; submission agent.submissions%ROWTYPE; source public.journal_entries%ROWTYPE; request jsonb;
99
+ BEGIN
100
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
101
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
102
+ SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
103
+ IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind
104
+ OR lease.lease_until::timestamptz<=clock_timestamp() THEN RAISE EXCEPTION '任务提交需要当前有效租约'; END IF;
105
+ SELECT * INTO plan FROM agent.task_plans WHERE actor=NEW.actor AND id=NEW.id;
106
+ IF NOT FOUND OR plan.task_id IS DISTINCT FROM NEW.task_id OR plan.fingerprint IS DISTINCT FROM NEW.plan_revision
107
+ OR current_setting('vault.change_set',true) IS DISTINCT FROM NEW.id THEN RAISE EXCEPTION '任务提交必须绑定原计划与审计批次'; END IF;
108
+ request:=plan.payload_json::jsonb->'request';
109
+ SELECT * INTO submission FROM agent.submissions WHERE actor=NEW.actor AND id=NEW.id;
110
+ IF NOT FOUND OR submission.idempotency_key IS DISTINCT FROM plan.submission_key
111
+ OR submission.result_json::jsonb->>'change_set' IS DISTINCT FROM NEW.id
112
+ OR submission.result_json::jsonb->>'source_id' IS DISTINCT FROM submission.source_id
113
+ OR submission.result_json::jsonb->>'state' IS DISTINCT FROM 'committed'
114
+ OR current_setting('vault.reason',true) IS DISTINCT FROM request->>'reason'
115
+ OR EXISTS(SELECT 1 FROM agent.reversions WHERE submission_id=NEW.id) THEN
116
+ RAISE EXCEPTION '任务提交缺少一致且未撤销的真实业务结果';
117
+ END IF;
118
+ IF submission.source_object='agent.source_snapshots' THEN
119
+ IF request->'source'->>'kind' IS DISTINCT FROM 'imported_source'
120
+ OR NOT EXISTS(SELECT 1 FROM agent.source_snapshots s WHERE s.id=submission.source_id AND s.actor=NEW.actor AND s.payload_json::jsonb=request->'source')
121
+ OR submission.result_json::jsonb->>'source_object' IS DISTINCT FROM 'agent.source_snapshots'
122
+ OR lease.max_sensitivity IS DISTINCT FROM plan.max_sensitivity THEN RAISE EXCEPTION '任务缺少真实导入来源快照'; END IF;
123
+ ELSE
124
+ -- 本版仍复用普通 submit 的原文快照;核对实际快照,不能只凭回调返回的 ID 记作成功。
125
+ SELECT * INTO source FROM public.journal_entries WHERE id=submission.source_id;
126
+ IF NOT FOUND OR source.deleted_at IS NOT NULL OR source.author_id IS DISTINCT FROM request->'source'->>'author_id'
127
+ OR source.about_person_id IS DISTINCT FROM request->'source'->>'about_person_id'
128
+ OR source.written_at IS DISTINCT FROM request->'source'->>'written_at'
129
+ OR source.title IS DISTINCT FROM request->'source'->>'title' OR source.content IS DISTINCT FROM request->'source'->>'content'
130
+ OR source.sensitivity IS DISTINCT FROM plan.max_sensitivity OR lease.max_sensitivity IS DISTINCT FROM plan.max_sensitivity THEN
131
+ RAISE EXCEPTION '任务提交的真实原文快照或处理范围不一致';
132
+ END IF;
133
+ END IF;
134
+ RETURN NEW;
135
+ END $$;
136
+
137
+ REVOKE ALL ON FUNCTION public.vault_task_source_guard() FROM PUBLIC;
138
+ REVOKE ALL ON FUNCTION public.vault_task_snapshot_guard() FROM PUBLIC;
139
+ DO $$ DECLARE name text; g record; BEGIN
140
+ FOREACH name IN ARRAY ARRAY['source_snapshots'] LOOP
141
+ 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);
142
+ EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
143
+ 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);
144
+ 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);
145
+ 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);
146
+ EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
147
+ 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
148
+ EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
149
+ END LOOP;
150
+ END LOOP;
151
+ END $$;
152
+ CREATE TRIGGER trg_source_snapshots_snapshot BEFORE INSERT ON agent.source_snapshots FOR EACH ROW EXECUTE FUNCTION public.vault_task_snapshot_guard();
@@ -0,0 +1,6 @@
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
+ ALTER TABLE agent.analysis_reports ADD COLUMN dataset_json text CHECK(dataset_json IS NULL OR public.vault_json_valid(dataset_json));
@@ -0,0 +1,139 @@
1
+ -- 维护任务保存独立只增计划/真实结果,复用已发布租约、取消与审计;不执行模型或备份。
2
+ SELECT pg_advisory_xact_lock(hashtext('relation-vault:write'));
3
+ DO $$ DECLARE c record; BEGIN
4
+ IF NOT public.vault_agent_writes_enabled() THEN RAISE EXCEPTION 'Agent 写入已停用,请明确维护后升级维护任务'; END IF;
5
+ -- 旧迁移为匿名 kind CHECK,通过目录确定唯一目标,不假定 PostgreSQL 自动命名编号。
6
+ FOR c IN SELECT conname FROM pg_constraint WHERE conrelid='agent.tasks'::regclass AND contype='c' AND pg_get_constraintdef(oid) LIKE '%extract_source%' LOOP
7
+ EXECUTE format('ALTER TABLE agent.tasks DROP CONSTRAINT %I',c.conname);
8
+ END LOOP;
9
+ END $$;
10
+ ALTER TABLE agent.tasks ADD CONSTRAINT tasks_kind_check CHECK((descriptor_json::jsonb->>'kind' IN ('extract_source','refresh_analysis','refresh_wiki')) IS TRUE);
11
+ ALTER TABLE agent.tasks ADD CONSTRAINT tasks_maintenance_source_check CHECK((
12
+ (descriptor_json::jsonb->>'kind'='extract_source' AND descriptor_json::jsonb->'source'->>'object'<>'agent.analysis_reports' AND NOT descriptor_json::jsonb ? 'target') OR
13
+ (descriptor_json::jsonb->>'kind' IN ('refresh_analysis','refresh_wiki') AND descriptor_json::jsonb->'source'->>'object'='agent.analysis_reports' AND ((descriptor_json::jsonb->>'kind'='refresh_wiki')=(descriptor_json::jsonb ? 'target')))
14
+ ) IS TRUE);
15
+ CREATE TABLE agent.task_maintenance_plans (
16
+ 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]+$'), task_id text NOT NULL,
17
+ lease_event_id text NOT NULL, lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat','resume')),
18
+ source_revision text NOT NULL CHECK(source_revision ~ '^[a-f0-9]{64}$'), fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),
19
+ payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)), created_at text NOT NULL,
20
+ UNIQUE(actor,id), UNIQUE(actor,task_id),
21
+ FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
22
+ FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind)
23
+ );
24
+ CREATE TABLE agent.task_maintenance_results (
25
+ 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]+$'),task_id text NOT NULL,
26
+ plan_id text NOT NULL,plan_revision text NOT NULL CHECK(plan_revision ~ '^[a-f0-9]{64}$'),
27
+ lease_event_id text NOT NULL,lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat','resume')),
28
+ source_report_id text NOT NULL REFERENCES agent.analysis_reports(id),analysis_id text REFERENCES agent.analysis_reports(id),
29
+ wiki_slug text,wiki_rev integer,block_key text,created_at text NOT NULL,
30
+ UNIQUE(actor,id),UNIQUE(actor,task_id),UNIQUE(actor,plan_id),
31
+ FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
32
+ FOREIGN KEY(actor,plan_id) REFERENCES agent.task_maintenance_plans(actor,id),
33
+ FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind),
34
+ FOREIGN KEY(wiki_slug,wiki_rev) REFERENCES public.wiki_revisions(slug,rev),
35
+ CHECK((analysis_id IS NOT NULL AND wiki_slug IS NULL AND wiki_rev IS NULL AND block_key IS NULL AND analysis_id<>source_report_id) OR (analysis_id IS NULL AND wiki_slug IS NOT NULL AND wiki_rev>0 AND block_key ~ '^[a-z][a-z0-9_-]{0,99}$'))
36
+ );
37
+ -- 一份旧报告至多一个后继版本;并发处理器不得各自替换同一旧分析造成分叉。
38
+ CREATE UNIQUE INDEX ix_task_analysis_successor ON agent.task_maintenance_results(source_report_id) WHERE analysis_id IS NOT NULL;
39
+ CREATE INDEX ix_task_wiki_bindings ON agent.task_maintenance_results(wiki_slug,block_key,created_at DESC,id DESC);
40
+ CREATE FUNCTION public.vault_task_maintenance_guard() RETURNS trigger
41
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
42
+ DECLARE task agent.tasks%ROWTYPE; lease agent.task_lease_events%ROWTYPE; plan agent.task_maintenance_plans%ROWTYPE; kind text;
43
+ BEGIN
44
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
45
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
46
+ SELECT * INTO task FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
47
+ kind:=task.descriptor_json::jsonb->>'kind';
48
+ IF kind IS NULL OR kind NOT IN ('refresh_analysis','refresh_wiki') THEN RAISE EXCEPTION '维护记录需要分析或 Wiki 任务'; END IF;
49
+ SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
50
+ IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind OR lease.lease_until::timestamptz<=clock_timestamp() THEN RAISE EXCEPTION '维护记录需要当前有效租约'; END IF;
51
+ IF TG_TABLE_NAME='task_maintenance_plans' THEN
52
+ IF NEW.source_revision IS DISTINCT FROM task.descriptor_json::jsonb->'source'->>'revision' OR NEW.payload_json::jsonb->>'action' IS DISTINCT FROM (CASE kind WHEN 'refresh_analysis' THEN 'analysis' ELSE 'wiki' END) THEN RAISE EXCEPTION '维护计划与来源或种类不符'; END IF;
53
+ ELSE
54
+ SELECT * INTO plan FROM agent.task_maintenance_plans WHERE actor=NEW.actor AND id=NEW.plan_id;
55
+ IF NOT FOUND OR plan.task_id IS DISTINCT FROM NEW.task_id OR plan.fingerprint IS DISTINCT FROM NEW.plan_revision OR NEW.source_report_id IS DISTINCT FROM task.descriptor_json::jsonb->'source'->'primary_key'->>'id' THEN RAISE EXCEPTION '维护结果缺少真实来源计划'; END IF;
56
+ IF kind='refresh_analysis' THEN
57
+ IF NEW.analysis_id IS NULL OR NOT EXISTS(SELECT 1 FROM agent.analysis_reports r WHERE r.id=NEW.analysis_id AND r.actor=NEW.actor AND r.idempotency_key='task:'||NEW.task_id||':analysis:v1') THEN RAISE EXCEPTION '维护结果缺少真实分析'; END IF;
58
+ ELSE
59
+ IF NEW.analysis_id IS NOT NULL OR NEW.wiki_slug IS DISTINCT FROM task.descriptor_json::jsonb->'target'->>'slug' OR NEW.block_key IS DISTINCT FROM task.descriptor_json::jsonb->'target'->>'block_key'
60
+ OR NOT EXISTS(SELECT 1 FROM public.wiki_revisions w WHERE w.slug=NEW.wiki_slug AND w.rev=NEW.wiki_rev AND w.actor=NEW.actor AND position('<!-- agent:analysis:'||NEW.block_key||' -->' IN w.body_md)>0 AND position(NEW.source_report_id IN w.body_md)>0) THEN RAISE EXCEPTION '维护结果缺少真实 Wiki 版本'; END IF;
61
+ END IF;
62
+ END IF;
63
+ RETURN NEW;
64
+ END $$;
65
+ REVOKE ALL ON FUNCTION public.vault_task_maintenance_guard() FROM PUBLIC;
66
+ DO $$ DECLARE name text; g record; BEGIN
67
+ FOREACH name IN ARRAY ARRAY['task_maintenance_plans','task_maintenance_results'] LOOP
68
+ 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);
69
+ EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
70
+ 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);
71
+ 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);
72
+ 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);
73
+ EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
74
+ 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
75
+ EXECUTE format('REVOKE ALL ON agent.%I FROM %I',name,g.rolname);
76
+ END LOOP;
77
+ 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
78
+ EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
79
+ EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
80
+ END LOOP;
81
+ END LOOP;
82
+ END $$;
83
+ DO $$ DECLARE name text; BEGIN
84
+ FOREACH name IN ARRAY ARRAY['task_maintenance_plans','task_maintenance_results'] LOOP
85
+ EXECUTE format('CREATE TRIGGER trg_%1$s_maintenance BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_maintenance_guard()',name);
86
+ 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);
87
+ END LOOP;
88
+ END $$;
89
+ CREATE OR REPLACE FUNCTION public.vault_task_completion_guard() RETURNS trigger
90
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
91
+ DECLARE lease agent.task_lease_events%ROWTYPE; task agent.tasks%ROWTYPE; bound agent.task_submissions%ROWTYPE; body jsonb:=NEW.payload_json::jsonb;
92
+ BEGIN
93
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
94
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
95
+ SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
96
+ 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
97
+ OR lease.lease_until::timestamptz<=clock_timestamp() THEN RAISE EXCEPTION '任务完成需要当前有效租约'; END IF;
98
+ SELECT * INTO task FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
99
+ IF NOT FOUND OR task.max_sensitivity IS DISTINCT FROM lease.max_sensitivity THEN RAISE EXCEPTION '任务完成范围与登记不一致'; END IF;
100
+ IF NEW.outcome='succeeded' AND task.descriptor_json::jsonb->>'kind'<>'extract_source' THEN
101
+ IF body->>'source_revision' IS DISTINCT FROM task.descriptor_json::jsonb->'source'->>'revision'
102
+ OR jsonb_typeof(body->'submission_ids') IS DISTINCT FROM 'array' OR jsonb_array_length(body->'submission_ids')<>0
103
+ OR jsonb_typeof(body->'analysis_ids') IS DISTINCT FROM 'array' THEN RAISE EXCEPTION '维护完成来源或业务结果类型不符'; END IF;
104
+ IF EXISTS(SELECT 1 FROM agent.task_maintenance_results b WHERE b.actor=NEW.actor AND b.task_id=NEW.task_id) THEN
105
+ IF body ? 'no_change_reason' OR NOT EXISTS(
106
+ SELECT 1 FROM agent.task_maintenance_results b WHERE b.actor=NEW.actor AND b.task_id=NEW.task_id
107
+ AND body->'maintenance_ids'=jsonb_build_array(b.id)
108
+ AND body->'analysis_ids'=CASE WHEN b.analysis_id IS NULL THEN '[]'::jsonb ELSE jsonb_build_array(b.analysis_id) END
109
+ ) THEN RAISE EXCEPTION '维护完成必须精确关联真实结果'; END IF;
110
+ ELSE
111
+ IF coalesce(body->'maintenance_ids','[]'::jsonb)<>'[]'::jsonb OR body->'analysis_ids'<>'[]'::jsonb
112
+ OR length(btrim(coalesce(body->>'no_change_reason',''))) NOT BETWEEN 1 AND 2000
113
+ OR EXISTS(SELECT 1 FROM agent.task_maintenance_plans WHERE actor=NEW.actor AND task_id=NEW.task_id)
114
+ OR EXISTS(SELECT 1 FROM agent.analysis_reports WHERE actor=NEW.actor AND idempotency_key='task:'||NEW.task_id||':analysis:v1')
115
+ THEN RAISE EXCEPTION '维护完成不能跳过原计划或未知业务结果'; END IF;
116
+ END IF;
117
+ ELSIF NEW.outcome='succeeded' THEN
118
+ IF body->>'source_revision' IS DISTINCT FROM task.descriptor_json::jsonb->'source'->>'revision'
119
+ OR jsonb_typeof(body->'submission_ids') IS DISTINCT FROM 'array' OR jsonb_typeof(body->'analysis_ids') IS DISTINCT FROM 'array'
120
+ OR jsonb_array_length(body->'analysis_ids')<>0 OR coalesce(body->'maintenance_ids','[]'::jsonb)<>'[]'::jsonb THEN RAISE EXCEPTION '任务成功缺少明确来源版本或包含未支持分析结果'; END IF;
121
+ SELECT * INTO bound FROM agent.task_submissions WHERE actor=NEW.actor AND task_id=NEW.task_id;
122
+ IF FOUND THEN
123
+ IF jsonb_array_length(body->'submission_ids')<>1 OR body->'submission_ids'->>0 IS DISTINCT FROM bound.id
124
+ OR body ? 'no_change_reason' OR EXISTS(SELECT 1 FROM agent.reversions WHERE submission_id=bound.id) THEN
125
+ RAISE EXCEPTION '任务成功结果与真实绑定不一致或已经撤销'; END IF;
126
+ ELSE
127
+ IF jsonb_array_length(body->'submission_ids')<>0 OR length(btrim(coalesce(body->>'no_change_reason',''))) NOT BETWEEN 1 AND 2000
128
+ OR EXISTS(SELECT 1 FROM agent.task_plans WHERE actor=NEW.actor AND task_id=NEW.task_id)
129
+ OR EXISTS(SELECT 1 FROM agent.submissions WHERE actor=NEW.actor AND idempotency_key='task:'||NEW.task_id||':submit:v1') THEN
130
+ RAISE EXCEPTION '任务成功不能跳过原计划或未知业务结果'; END IF;
131
+ END IF;
132
+ ELSE
133
+ -- 固定分类是执行器报告,不作为来源变化已核实或业务撤销的证明。
134
+ IF body->>'error_code' IS NULL OR body->>'error_code' NOT IN ('EXECUTOR_UNAVAILABLE','SOURCE_CHANGED','BUSINESS_CONFLICT','EXECUTION_FAILED') THEN
135
+ RAISE EXCEPTION '任务失败分类无效'; END IF;
136
+ END IF;
137
+ RETURN NEW;
138
+ END $$;
139
+ REVOKE ALL ON FUNCTION public.vault_task_completion_guard() FROM PUBLIC;
@@ -0,0 +1,66 @@
1
+ -- 任务结构操作先冻结、后执行;结构与真实回执共用 owner 事务,不复制原始业务数据或创建备份。
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_structure_plans (
7
+ id text PRIMARY KEY,actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),task_id text NOT NULL,
8
+ operation_key text NOT NULL CHECK(operation_key ~ '^[a-z][a-z0-9_-]{0,79}$'),
9
+ lease_event_id text NOT NULL,lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat','resume')),
10
+ fingerprint text NOT NULL CHECK(fingerprint ~ '^[a-f0-9]{64}$'),source_revision text NOT NULL CHECK(source_revision ~ '^[a-f0-9]{64}$'),
11
+ payload_json text NOT NULL CHECK(public.vault_json_valid(payload_json)),created_at text NOT NULL,
12
+ UNIQUE(actor,id),UNIQUE(actor,task_id,operation_key),UNIQUE(actor,task_id,id),
13
+ FOREIGN KEY(actor,task_id) REFERENCES agent.tasks(actor,id),
14
+ FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind),
15
+ CHECK(id ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'),
16
+ CHECK((payload_json::jsonb->>'action' IN ('create','upgrade')) IS TRUE)
17
+ );
18
+ CREATE TABLE agent.task_structure_results (
19
+ id text PRIMARY KEY,actor text NOT NULL CHECK(actor ~ '^agent:local:[0-9]+$'),task_id text NOT NULL,
20
+ lease_event_id text NOT NULL,lease_kind text NOT NULL CHECK(lease_kind IN ('claim','heartbeat','resume')),
21
+ plan_revision text NOT NULL CHECK(plan_revision ~ '^[a-f0-9]{64}$'),
22
+ extension_id text NOT NULL,version bigint NOT NULL,checksum text NOT NULL CHECK(checksum ~ '^[a-f0-9]{64}$'),
23
+ result_json text NOT NULL CHECK(public.vault_json_valid(result_json)),created_at text NOT NULL,
24
+ FOREIGN KEY(actor,task_id,id) REFERENCES agent.task_structure_plans(actor,task_id,id),
25
+ FOREIGN KEY(actor,lease_event_id,lease_kind) REFERENCES agent.task_lease_events(actor,id,kind),
26
+ FOREIGN KEY(extension_id,version) REFERENCES agent.extension_migrations(extension_id,version)
27
+ );
28
+ CREATE FUNCTION public.vault_task_structure_guard() RETURNS trigger
29
+ LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog,pg_temp AS $$
30
+ DECLARE lease agent.task_lease_events%ROWTYPE; task agent.tasks%ROWTYPE; plan agent.task_structure_plans%ROWTYPE;
31
+ BEGIN
32
+ IF current_setting('vault.actor',true)='migration' THEN RETURN NEW; END IF;
33
+ PERFORM pg_advisory_xact_lock(hashtext('relation-vault:write'));
34
+ SELECT * INTO task FROM agent.tasks WHERE actor=NEW.actor AND id=NEW.task_id;
35
+ IF NOT FOUND OR task.descriptor_json::jsonb->>'kind'<>'extract_source' THEN RAISE EXCEPTION '结构操作需要原始来源任务'; END IF;
36
+ SELECT * INTO lease FROM agent.task_lease_events WHERE actor=NEW.actor AND task_id=NEW.task_id ORDER BY sequence DESC LIMIT 1;
37
+ IF NOT FOUND OR lease.id IS DISTINCT FROM NEW.lease_event_id OR lease.kind IS DISTINCT FROM NEW.lease_kind OR lease.lease_until::timestamptz<=clock_timestamp() THEN RAISE EXCEPTION '结构操作需要当前有效租约'; END IF;
38
+ IF TG_TABLE_NAME='task_structure_plans' THEN
39
+ IF NEW.source_revision IS DISTINCT FROM task.descriptor_json::jsonb->'source'->>'revision' THEN RAISE EXCEPTION '结构计划来源版本不符'; END IF;
40
+ ELSE
41
+ SELECT * INTO plan FROM agent.task_structure_plans WHERE actor=NEW.actor AND task_id=NEW.task_id AND id=NEW.id;
42
+ IF NOT FOUND OR plan.fingerprint IS DISTINCT FROM NEW.plan_revision OR plan.source_revision IS DISTINCT FROM task.descriptor_json::jsonb->'source'->>'revision'
43
+ OR NOT EXISTS(SELECT 1 FROM agent.extension_migrations m WHERE m.extension_id=NEW.extension_id AND m.version=NEW.version AND m.checksum=NEW.checksum)
44
+ OR NEW.result_json::jsonb->>'extension_id' IS DISTINCT FROM NEW.extension_id OR NEW.result_json::jsonb->>'version' IS DISTINCT FROM NEW.version::text
45
+ OR NEW.result_json::jsonb->>'checksum' IS DISTINCT FROM NEW.checksum THEN RAISE EXCEPTION '结构结果与计划或真实版本历史不符'; END IF;
46
+ END IF;
47
+ RETURN NEW;
48
+ END $$;
49
+ REVOKE ALL ON FUNCTION public.vault_task_structure_guard() FROM PUBLIC;
50
+ DO $$ DECLARE name text; g record; BEGIN
51
+ FOREACH name IN ARRAY ARRAY['task_structure_plans','task_structure_results'] LOOP
52
+ 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);
53
+ EXECUTE format('CREATE TRIGGER trg_%1$s_truncate BEFORE TRUNCATE ON agent.%1$I FOR EACH STATEMENT EXECUTE FUNCTION public.vault_guard(''append'')',name);
54
+ 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);
55
+ 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);
56
+ 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);
57
+ 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);
58
+ EXECUTE format('CREATE TRIGGER trg_%1$s_structure BEFORE INSERT ON agent.%1$I FOR EACH ROW EXECUTE FUNCTION public.vault_task_structure_guard()',name);
59
+ EXECUTE format('REVOKE ALL ON agent.%I FROM PUBLIC',name);
60
+ -- 结果只能由结构服务的 owner 事务追加;app 只可保存计划,资料读取在停用后继续保留。
61
+ 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='SELECT' OR (name='task_structure_plans' AND a.privilege_type='INSERT')) LOOP
62
+ EXECUTE format('GRANT USAGE ON SCHEMA agent TO %I',g.rolname);
63
+ EXECUTE format('GRANT %s ON agent.%I TO %I',g.privilege_type,name,g.rolname);
64
+ END LOOP;
65
+ END LOOP;
66
+ END $$;