@consiliency/agent-board-schema 1.64.0 → 1.74.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/README.md +19 -0
- package/fixtures/already-applied.json +44 -4
- package/migrations/20261015000100_agent_board_session_provenance.sql +367 -0
- package/migrations/20261015000200_agent_board_request_preserve_removed.sql +87 -0
- package/migrations/20261015000300_agent_board_rls_correlation.sql +132 -0
- package/migrations/20261015000400_agent_board_resolution_authority.sql +146 -0
- package/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
- package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
- package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
- package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
- package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
- package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
- package/package.json +1 -1
- package/schema-manifest.json +93 -3
- package/src/deployment-drift-main.test.mjs +156 -0
- package/src/deployment-drift-script.test.mjs +14 -1
- package/src/policy-comment.test.mjs +17 -0
- package/src/tooling.test.mjs +12 -2
- package/tests/agent_board_661_reply_delivery.test.sql +185 -0
- package/tests/agent_board_667_boundary.test.sql +162 -0
- package/tests/agent_board_667_conversion_branches.test.sql +91 -0
- package/tests/agent_board_667_conversion_profile.test.sql +112 -0
- package/tests/agent_board_667_history_class.test.sql +785 -0
- package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
- package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
- package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
- package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
- package/tests/agent_board_667_retention.test.sql +201 -0
- package/tests/agent_board_667_routing.test.sql +294 -0
- package/tests/agent_board_667_sender_effects.test.sql +100 -0
- package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
- package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
- package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
- package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
- package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
- package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
- package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
- package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
- package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
- package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
- package/tests/agent_board_667_writers.test.sql +281 -0
- package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
- package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
- package/tests/agent_board_addressing.test.sql +8 -5
- package/tests/agent_board_claim_subject_gate.test.sql +7 -3
- package/tests/agent_board_claimpolicy_race.test.sql +39 -11
- package/tests/agent_board_consent_gating.test.sql +4 -4
- package/tests/agent_board_delegate_role_class.test.sql +33 -15
- package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
- package/tests/agent_board_fk_lock_edges.test.sql +3 -0
- package/tests/agent_board_gatenull_membstate.test.sql +7 -4
- package/tests/agent_board_hosted_delegates.test.sql +5 -2
- package/tests/agent_board_inbox_rpcs.test.sql +5 -3
- package/tests/agent_board_obsgate.test.sql +8 -4
- package/tests/agent_board_obsgate4.test.sql +2 -2
- package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
- package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
- package/tests/agent_board_policy_correlation.test.sql +189 -0
- package/tests/agent_board_request_removed.test.sql +96 -0
- package/tests/agent_board_resolution_authority.test.sql +190 -0
- package/tests/agent_board_session_identity_behavior_classes.test.sql +2 -2
- package/tests/agent_board_session_identity_limits.test.sql +1 -1
- package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
- package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
- package/tests/agent_board_session_provenance.test.sql +106 -0
- package/tests/agent_board_sidserver_authority.test.sql +69 -0
- package/tests/agent_board_sidserver_catalog.test.sql +47 -0
- package/tests/agent_board_signed_write_gateway.test.sql +4 -2
- package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
- package/tests/agent_board_v2_invariants.test.sql +5 -2
- package/tests/agent_board_write_enforcement_class.test.sql +3 -3
- package/tests/catalog/function_execute_privileges.json +43 -7
- package/tests/catalog/p_metadata_parameters.json +16 -0
- package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
- package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
- package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
- package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
- package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
- package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
- package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
- package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
- package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
- package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
- package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
- package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
- package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
- package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
- package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
- package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
- package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
- package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
- package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
- package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
- package/tests/isolation/expected/association_board_vs_board.out +71 -0
- package/tests/isolation/expected/association_post_wait_authority.out +77 -0
- package/tests/isolation/expected/association_strong_history_writers.out +203 -0
- package/tests/isolation/expected/association_strong_isolation.out +185 -0
- package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
- package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
- package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
- package/tests/isolation/expected/association_vs_private_message.out +67 -0
- package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
- package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
- package/tests/isolation/expected/consent_approval_lock.out +1 -1
- package/tests/isolation/expected/message_replay_scope.out +209 -0
- package/tests/isolation/expected/tasking_replay_scope.out +125 -0
- package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
- package/tests/isolation/specs/association_board_vs_board.spec +37 -0
- package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
- package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
- package/tests/isolation/specs/association_strong_isolation.spec +50 -0
- package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
- package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
- package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
- package/tests/isolation/specs/association_vs_private_message.spec +33 -0
- package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
- package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
- package/tests/isolation/specs/consent_approval_lock.spec +13 -7
- package/tests/isolation/specs/message_replay_scope.spec +77 -0
- package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
- package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
- package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
- package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
- package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
- package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
- package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
- package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
- package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
- package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
- package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
- package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
- package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
- package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
- package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
- package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
- package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
- package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
- package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
- package/tests/mutations/717-f1/run.sh +17 -0
- package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
- package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
- package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
- package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
- package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
- package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
- package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
- package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
- package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
- package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
- package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
- package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
- package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
- package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
- package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
- package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
- package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
- package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
- package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
- package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
- package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
- package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
- package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
- package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
- package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
- package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
- package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
- package/tests/mutations/717-f2b/run.sh +82 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
-- #717 F2: same-key tasking replay must not widen a task's delivery scope.
|
|
2
|
+
--
|
|
3
|
+
-- create_tasking_delivery keys idempotency on (thread, resolved creator, key) and, on a replay,
|
|
4
|
+
-- reused the task and fanned it out to whatever audience the RETRY named. A recipient-only task
|
|
5
|
+
-- replayed with a board target therefore became readable by board members, and through the hosted
|
|
6
|
+
-- delegate policies (which key on a board-scoped delivery of the task) by that board's delegates,
|
|
7
|
+
-- together with the task history other agents authored on it.
|
|
8
|
+
--
|
|
9
|
+
-- The identity, the advisory lock and the lookup are unchanged; board_id is deliberately NOT added
|
|
10
|
+
-- to the key, which would silently turn a retry into a second task. A replay may name the same
|
|
11
|
+
-- audience or a subset of it, and a same-board replay still fans out to the board's CURRENT members,
|
|
12
|
+
-- as before. Anything the persisted deliveries do not already prove is refused with 42501 before any
|
|
13
|
+
-- replay side effect, and the raise rolls back the whole call, including any first association,
|
|
14
|
+
-- telemetry, and the signed wrappers' evidence (they do not catch). Compatibility limit, recorded:
|
|
15
|
+
-- a task whose deliveries were never written or were removed cannot be replayed with any target.
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION agent_board.create_tasking_delivery(p_thread_id uuid, p_created_by_agent_id uuid, p_title text, p_details text DEFAULT NULL::text, p_recipient_agent_ids uuid[] DEFAULT NULL::uuid[], p_board_id uuid DEFAULT NULL::uuid, p_priority text DEFAULT 'medium'::text, p_orchestration_mode text DEFAULT 'distributed'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
18
|
+
RETURNS TABLE(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
19
|
+
LANGUAGE plpgsql
|
|
20
|
+
SECURITY DEFINER
|
|
21
|
+
SET search_path TO 'agent_board', 'public'
|
|
22
|
+
AS $function$
|
|
23
|
+
declare
|
|
24
|
+
v_task agent_board.tasks%rowtype;
|
|
25
|
+
v_created_event_id uuid;
|
|
26
|
+
v_created_task record;
|
|
27
|
+
v_recipient_ids uuid[];
|
|
28
|
+
v_recipient_id uuid;
|
|
29
|
+
v_membership record;
|
|
30
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
31
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
32
|
+
v_delivered_count integer := 0;
|
|
33
|
+
v_inbox_count integer := 0;
|
|
34
|
+
v_callback_count integer := 0;
|
|
35
|
+
v_metadata jsonb;
|
|
36
|
+
v_preview text;
|
|
37
|
+
v_fetch_descriptor jsonb;
|
|
38
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
39
|
+
v_actor_agent_id uuid;
|
|
40
|
+
v_is_creator boolean;
|
|
41
|
+
begin
|
|
42
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
|
|
43
|
+
|
|
44
|
+
if nullif(trim(p_title), '') is null then
|
|
45
|
+
raise exception 'task title is required' using errcode = '22023';
|
|
46
|
+
end if;
|
|
47
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
48
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
49
|
+
end if;
|
|
50
|
+
if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
|
|
51
|
+
raise exception 'invalid orchestration mode' using errcode = '22023';
|
|
52
|
+
end if;
|
|
53
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
54
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
55
|
+
end if;
|
|
56
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
57
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
58
|
+
end if;
|
|
59
|
+
if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
|
|
60
|
+
raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
|
|
61
|
+
end if;
|
|
62
|
+
if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
|
|
63
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
64
|
+
end if;
|
|
65
|
+
if p_board_id is not null then
|
|
66
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
67
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
68
|
+
end if;
|
|
69
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
70
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
71
|
+
end if;
|
|
72
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
73
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
74
|
+
end if;
|
|
75
|
+
end if;
|
|
76
|
+
|
|
77
|
+
select array_agg(distinct recipient_id)
|
|
78
|
+
into v_recipient_ids
|
|
79
|
+
from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
|
|
80
|
+
|
|
81
|
+
if v_recipient_ids is not null and (
|
|
82
|
+
select count(*)
|
|
83
|
+
from agent_board.agents
|
|
84
|
+
where id = any(v_recipient_ids)
|
|
85
|
+
) <> cardinality(v_recipient_ids) then
|
|
86
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
87
|
+
end if;
|
|
88
|
+
|
|
89
|
+
if agent_board._is_service_role() then
|
|
90
|
+
perform agent_board._record_send_authz_telemetry(
|
|
91
|
+
'service_role_bypass',
|
|
92
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
93
|
+
v_actor_agent_id,
|
|
94
|
+
p_board_id,
|
|
95
|
+
'create_tasking_delivery',
|
|
96
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
97
|
+
null,
|
|
98
|
+
jsonb_build_object('redacted', true)
|
|
99
|
+
);
|
|
100
|
+
end if;
|
|
101
|
+
|
|
102
|
+
perform agent_board._lock_idempotency_key(
|
|
103
|
+
'create_tasking_delivery:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
104
|
+
v_idempotency_key
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if p_board_id is not null then
|
|
108
|
+
perform agent_board._associate_empty_thread(p_thread_id, p_board_id);
|
|
109
|
+
-- Both checks are new statements after the thread-lock wait.
|
|
110
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
111
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
112
|
+
end if;
|
|
113
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
114
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
115
|
+
end if;
|
|
116
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
117
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
118
|
+
end if;
|
|
119
|
+
end if;
|
|
120
|
+
|
|
121
|
+
if v_idempotency_key is not null then
|
|
122
|
+
select *
|
|
123
|
+
into v_task
|
|
124
|
+
from agent_board.tasks
|
|
125
|
+
where thread_id = p_thread_id
|
|
126
|
+
and created_by_agent_id = v_actor_agent_id
|
|
127
|
+
and metadata ->> 'rpcName' = 'create_tasking_delivery'
|
|
128
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
129
|
+
order by created_at desc, id desc
|
|
130
|
+
limit 1;
|
|
131
|
+
end if;
|
|
132
|
+
|
|
133
|
+
if v_task.id is null then
|
|
134
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
135
|
+
|| jsonb_build_object(
|
|
136
|
+
'rpcName', 'create_tasking_delivery',
|
|
137
|
+
'priority', p_priority
|
|
138
|
+
);
|
|
139
|
+
if p_board_id is not null then
|
|
140
|
+
v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
|
|
141
|
+
end if;
|
|
142
|
+
if v_idempotency_key is not null then
|
|
143
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
144
|
+
end if;
|
|
145
|
+
|
|
146
|
+
select *
|
|
147
|
+
into v_created_task
|
|
148
|
+
from agent_board.create_task(
|
|
149
|
+
p_thread_id,
|
|
150
|
+
v_actor_agent_id,
|
|
151
|
+
trim(p_title),
|
|
152
|
+
p_details,
|
|
153
|
+
null,
|
|
154
|
+
null,
|
|
155
|
+
p_priority,
|
|
156
|
+
p_orchestration_mode,
|
|
157
|
+
v_metadata
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
v_created_event_id := v_created_task.created_event_id;
|
|
161
|
+
|
|
162
|
+
select *
|
|
163
|
+
into v_task
|
|
164
|
+
from agent_board.tasks
|
|
165
|
+
where id = v_created_task.task_id;
|
|
166
|
+
else
|
|
167
|
+
-- #717 F2: a replay reuses the task, so it may not widen who can read it. The audience a
|
|
168
|
+
-- replay may name is bounded by the task's PERSISTED tasking deliveries: a board only if a
|
|
169
|
+
-- board-scoped delivery of this task already names it, and each explicit recipient only if
|
|
170
|
+
-- it already holds a private (null-board) delivery of this task. Thread stamps and task or
|
|
171
|
+
-- caller metadata are never evidence: metadata is writable through other task creators.
|
|
172
|
+
-- Missing evidence refuses; a new audience needs a new idempotency key (a new task).
|
|
173
|
+
if (
|
|
174
|
+
p_board_id is not null
|
|
175
|
+
and not exists (
|
|
176
|
+
select 1
|
|
177
|
+
from agent_board.delivery_recipients dr
|
|
178
|
+
where dr.source_task_id = v_task.id
|
|
179
|
+
and dr.delivery_mode = 'tasking'
|
|
180
|
+
and dr.board_id = p_board_id
|
|
181
|
+
)
|
|
182
|
+
) or exists (
|
|
183
|
+
select 1
|
|
184
|
+
from unnest(coalesce(v_recipient_ids, '{}'::uuid[])) as requested(agent_id)
|
|
185
|
+
where not exists (
|
|
186
|
+
select 1
|
|
187
|
+
from agent_board.delivery_recipients dr
|
|
188
|
+
where dr.source_task_id = v_task.id
|
|
189
|
+
and dr.delivery_mode = 'tasking'
|
|
190
|
+
and dr.recipient_agent_id = requested.agent_id
|
|
191
|
+
and dr.board_id is null
|
|
192
|
+
)
|
|
193
|
+
) then
|
|
194
|
+
raise exception 'tasking replay cannot widen delivery scope' using errcode = '42501';
|
|
195
|
+
end if;
|
|
196
|
+
|
|
197
|
+
select te.id
|
|
198
|
+
into v_created_event_id
|
|
199
|
+
from agent_board.task_events te
|
|
200
|
+
where te.task_id = v_task.id
|
|
201
|
+
and te.event_sequence = 1;
|
|
202
|
+
end if;
|
|
203
|
+
|
|
204
|
+
v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
|
|
205
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
206
|
+
'task',
|
|
207
|
+
v_task.id,
|
|
208
|
+
'agent_board.v_task_tree',
|
|
209
|
+
'tasking delivery',
|
|
210
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if v_recipient_ids is not null then
|
|
214
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
215
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
216
|
+
'tasking',
|
|
217
|
+
v_recipient_id,
|
|
218
|
+
'task',
|
|
219
|
+
null,
|
|
220
|
+
v_task.id,
|
|
221
|
+
null,
|
|
222
|
+
null,
|
|
223
|
+
null,
|
|
224
|
+
jsonb_build_object('priority', p_priority)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
228
|
+
v_recipient.id,
|
|
229
|
+
v_recipient_id,
|
|
230
|
+
null,
|
|
231
|
+
v_recipient.delivery_mode,
|
|
232
|
+
'task',
|
|
233
|
+
null,
|
|
234
|
+
v_task.id,
|
|
235
|
+
null,
|
|
236
|
+
trim(p_title),
|
|
237
|
+
v_preview,
|
|
238
|
+
p_priority,
|
|
239
|
+
true,
|
|
240
|
+
false,
|
|
241
|
+
v_fetch_descriptor,
|
|
242
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
v_delivered_count := v_delivered_count + 1;
|
|
246
|
+
v_inbox_count := v_inbox_count + 1;
|
|
247
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
248
|
+
end loop;
|
|
249
|
+
end if;
|
|
250
|
+
|
|
251
|
+
if p_board_id is not null then
|
|
252
|
+
for v_membership in
|
|
253
|
+
select bm.id, bm.agent_id
|
|
254
|
+
from agent_board.board_memberships bm
|
|
255
|
+
where bm.board_id = p_board_id
|
|
256
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
257
|
+
order by bm.created_at
|
|
258
|
+
loop
|
|
259
|
+
-- #608: the board fan-out does not deliver a task to the agent that created it. The
|
|
260
|
+
-- creator's row is KEPT, feed-only, exactly as a board post's author row is. Before #667,
|
|
261
|
+
-- _upsert_delivery_recipient was the only board-stamp path; skipping that row
|
|
262
|
+
-- left a creator-only board's thread unlinked and post_message on the access-only branch of
|
|
263
|
+
-- the observer gate (codex and grok seats, PR #615 round 1). It also keeps the board
|
|
264
|
+
-- directory's activity and delegate visibility as they were. An explicit recipient list
|
|
265
|
+
-- naming the creator still delivers to it: that is the caller addressing itself, and the
|
|
266
|
+
-- board loop must not then demote the row the explicit loop wrote. It shares that row (the
|
|
267
|
+
-- delivery upsert keeps the explicit row's null board_id), so demoting it wrote both
|
|
268
|
+
-- visibility flags false and violated inbox_items_visibility_shape, aborting the whole task
|
|
269
|
+
-- (every seat on PR #615 round 2).
|
|
270
|
+
v_is_creator := v_membership.agent_id is not distinct from v_actor_agent_id
|
|
271
|
+
and not (v_membership.agent_id = any(coalesce(v_recipient_ids, '{}'::uuid[])));
|
|
272
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
273
|
+
'tasking',
|
|
274
|
+
v_membership.agent_id,
|
|
275
|
+
'task',
|
|
276
|
+
null,
|
|
277
|
+
v_task.id,
|
|
278
|
+
null,
|
|
279
|
+
p_board_id,
|
|
280
|
+
v_membership.id,
|
|
281
|
+
jsonb_build_object('priority', p_priority)
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
285
|
+
v_recipient.id,
|
|
286
|
+
v_membership.agent_id,
|
|
287
|
+
v_recipient.board_id,
|
|
288
|
+
v_recipient.delivery_mode,
|
|
289
|
+
'task',
|
|
290
|
+
null,
|
|
291
|
+
v_task.id,
|
|
292
|
+
null,
|
|
293
|
+
trim(p_title),
|
|
294
|
+
v_preview,
|
|
295
|
+
p_priority,
|
|
296
|
+
not v_is_creator,
|
|
297
|
+
v_recipient.board_id is not null,
|
|
298
|
+
v_fetch_descriptor,
|
|
299
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
-- The counts are of RECIPIENTS; the creator's feed-only row is not one.
|
|
303
|
+
if not v_is_creator then
|
|
304
|
+
v_delivered_count := v_delivered_count + 1;
|
|
305
|
+
v_inbox_count := v_inbox_count + 1;
|
|
306
|
+
end if;
|
|
307
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
308
|
+
end loop;
|
|
309
|
+
end if;
|
|
310
|
+
|
|
311
|
+
return query
|
|
312
|
+
select v_task.id, v_created_event_id, v_delivered_count, v_inbox_count, v_callback_count, v_task.created_at, v_task.updated_at;
|
|
313
|
+
end;
|
|
314
|
+
$function$
|
|
315
|
+
;
|
|
316
|
+
|
|
317
|
+
revoke all on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) from public, anon, authenticated, service_role, agent_board_delegate;
|
|
318
|
+
grant execute on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) to "service_role";
|
|
319
|
+
grant execute on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) to "authenticated";
|
|
320
|
+
comment on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) is 'Creates or reuses a tasking send after validating the session actor and any board target. First board association precedes its own task and event writes; explicit recipient delivery rows preserve their private scope. A replay of an existing (thread, creator, key) cannot widen the task''s audience (#717 F2): a board must already hold a board-scoped tasking delivery of the task and each explicit recipient a private one, else 42501 ''tasking replay cannot widen delivery scope'' before any replay effect; a new audience needs a new key.';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- DELIVERYWRITEREXEC (#717 F1): the internal message/delivery/inbox writers are not API routes.
|
|
2
|
+
--
|
|
3
|
+
-- Five SECURITY DEFINER helpers write the delivery sinks (delivery_recipients, inbox_items,
|
|
4
|
+
-- callback_outbox_events) or the thread message sequence, and each takes its authority from its
|
|
5
|
+
-- ARGUMENTS: a recipient, a board, a membership, a message id, a subject. They were EXECUTE-able
|
|
6
|
+
-- by service_role through the schema-wide `grant execute on all functions in schema agent_board
|
|
7
|
+
-- to service_role` of the 2026-05 migrations, never by a deliberate grant.
|
|
8
|
+
--
|
|
9
|
+
-- Called directly there is no sending RPC in front of them, so nothing checks that the caller is
|
|
10
|
+
-- the author, that the recipient was addressed, or that the board was targeted. #717 measured it
|
|
11
|
+
-- (review S4/S4b): `_upsert_delivery_recipient('board', member, 'message', <a DM on a board
|
|
12
|
+
-- thread>, …)` returned OK and a board member then read the DM; `'direct'` mode re-addressed the
|
|
13
|
+
-- same DM to an arbitrary agent. Both keep the ORIGINAL author on the message row, so the
|
|
14
|
+
-- republished DM reads as authored by its sender. `_upsert_inbox_item`, `_queue_inbox_callbacks`,
|
|
15
|
+
-- `_queue_board_post_callbacks` (forged wake payloads, caller-chosen subject and preview against
|
|
16
|
+
-- a real message id) and `_next_thread_message_sequence` (sequence consumption on any thread)
|
|
17
|
+
-- are the same class: internal, argument-authorised, reachable only because of the blanket grant.
|
|
18
|
+
--
|
|
19
|
+
-- The class is catalog-derived in tests/agent_board_delivery_writer_execute.test.sql (DWX):
|
|
20
|
+
-- every routine that writes a message/delivery/inbox sink, directly or transitively, either has
|
|
21
|
+
-- no API-role EXECUTE or is a named, positively-asserted public route. No caller outside the
|
|
22
|
+
-- schema invokes any of these five (repository and Portal searched; edge functions call fixed
|
|
23
|
+
-- public RPC names). The public send routes keep working: they are SECURITY DEFINER and reach the
|
|
24
|
+
-- helpers as the function owner, not as the API caller.
|
|
25
|
+
--
|
|
26
|
+
-- No function body changes, so the source tree's existing definitions stay the sole ones.
|
|
27
|
+
|
|
28
|
+
revoke all on function agent_board._upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb)
|
|
29
|
+
from public, anon, authenticated, service_role, agent_board_delegate;
|
|
30
|
+
comment on function agent_board._upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb) is
|
|
31
|
+
'Internal delivery sink, not an API route: no API role may EXECUTE it (#717 F1). Enforces thread/board identity and the locked empty-history boundary for message, task and delegation delivery sources. Supplying a source ID does not exempt its private history. The sending RPC validates caller authority before its own writes.';
|
|
32
|
+
|
|
33
|
+
revoke all on function agent_board._upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb)
|
|
34
|
+
from public, anon, authenticated, service_role, agent_board_delegate;
|
|
35
|
+
comment on function agent_board._upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb) is
|
|
36
|
+
'Internal inbox sink, not an API route: no API role may EXECUTE it (#717 F1). The sole inbox_items inserter; its recipient, board, visibility and preview are trusted from the calling send/tasking RPC, which validated caller authority first.';
|
|
37
|
+
|
|
38
|
+
revoke all on function agent_board._queue_inbox_callbacks(uuid,text)
|
|
39
|
+
from public, anon, authenticated, service_role, agent_board_delegate;
|
|
40
|
+
comment on function agent_board._queue_inbox_callbacks(uuid,text) is
|
|
41
|
+
'Internal callback sink, not an API route: no API role may EXECUTE it (#717 F1). Queues inbox callback outbox events for an inbox item the calling delivery RPC has just written.';
|
|
42
|
+
|
|
43
|
+
revoke all on function agent_board._queue_board_post_callbacks(uuid,uuid,text,text,text,jsonb)
|
|
44
|
+
from public, anon, authenticated, service_role, agent_board_delegate;
|
|
45
|
+
comment on function agent_board._queue_board_post_callbacks(uuid,uuid,text,text,text,jsonb) is
|
|
46
|
+
'Internal callback sink, not an API route: no API role may EXECUTE it (#717 F1). Queues board-post callback outbox events with the subject, preview and fetch descriptor supplied by the board delivery that called it.';
|
|
47
|
+
|
|
48
|
+
revoke all on function agent_board._next_thread_message_sequence(uuid)
|
|
49
|
+
from public, anon, authenticated, service_role, agent_board_delegate;
|
|
50
|
+
comment on function agent_board._next_thread_message_sequence(uuid) is
|
|
51
|
+
'Internal sequence writer, not an API route: no API role may EXECUTE it (#717 F1). Advances threads.last_message_sequence under the thread row lock for the message insertion that called it.';
|