@consiliency/agent-board-schema 1.68.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/fixtures/already-applied.json +28 -4
- 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 +57 -3
- package/src/tooling.test.mjs +8 -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_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_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_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_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 +41 -5
- 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/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/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,661 @@
|
|
|
1
|
+
-- #717 F2: a same-key create_tasking_delivery replay cannot widen the task's delivery scope.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT (main 21b7b158, review S9). Idempotency is keyed on (thread, resolved creator, key).
|
|
4
|
+
-- A replay reused the task and fanned it out to whatever the RETRY named, so a private task on a
|
|
5
|
+
-- board-stamped thread, replayed with that board, became readable by every board member and --
|
|
6
|
+
-- through the hosted delegate policies, which admit a task's events, claims and escalations once
|
|
7
|
+
-- ANY board-scoped delivery of the task names a board the delegate is on -- by that board's
|
|
8
|
+
-- delegates, together with history OTHER agents authored on the task.
|
|
9
|
+
--
|
|
10
|
+
-- THE CONTRACT pinned here: canonical identity and lock unchanged; a replay may name the same
|
|
11
|
+
-- audience or a subset; a board only if a board-scoped tasking delivery of the task already names
|
|
12
|
+
-- it; an explicit recipient only if it already holds a PRIVATE (null-board) tasking delivery of the
|
|
13
|
+
-- task. Anything else is 42501 'tasking replay cannot widen delivery scope', with no side effect.
|
|
14
|
+
-- Metadata and thread stamps are not evidence. A new audience needs a new key.
|
|
15
|
+
--
|
|
16
|
+
-- METHOD (AGENTS.md rules 2 and 3).
|
|
17
|
+
-- * Every actor runs under its OWN role AND claims: `set local role authenticated|service_role|
|
|
18
|
+
-- agent_board_delegate` plus a JSON claim set, and the flattened request.jwt.claim.* GUCs are
|
|
19
|
+
-- cleared on every switch so they cannot shadow the JSON claims and test another identity.
|
|
20
|
+
-- * Hosted delegates are minted by the real exchange_delegate_credential and placed by their
|
|
21
|
+
-- owner through add_delegate_to_board, and call with their real credential id.
|
|
22
|
+
-- * The private task's history is written by its ASSIGNEE C through the public RPCs, after a
|
|
23
|
+
-- legal assignment, so the rows that must stay hidden are another agent's, not the creator's.
|
|
24
|
+
-- * Every call is run by a helper with its own exception block that returns 'ok' or
|
|
25
|
+
-- '<sqlstate>:<message>'; refusals compare the exact message, and the same helper reports the
|
|
26
|
+
-- side-effect fingerprint delta it measured around the call. State reads are separate queries.
|
|
27
|
+
create extension if not exists pgtap with schema extensions;
|
|
28
|
+
set search_path = extensions, public, agent_board;
|
|
29
|
+
|
|
30
|
+
begin;
|
|
31
|
+
|
|
32
|
+
select plan(56);
|
|
33
|
+
|
|
34
|
+
do $$
|
|
35
|
+
begin
|
|
36
|
+
if not (select rolsuper from pg_roles where rolname = current_user) then
|
|
37
|
+
execute format('grant agent_board_delegate to %I with set true', current_user);
|
|
38
|
+
end if;
|
|
39
|
+
end $$;
|
|
40
|
+
|
|
41
|
+
create temp table _h(k text primary key, v text);
|
|
42
|
+
create function pg_temp.v(p_k text) returns text language sql as $$ select v from _h where k = p_k $$;
|
|
43
|
+
create function pg_temp.u(p_k text) returns uuid language sql as $$ select v::uuid from _h where k = p_k $$;
|
|
44
|
+
create function pg_temp.put(p_k text, p_v text) returns void language sql as $$
|
|
45
|
+
insert into _h values (p_k, p_v) on conflict (k) do update set v = excluded.v $$;
|
|
46
|
+
|
|
47
|
+
-- Identity switch: one JSON claim set, every flattened claim GUC cleared.
|
|
48
|
+
create function pg_temp.ident(p_role text, p_sub uuid, p_cred uuid) returns void language plpgsql as $$
|
|
49
|
+
begin
|
|
50
|
+
perform set_config('request.jwt.claim.sub', '', true);
|
|
51
|
+
perform set_config('request.jwt.claim.role', '', true);
|
|
52
|
+
perform set_config('request.jwt.claim.agent_credential_id', '', true);
|
|
53
|
+
perform set_config('request.jwt.claim.identity_lease_epoch', '', true);
|
|
54
|
+
perform set_config('request.jwt.claims',
|
|
55
|
+
case when p_role is null then ''
|
|
56
|
+
else jsonb_strip_nulls(jsonb_build_object('role', p_role, 'sub', p_sub, 'agent_credential_id', p_cred))::text end,
|
|
57
|
+
true);
|
|
58
|
+
end $$;
|
|
59
|
+
|
|
60
|
+
-- Run p_sql AS actor p_who (its role and claims). Returns 'ok' or '<sqlstate>:<message>'.
|
|
61
|
+
create function pg_temp.as_(p_who text, p_sql text) returns text language plpgsql as $$
|
|
62
|
+
declare
|
|
63
|
+
v text;
|
|
64
|
+
v_role text := pg_temp.v(p_who || '.role');
|
|
65
|
+
v_sub uuid := pg_temp.u(p_who || '.auth');
|
|
66
|
+
v_cred uuid := pg_temp.u(p_who || '.cred');
|
|
67
|
+
begin
|
|
68
|
+
perform pg_temp.ident(v_role, v_sub, v_cred);
|
|
69
|
+
execute format('set local role %I', v_role);
|
|
70
|
+
begin
|
|
71
|
+
execute p_sql;
|
|
72
|
+
v := 'ok';
|
|
73
|
+
exception when others then
|
|
74
|
+
v := sqlstate || ':' || sqlerrm;
|
|
75
|
+
end;
|
|
76
|
+
reset role;
|
|
77
|
+
perform pg_temp.ident(null, null, null);
|
|
78
|
+
return v;
|
|
79
|
+
end $$;
|
|
80
|
+
|
|
81
|
+
-- Run p_sql AS actor p_who and return its single scalar.
|
|
82
|
+
create function pg_temp.val(p_who text, p_sql text) returns text language plpgsql as $$
|
|
83
|
+
declare
|
|
84
|
+
v text;
|
|
85
|
+
v_role text := pg_temp.v(p_who || '.role');
|
|
86
|
+
v_sub uuid := pg_temp.u(p_who || '.auth');
|
|
87
|
+
v_cred uuid := pg_temp.u(p_who || '.cred');
|
|
88
|
+
begin
|
|
89
|
+
perform pg_temp.ident(v_role, v_sub, v_cred);
|
|
90
|
+
execute format('set local role %I', v_role);
|
|
91
|
+
begin
|
|
92
|
+
execute p_sql into v;
|
|
93
|
+
exception when others then
|
|
94
|
+
v := 'ERR ' || sqlstate || ':' || sqlerrm;
|
|
95
|
+
end;
|
|
96
|
+
reset role;
|
|
97
|
+
perform pg_temp.ident(null, null, null);
|
|
98
|
+
return coalesce(v, '(null)');
|
|
99
|
+
end $$;
|
|
100
|
+
|
|
101
|
+
-- Everything a tasking send can write, counted globally (this transaction is the only writer).
|
|
102
|
+
-- fp_rows() counts rows; fp() adds a digest of every inbox item's presentation, which a replay
|
|
103
|
+
-- legitimately refreshes on an accepted retry but must not touch on a refused one.
|
|
104
|
+
create function pg_temp.fp_rows() returns text language plpgsql as $$
|
|
105
|
+
declare v text;
|
|
106
|
+
begin
|
|
107
|
+
select concat_ws(' ',
|
|
108
|
+
'tasks=' || (select count(*) from agent_board.tasks),
|
|
109
|
+
'events=' || (select count(*) from agent_board.task_events),
|
|
110
|
+
'deliveries=' || (select count(*) from agent_board.delivery_recipients),
|
|
111
|
+
'inbox=' || (select count(*) from agent_board.inbox_items),
|
|
112
|
+
'callbacks=' || (select count(*) from agent_board.callback_outbox_events),
|
|
113
|
+
'evidence=' || (select count(*) from agent_board.signature_evidence),
|
|
114
|
+
'telemetry=' || (select count(*) from agent_board.send_authz_telemetry),
|
|
115
|
+
'stamped=' || (select count(*) from agent_board.threads where board_id is not null))
|
|
116
|
+
into v;
|
|
117
|
+
return v;
|
|
118
|
+
end $$;
|
|
119
|
+
create function pg_temp.fp() returns text language sql as $$
|
|
120
|
+
select pg_temp.fp_rows() || ' inboxtxt=' || (select md5(coalesce(string_agg(id::text || subject || coalesce(preview, ''), ',' order by id), ''))
|
|
121
|
+
from agent_board.inbox_items)
|
|
122
|
+
$$;
|
|
123
|
+
|
|
124
|
+
-- A call that must be REFUSED: '<result> | side-effects=none' when nothing changed.
|
|
125
|
+
create function pg_temp.refuse(p_who text, p_sql text) returns text language plpgsql as $$
|
|
126
|
+
declare v_before text := pg_temp.fp(); v_res text; v_after text;
|
|
127
|
+
begin
|
|
128
|
+
v_res := pg_temp.as_(p_who, p_sql);
|
|
129
|
+
v_after := pg_temp.fp();
|
|
130
|
+
return v_res || ' | side-effects=' || case when v_after = v_before then 'none' else v_before || ' -> ' || v_after end;
|
|
131
|
+
end $$;
|
|
132
|
+
|
|
133
|
+
-- Rows of task p_task as actor p_who sees them: tasks/events/claims/escalations/evidence.
|
|
134
|
+
create function pg_temp.sees(p_who text, p_task text) returns text language plpgsql as $$
|
|
135
|
+
begin
|
|
136
|
+
return pg_temp.val(p_who, format($q$
|
|
137
|
+
select (select count(*) from agent_board.tasks where id = %1$L)
|
|
138
|
+
|| '/' || (select count(*) from agent_board.task_events where task_id = %1$L)
|
|
139
|
+
|| '/' || (select count(*) from agent_board.claims where task_id = %1$L)
|
|
140
|
+
|| '/' || (select count(*) from agent_board.escalations where task_id = %1$L)
|
|
141
|
+
|| '/' || (select count(*) from agent_board.signature_evidence se
|
|
142
|
+
join agent_board.task_events te on te.id = se.source_task_event_id where te.task_id = %1$L)
|
|
143
|
+
$q$, pg_temp.v(p_task)));
|
|
144
|
+
end $$;
|
|
145
|
+
|
|
146
|
+
-- A task's delivery scope: sorted 'recipient-handle:board-slug|private'.
|
|
147
|
+
create function pg_temp.scope(p_task text) returns text language sql as $$
|
|
148
|
+
select coalesce(string_agg(a.handle || ':' || coalesce(b.slug, 'private'), ',' order by a.handle, b.slug), '(none)')
|
|
149
|
+
from agent_board.delivery_recipients dr
|
|
150
|
+
join agent_board.agents a on a.id = dr.recipient_agent_id
|
|
151
|
+
left join agent_board.boards b on b.id = dr.board_id
|
|
152
|
+
where dr.source_task_id = pg_temp.u(p_task)
|
|
153
|
+
$$;
|
|
154
|
+
|
|
155
|
+
-- The private task's rows, byte for byte: ids, authors and content of the task and its history.
|
|
156
|
+
create function pg_temp.history(p_task text) returns text language sql as $$
|
|
157
|
+
select md5(concat_ws('#',
|
|
158
|
+
(select t::text from agent_board.tasks t where t.id = pg_temp.u(p_task)),
|
|
159
|
+
(select string_agg(te::text, ',' order by te.id) from agent_board.task_events te where te.task_id = pg_temp.u(p_task)),
|
|
160
|
+
(select string_agg(c::text, ',' order by c.id) from agent_board.claims c where c.task_id = pg_temp.u(p_task)),
|
|
161
|
+
(select string_agg(e::text, ',' order by e.id) from agent_board.escalations e where e.task_id = pg_temp.u(p_task))))
|
|
162
|
+
$$;
|
|
163
|
+
|
|
164
|
+
create function pg_temp.ctd(p_who text, p_thread text, p_title text, p_recipients text, p_board text, p_key text,
|
|
165
|
+
p_metadata jsonb default '{}'::jsonb) returns text language sql as $$
|
|
166
|
+
select format('select * from agent_board.create_tasking_delivery(%L::uuid, %L::uuid, %L, null, %s, %s, ''medium'', ''distributed'', %L, %L::jsonb)',
|
|
167
|
+
pg_temp.v(p_thread), pg_temp.v(p_who), p_title,
|
|
168
|
+
coalesce(p_recipients, 'null::uuid[]'),
|
|
169
|
+
coalesce(quote_literal(pg_temp.v(p_board)) || '::uuid', 'null::uuid'),
|
|
170
|
+
p_key, p_metadata::text)
|
|
171
|
+
$$;
|
|
172
|
+
-- uuid[] literal of the named agents, in the order given (duplicates kept).
|
|
173
|
+
create function pg_temp.ids(variadic p_who text[]) returns text language sql as $$
|
|
174
|
+
select 'array[' || string_agg(quote_literal(pg_temp.v(w)), ',' order by o) || ']::uuid[]'
|
|
175
|
+
from unnest(p_who) with ordinality as x(w, o)
|
|
176
|
+
$$;
|
|
177
|
+
|
|
178
|
+
-- ===========================================================================
|
|
179
|
+
-- Fixtures
|
|
180
|
+
-- ===========================================================================
|
|
181
|
+
insert into agent_board.identity_providers
|
|
182
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
183
|
+
values ('https://portal.f2717.test', 'message-board', true, true, '{}', '{}', '{agent,human}');
|
|
184
|
+
|
|
185
|
+
do $$
|
|
186
|
+
declare r record;
|
|
187
|
+
begin
|
|
188
|
+
perform set_config('request.jwt.claims', '', true);
|
|
189
|
+
select * into r from agent_board.enroll_federated_human(
|
|
190
|
+
'https://portal.f2717.test', 'message-board', 'f2717-owner',
|
|
191
|
+
jsonb_build_object('sub', 'f2717-owner', 'email', 'owner@f2717.test', 'email_verified', true), 'F2 Owner');
|
|
192
|
+
perform pg_temp.put('O', r.agent_id::text);
|
|
193
|
+
perform pg_temp.put('O.auth', r.auth_user_id::text);
|
|
194
|
+
perform pg_temp.put('O.role', 'authenticated');
|
|
195
|
+
end $$;
|
|
196
|
+
|
|
197
|
+
-- Keyed agents: A creator, A2 second creator, C private recipient/assignee, M and N ordinary
|
|
198
|
+
-- board members (N joins later), X an agent outside every board. C is a member of B too: a
|
|
199
|
+
-- non-member cannot post task history on a board thread, and C's membership gives the board no
|
|
200
|
+
-- read path to a task delivered to C privately.
|
|
201
|
+
insert into auth.users(id, instance_id, aud, role, email)
|
|
202
|
+
select ('71720000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
203
|
+
'00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'f2717-' || n || '@t.test'
|
|
204
|
+
from generate_series(1, 6) n;
|
|
205
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
|
|
206
|
+
select ('71721000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, 'agent', 'agent',
|
|
207
|
+
'f2717-' || h, 'F2 ' || h, 'active', ('71720000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid
|
|
208
|
+
from unnest(array['a', 'a2', 'c', 'm', 'n', 'x']) with ordinality as x(h, n);
|
|
209
|
+
do $$
|
|
210
|
+
declare h text; n int := 0;
|
|
211
|
+
begin
|
|
212
|
+
foreach h in array array['A', 'A2', 'C', 'M', 'N', 'X'] loop
|
|
213
|
+
n := n + 1;
|
|
214
|
+
perform pg_temp.put(h, '71721000-0000-4000-8000-' || lpad(n::text, 12, '0'));
|
|
215
|
+
perform pg_temp.put(h || '.auth', '71720000-0000-4000-8000-' || lpad(n::text, 12, '0'));
|
|
216
|
+
perform pg_temp.put(h || '.role', 'authenticated');
|
|
217
|
+
end loop;
|
|
218
|
+
end $$;
|
|
219
|
+
select pg_temp.put('SVC.role', 'service_role');
|
|
220
|
+
-- Verified own_inbox callback subscriptions for C, M and X, so a delivery to any of them would
|
|
221
|
+
-- also queue a callback: the side-effect fingerprint then discriminates a callback write.
|
|
222
|
+
insert into agent_board.callback_endpoints(id, owner_agent_id, endpoint_url, endpoint_status)
|
|
223
|
+
select ('71723000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, pg_temp.u(h), 'https://example.test/f2717/' || h, 'verified'
|
|
224
|
+
from unnest(array['C', 'M', 'X']) with ordinality as x(h, n);
|
|
225
|
+
insert into agent_board.callback_subscriptions(endpoint_id, agent_id, subscription_scope, event_category)
|
|
226
|
+
select ('71723000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, pg_temp.u(h), 'own_inbox', 'inbox.item.delivered'
|
|
227
|
+
from unnest(array['C', 'M', 'X']) with ordinality as x(h, n);
|
|
228
|
+
|
|
229
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
230
|
+
values ('71722000-0000-4000-8000-000000000001', 'f2717-b', 'F2 B', pg_temp.u('A'), 'private', 'active'),
|
|
231
|
+
('71722000-0000-4000-8000-000000000002', 'f2717-b2', 'F2 B2', pg_temp.u('A'), 'private', 'active');
|
|
232
|
+
select pg_temp.put('B', '71722000-0000-4000-8000-000000000001'), pg_temp.put('B2', '71722000-0000-4000-8000-000000000002');
|
|
233
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
234
|
+
values (pg_temp.u('B'), pg_temp.u('A'), 'owner', 'joined', pg_temp.u('A')),
|
|
235
|
+
(pg_temp.u('B'), pg_temp.u('A2'), 'member', 'joined', pg_temp.u('A')),
|
|
236
|
+
(pg_temp.u('B'), pg_temp.u('C'), 'member', 'joined', pg_temp.u('A')),
|
|
237
|
+
(pg_temp.u('B'), pg_temp.u('M'), 'member', 'joined', pg_temp.u('A')),
|
|
238
|
+
(pg_temp.u('B'), pg_temp.u('O'), 'owner', 'joined', pg_temp.u('A')),
|
|
239
|
+
(pg_temp.u('B2'), pg_temp.u('A'), 'owner', 'joined', pg_temp.u('A')),
|
|
240
|
+
(pg_temp.u('B2'), pg_temp.u('O'), 'member', 'joined', pg_temp.u('A'));
|
|
241
|
+
|
|
242
|
+
-- Hosted delegates D (placed on B) and E (placed on B2 only), through the real exchange.
|
|
243
|
+
create function pg_temp.exch(p_iid text, p_gid text, p_key text) returns text language plpgsql as $$
|
|
244
|
+
declare r record;
|
|
245
|
+
begin
|
|
246
|
+
perform pg_temp.ident('service_role', null, null);
|
|
247
|
+
set local role service_role;
|
|
248
|
+
select * into r from agent_board.exchange_delegate_credential(jsonb_build_object(
|
|
249
|
+
'iss', 'https://portal.f2717.test', 'sub', 'f2717-owner', 'slot_resource', null,
|
|
250
|
+
'agent_board_delegate_instance', jsonb_build_object(
|
|
251
|
+
'instance_id', p_iid, 'grant_id', p_gid, 'generation', 1, 'owner_sub', 'f2717-owner',
|
|
252
|
+
'client_id', 'client-f2717', 'status', 'active', 'client_change', null)));
|
|
253
|
+
reset role;
|
|
254
|
+
perform pg_temp.ident(null, null, null);
|
|
255
|
+
perform pg_temp.put(p_key, r.agent_id::text);
|
|
256
|
+
perform pg_temp.put(p_key || '.auth', r.auth_user_id::text);
|
|
257
|
+
perform pg_temp.put(p_key || '.cred', r.credential_id::text);
|
|
258
|
+
perform pg_temp.put(p_key || '.role', 'agent_board_delegate');
|
|
259
|
+
return 'ok';
|
|
260
|
+
end $$;
|
|
261
|
+
do $$ begin
|
|
262
|
+
perform pg_temp.exch('inst_f2f2f2f2f2f27170_01', 'grant_f2f2f2f2f27170_01_1', 'D');
|
|
263
|
+
perform pg_temp.exch('inst_f2f2f2f2f2f27170_02', 'grant_f2f2f2f2f27170_02_1', 'E');
|
|
264
|
+
end $$;
|
|
265
|
+
|
|
266
|
+
-- 1
|
|
267
|
+
select is(
|
|
268
|
+
pg_temp.as_('O', format('select agent_board.add_delegate_to_board(%L, %L)', pg_temp.v('D'), pg_temp.v('B')))
|
|
269
|
+
|| ' ' || pg_temp.as_('O', format('select agent_board.add_delegate_to_board(%L, %L)', pg_temp.v('E'), pg_temp.v('B2'))),
|
|
270
|
+
'ok ok',
|
|
271
|
+
'fixture: the owner places delegate D on B and delegate E on B2 through add_delegate_to_board');
|
|
272
|
+
|
|
273
|
+
-- The board-stamped thread T: created by A, stamped by A's board post to B.
|
|
274
|
+
select pg_temp.put('T', pg_temp.val('A', $$ select thread_id::text from agent_board.create_or_find_thread(null, 'F2 thread', 'f2717-thread') $$));
|
|
275
|
+
-- 2
|
|
276
|
+
select pg_temp.put('T.post', pg_temp.as_('A', format($$ select * from agent_board.post_board_message(%L, %L, %L, 's', 'board post') $$,
|
|
277
|
+
pg_temp.v('T'), pg_temp.v('A'), pg_temp.v('B'))));
|
|
278
|
+
select is(
|
|
279
|
+
pg_temp.v('T.post') || ' ' || (select coalesce(board_id::text, 'null') from agent_board.threads where id = pg_temp.u('T')),
|
|
280
|
+
'ok ' || pg_temp.v('B'),
|
|
281
|
+
'fixture: thread T is stamped to board B by a board post before any task exists');
|
|
282
|
+
|
|
283
|
+
-- The private task X: recipient-only, key k1, on the already board-stamped thread.
|
|
284
|
+
select pg_temp.put('X.task', split_part(pg_temp.val('A',
|
|
285
|
+
replace(pg_temp.ctd('A', 'T', 'F2-SECRET-TASK', pg_temp.ids('C'), null, 'k1'), 'select *', 'select task_id::text')), ' ', 1));
|
|
286
|
+
select pg_temp.put('X.ev1', (select id::text from agent_board.task_events where task_id = pg_temp.u('X.task') and event_sequence = 1));
|
|
287
|
+
-- 3
|
|
288
|
+
select is(
|
|
289
|
+
(select count(*)::int from agent_board.tasks where thread_id = pg_temp.u('T') and metadata ->> 'idempotencyKey' = 'k1')
|
|
290
|
+
|| ' ' || pg_temp.scope('X.task'),
|
|
291
|
+
'1 f2717-c:private',
|
|
292
|
+
'first recipient-only send: one task, explicit private delivery to C only');
|
|
293
|
+
|
|
294
|
+
-- Legal assignment by the creator, then C's own history on the task through the public RPCs.
|
|
295
|
+
-- 4
|
|
296
|
+
select is(
|
|
297
|
+
pg_temp.as_('A', format($$ select * from agent_board.append_task_event(%L, %L, 'status_changed', '{}'::jsonb, null, %L) $$,
|
|
298
|
+
pg_temp.v('X.task'), pg_temp.v('A'), pg_temp.v('C')))
|
|
299
|
+
|| ' ' || pg_temp.as_('C', format($$ select * from agent_board.append_task_event(%L, %L, 'commented', '{"note":"C-PRIVATE-HISTORY"}'::jsonb) $$,
|
|
300
|
+
pg_temp.v('X.task'), pg_temp.v('C')))
|
|
301
|
+
|| ' ' || pg_temp.as_('C', format($$ select * from agent_board.acquire_claim(%L, 'task', 'f2717-c-claim', now() + interval '1 hour', %L, %L) $$,
|
|
302
|
+
pg_temp.v('C'), pg_temp.v('X.task'), pg_temp.v('T')))
|
|
303
|
+
|| ' ' || pg_temp.as_('C', format($$ select * from agent_board.raise_escalation(%L, %L, 'C-PRIVATE-ESCALATION', 'other', 'medium', %L, null, '{}'::jsonb) $$,
|
|
304
|
+
pg_temp.v('T'), pg_temp.v('C'), pg_temp.v('X.task'))),
|
|
305
|
+
'ok ok ok ok',
|
|
306
|
+
'fixture: A assigns X to C; C appends an event, takes a task claim and raises a task escalation');
|
|
307
|
+
-- 5 -- the exact history rows and their authors
|
|
308
|
+
select is(
|
|
309
|
+
(select string_agg(te.event_sequence || ':' || te.event_type || ':' || a.handle, ' ' order by te.event_sequence)
|
|
310
|
+
from agent_board.task_events te join agent_board.agents a on a.id = te.actor_agent_id where te.task_id = pg_temp.u('X.task'))
|
|
311
|
+
|| ' | ' || (select string_agg(a.handle, ',') from agent_board.claims c join agent_board.agents a on a.id = c.agent_id where c.task_id = pg_temp.u('X.task'))
|
|
312
|
+
|| ' | ' || (select string_agg(a.handle, ',') from agent_board.escalations e join agent_board.agents a on a.id = e.raised_by_agent_id where e.task_id = pg_temp.u('X.task'))
|
|
313
|
+
|| ' | assignee=' || (select a.handle from agent_board.tasks t join agent_board.agents a on a.id = t.assigned_agent_id where t.id = pg_temp.u('X.task')),
|
|
314
|
+
'1:created:f2717-a 2:status_changed:f2717-a 3:commented:f2717-c 4:claimed:f2717-c 5:escalated:f2717-c | f2717-c | f2717-c | assignee=f2717-c',
|
|
315
|
+
'fixture: X history is the creator''s two events plus C-authored event, claim and escalation; C is the assignee');
|
|
316
|
+
select pg_temp.put('X.history', pg_temp.history('X.task'));
|
|
317
|
+
|
|
318
|
+
-- ===========================================================================
|
|
319
|
+
-- Visibility before any replay (tasks/events/claims/escalations/evidence)
|
|
320
|
+
-- ===========================================================================
|
|
321
|
+
-- 6
|
|
322
|
+
select is(pg_temp.sees('M', 'X.task') || ' ' || pg_temp.sees('D', 'X.task') || ' ' || pg_temp.sees('E', 'X.task'),
|
|
323
|
+
'0/0/0/0/0 0/0/0/0/0 0/0/0/0/0',
|
|
324
|
+
'before replay: board member M, board delegate D and off-board delegate E see nothing of private task X');
|
|
325
|
+
-- 7
|
|
326
|
+
select is(pg_temp.sees('C', 'X.task') || ' ' || pg_temp.sees('A', 'X.task'),
|
|
327
|
+
'1/5/1/1/0 1/5/1/1/0',
|
|
328
|
+
'before replay: C (recipient and assignee) and A (creator) see X and its whole history');
|
|
329
|
+
|
|
330
|
+
-- ===========================================================================
|
|
331
|
+
-- Refused replays: each is 42501 with the named message and writes nothing
|
|
332
|
+
-- ===========================================================================
|
|
333
|
+
-- 8 -- S9: private -> same key + the thread's own board, by a creator who may send there
|
|
334
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'other title', null, 'B', 'k1')),
|
|
335
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
336
|
+
'S9: a private task replayed with a board target is refused, though the creator can send to that board');
|
|
337
|
+
-- 9
|
|
338
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'other title', pg_temp.ids('C'), 'B', 'k1')),
|
|
339
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
340
|
+
'private -> same recipients plus a board is refused');
|
|
341
|
+
-- 10
|
|
342
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'other title', pg_temp.ids('C', 'X'), null, 'k1')),
|
|
343
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
344
|
+
'a replay that introduces a new explicit recipient is refused');
|
|
345
|
+
-- 11
|
|
346
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'other title', null, 'B', 'k1',
|
|
347
|
+
jsonb_build_object('boardId', pg_temp.v('B'), 'rpcName', 'create_tasking_delivery', 'idempotencyKey', 'k1'))),
|
|
348
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
349
|
+
'caller metadata claiming boardId/rpcName on the replay is not evidence of prior board scope');
|
|
350
|
+
-- 12
|
|
351
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'other title', null, 'B2', 'k1')),
|
|
352
|
+
'42501:thread belongs to a different board | side-effects=none',
|
|
353
|
+
'same key to a different board is refused (the thread-board mismatch answers first)');
|
|
354
|
+
-- 13 -- service_role, the raw core: same refusal, and its telemetry write is rolled back too
|
|
355
|
+
select is(pg_temp.refuse('SVC', pg_temp.ctd('A', 'T', 'svc title', null, 'B', 'k1')),
|
|
356
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
357
|
+
'service_role on the UUID core: the same refusal, with the bypass telemetry row rolled back');
|
|
358
|
+
-- 14 -- addressing overload, board slug
|
|
359
|
+
select is(pg_temp.refuse('A', format($$ select * from agent_board.create_tasking_delivery(%L::uuid, %L::uuid, 'addr', null, null, 'f2717-b', 'addressing_v1', 'medium', 'distributed', 'k1', '{}'::jsonb) $$,
|
|
360
|
+
pg_temp.v('T'), pg_temp.v('A'))),
|
|
361
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
362
|
+
'addressing overload: same key with the board slug is refused');
|
|
363
|
+
-- 15 -- addressing overload, descriptors naming a new recipient
|
|
364
|
+
select is(pg_temp.refuse('A', format($$ select * from agent_board.create_tasking_delivery(%L::uuid, %L::uuid, 'addr', null,
|
|
365
|
+
jsonb_build_array(jsonb_build_object('kind', 'agent_id', 'agentId', %L), jsonb_build_object('kind', 'agent_id', 'agentId', %L)),
|
|
366
|
+
null, 'addressing_v1', 'medium', 'distributed', 'k1', '{}'::jsonb) $$,
|
|
367
|
+
pg_temp.v('T'), pg_temp.v('A'), pg_temp.v('C'), pg_temp.v('X'))),
|
|
368
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
369
|
+
'addressing overload: same key with a descriptor naming a new recipient is refused');
|
|
370
|
+
|
|
371
|
+
-- Signed wrapper, both branches, as service_role with a gateway decision for A. The decision is a
|
|
372
|
+
-- SQL fixture: this proves the wrapper's routing and atomicity, not signature verification.
|
|
373
|
+
create function pg_temp.gw(p_actor text) returns text language sql as $$
|
|
374
|
+
select quote_literal(jsonb_build_object('agent_id', pg_temp.v(p_actor), 'verification_status', 'warn',
|
|
375
|
+
'payload_digest', 'sha256:' || repeat('7', 64), 'enforcement_mode', 'warn')::text) || '::jsonb'
|
|
376
|
+
$$;
|
|
377
|
+
create function pg_temp.signed(p_payload jsonb, p_actor text default 'A') returns text language sql as $$
|
|
378
|
+
select format('select * from agent_board.signed_create_tasking_delivery(%L::jsonb, %s)', p_payload::text, pg_temp.gw(p_actor))
|
|
379
|
+
$$;
|
|
380
|
+
-- 16
|
|
381
|
+
select is(pg_temp.refuse('SVC', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A'),
|
|
382
|
+
'p_title', 'signed', 'p_board_id', pg_temp.v('B'), 'p_idempotency_key', 'k1'))),
|
|
383
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
384
|
+
'signed UUID branch: board widening refused, no signature evidence, delivery or callback written');
|
|
385
|
+
-- 17
|
|
386
|
+
select is(pg_temp.refuse('SVC', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A'),
|
|
387
|
+
'p_title', 'signed', 'p_recipient_agent_ids', jsonb_build_array(pg_temp.v('C'), pg_temp.v('X')), 'p_idempotency_key', 'k1'))),
|
|
388
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
389
|
+
'signed UUID branch: recipient widening refused with no side effect');
|
|
390
|
+
-- 18
|
|
391
|
+
select is(pg_temp.refuse('SVC', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A'),
|
|
392
|
+
'p_title', 'signed', 'p_board_slug', 'f2717-b', 'p_addressing_mode', 'addressing_v1', 'p_idempotency_key', 'k1'))),
|
|
393
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
394
|
+
'signed descriptor/slug branch: board widening refused with no side effect');
|
|
395
|
+
-- 19
|
|
396
|
+
select is(pg_temp.refuse('SVC', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A'),
|
|
397
|
+
'p_title', 'signed', 'p_recipient_descriptors', jsonb_build_array(jsonb_build_object('kind', 'agent_id', 'agentId', pg_temp.v('X'))),
|
|
398
|
+
'p_addressing_mode', 'addressing_v1', 'p_idempotency_key', 'k1'))),
|
|
399
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
400
|
+
'signed descriptor branch: a new descriptor recipient is refused with no side effect');
|
|
401
|
+
-- 20
|
|
402
|
+
select is(pg_temp.refuse('SVC', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A2'),
|
|
403
|
+
'p_title', 'signed', 'p_board_id', pg_temp.v('B'), 'p_idempotency_key', 'k1'))),
|
|
404
|
+
'42501:actor_mismatch: signed write payload actor must match gateway actor | side-effects=none',
|
|
405
|
+
'signed: a payload actor that differs from the gateway actor is refused');
|
|
406
|
+
-- 21
|
|
407
|
+
select is(pg_temp.refuse('A', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_title', 'signed', 'p_board_id', pg_temp.v('B'), 'p_idempotency_key', 'k1')))
|
|
408
|
+
|| ' || ' || pg_temp.refuse('D', pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_title', 'signed', 'p_board_id', pg_temp.v('B'), 'p_idempotency_key', 'k1'))),
|
|
409
|
+
'42501:permission denied for function signed_create_tasking_delivery | side-effects=none || 42501:permission denied for function signed_create_tasking_delivery | side-effects=none',
|
|
410
|
+
'signed wrapper: an authenticated agent and a hosted delegate cannot call it directly');
|
|
411
|
+
-- 22
|
|
412
|
+
select is(pg_temp.refuse('D', pg_temp.ctd('A', 'T', 'delegate', null, 'B', 'k1')),
|
|
413
|
+
'42501:permission denied for function create_tasking_delivery | side-effects=none',
|
|
414
|
+
'a hosted delegate cannot reach the tasking core at all');
|
|
415
|
+
-- 23 -- existing authorization: a caller cannot supply another creator
|
|
416
|
+
select is(pg_temp.refuse('M', pg_temp.ctd('A', 'T', 'impersonated', null, 'B', 'k1')),
|
|
417
|
+
'42501:p_created_by_agent_id must match current_agent_id() | side-effects=none',
|
|
418
|
+
'an authenticated caller naming another creator is refused by the existing actor binding');
|
|
419
|
+
|
|
420
|
+
-- Forged reserved metadata through create_task: the lookup finds the forged task, and no
|
|
421
|
+
-- metadata on it can establish a delivery scope.
|
|
422
|
+
-- 24
|
|
423
|
+
select is(pg_temp.as_('A', format($$ select * from agent_board.create_task(%L, %L, 'FORGED', null, null, null, 'medium', 'distributed', %L::jsonb) $$,
|
|
424
|
+
pg_temp.v('T'), pg_temp.v('A'),
|
|
425
|
+
jsonb_build_object('rpcName', 'create_tasking_delivery', 'idempotencyKey', 'kf', 'boardId', pg_temp.v('B'))::text)),
|
|
426
|
+
'ok', 'fixture: create_task accepts caller metadata carrying the reserved rpcName/idempotencyKey/boardId fields');
|
|
427
|
+
select pg_temp.put('F.task', (select id::text from agent_board.tasks where title = 'FORGED' and thread_id = pg_temp.u('T')));
|
|
428
|
+
-- 25
|
|
429
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'forged replay', null, 'B', 'kf'))
|
|
430
|
+
|| ' || ' || pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'forged replay', pg_temp.ids('C'), null, 'kf')),
|
|
431
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none || 42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
432
|
+
'a task whose metadata forges boardId has no persisted delivery scope: board and recipient replays are refused');
|
|
433
|
+
-- 26
|
|
434
|
+
select is(pg_temp.scope('F.task') || ' ' || pg_temp.sees('M', 'F.task') || ' ' || pg_temp.sees('D', 'F.task'),
|
|
435
|
+
'(none) 0/0/0/0/0 0/0/0/0/0',
|
|
436
|
+
'the forged task stays undelivered and invisible to the board');
|
|
437
|
+
|
|
438
|
+
-- ===========================================================================
|
|
439
|
+
-- Board-only task Y: explicit private scope cannot be introduced on replay
|
|
440
|
+
-- ===========================================================================
|
|
441
|
+
select pg_temp.put('Y.task', split_part(pg_temp.val('A',
|
|
442
|
+
replace(pg_temp.ctd('A', 'T', 'F2-BOARD-TASK', null, 'B', 'kb'), 'select *', 'select task_id::text')), ' ', 1));
|
|
443
|
+
-- 27
|
|
444
|
+
select is(pg_temp.scope('Y.task'),
|
|
445
|
+
(select string_agg(a.handle || ':f2717-b', ',' order by a.handle) from agent_board.board_memberships bm
|
|
446
|
+
join agent_board.agents a on a.id = bm.agent_id
|
|
447
|
+
where bm.board_id = pg_temp.u('B') and bm.membership_state in ('joined', 'subscribed')),
|
|
448
|
+
'board-only task Y: one board-scoped delivery per current member of B (the creator''s feed-only row included)');
|
|
449
|
+
-- 28
|
|
450
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'y', pg_temp.ids('M'), 'B', 'kb'))
|
|
451
|
+
|| ' || ' || pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'y', pg_temp.ids('X'), 'B', 'kb')),
|
|
452
|
+
'42501:tasking replay cannot widen delivery scope | side-effects=none || 42501:tasking replay cannot widen delivery scope | side-effects=none',
|
|
453
|
+
'board task -> same key naming an explicit recipient is refused, even one that already holds a board-only delivery');
|
|
454
|
+
-- 29
|
|
455
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'y', null, 'B2', 'kb')),
|
|
456
|
+
'42501:thread belongs to a different board | side-effects=none',
|
|
457
|
+
'board task -> same key to a different board is refused');
|
|
458
|
+
|
|
459
|
+
-- ===========================================================================
|
|
460
|
+
-- Accepted replays
|
|
461
|
+
-- ===========================================================================
|
|
462
|
+
-- 30 -- same explicit set, reordered and duplicated: same task and event, nothing new written
|
|
463
|
+
select pg_temp.put('fp.before', pg_temp.fp_rows());
|
|
464
|
+
select is(
|
|
465
|
+
pg_temp.val('A', replace(pg_temp.ctd('A', 'T', 'retry', pg_temp.ids('C', 'C'), null, 'k1'), 'select *', 'select task_id::text || '' '' || created_event_id::text')),
|
|
466
|
+
pg_temp.v('X.task') || ' ' || pg_temp.v('X.ev1'),
|
|
467
|
+
'normal retry (recipient duplicated): same task id and created event id');
|
|
468
|
+
-- 31
|
|
469
|
+
select is(pg_temp.fp_rows(), pg_temp.v('fp.before'),
|
|
470
|
+
'normal retry: no new task, event, delivery, inbox item, callback, evidence or telemetry row');
|
|
471
|
+
-- 32 -- the service path's normal retry is also the canonical task
|
|
472
|
+
select is(
|
|
473
|
+
pg_temp.val('SVC', replace(pg_temp.ctd('A', 'T', 'retry', pg_temp.ids('C'), null, 'k1'), 'select *', 'select task_id::text')),
|
|
474
|
+
pg_temp.v('X.task'),
|
|
475
|
+
'service_role normal retry with the same audience returns the canonical task');
|
|
476
|
+
-- 33 -- signed normal retry, UUID branch
|
|
477
|
+
select is(
|
|
478
|
+
pg_temp.val('SVC', replace(pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A'),
|
|
479
|
+
'p_title', 'signed retry', 'p_recipient_agent_ids', jsonb_build_array(pg_temp.v('C')), 'p_idempotency_key', 'k1')), 'select *', 'select task_id::text')),
|
|
480
|
+
pg_temp.v('X.task'),
|
|
481
|
+
'signed UUID branch normal retry returns the canonical task');
|
|
482
|
+
-- 34 -- signed normal retry, descriptor branch
|
|
483
|
+
select is(
|
|
484
|
+
pg_temp.val('SVC', replace(pg_temp.signed(jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_created_by_agent_id', pg_temp.v('A'),
|
|
485
|
+
'p_title', 'signed retry', 'p_recipient_descriptors', jsonb_build_array(jsonb_build_object('kind', 'agent_id', 'agentId', pg_temp.v('C'))),
|
|
486
|
+
'p_addressing_mode', 'addressing_v1', 'p_idempotency_key', 'k1')), 'select *', 'select task_id::text')),
|
|
487
|
+
pg_temp.v('X.task'),
|
|
488
|
+
'signed descriptor branch normal retry returns the canonical task');
|
|
489
|
+
|
|
490
|
+
-- Subset: Z goes to C and X; a replay naming only X is accepted and removes nothing.
|
|
491
|
+
select pg_temp.put('Z.task', split_part(pg_temp.val('A',
|
|
492
|
+
replace(pg_temp.ctd('A', 'T', 'F2-Z', pg_temp.ids('C', 'X'), null, 'kz'), 'select *', 'select task_id::text')), ' ', 1));
|
|
493
|
+
-- 35
|
|
494
|
+
select is(
|
|
495
|
+
pg_temp.val('A', replace(pg_temp.ctd('A', 'T', 'z', pg_temp.ids('X'), null, 'kz'), 'select *', 'select task_id::text'))
|
|
496
|
+
|| ' ' || pg_temp.scope('Z.task'),
|
|
497
|
+
pg_temp.v('Z.task') || ' f2717-c:private,f2717-x:private',
|
|
498
|
+
'subset of the existing private recipients is accepted; the omitted recipient keeps its delivery');
|
|
499
|
+
|
|
500
|
+
-- Mixed W (C + board B): omitting the board on replay is accepted and revokes nothing.
|
|
501
|
+
select pg_temp.put('W.task', split_part(pg_temp.val('A',
|
|
502
|
+
replace(pg_temp.ctd('A', 'T', 'F2-W', pg_temp.ids('C'), 'B', 'kw'), 'select *', 'select task_id::text')), ' ', 1));
|
|
503
|
+
select pg_temp.put('W.scope', pg_temp.scope('W.task'));
|
|
504
|
+
-- 36
|
|
505
|
+
select is(
|
|
506
|
+
pg_temp.val('A', replace(pg_temp.ctd('A', 'T', 'w', pg_temp.ids('C'), null, 'kw'), 'select *', 'select task_id::text'))
|
|
507
|
+
|| ' ' || (pg_temp.scope('W.task') = pg_temp.v('W.scope'))::text,
|
|
508
|
+
pg_temp.v('W.task') || ' true',
|
|
509
|
+
'mixed task: replay omitting the board is accepted; the board deliveries are not removed');
|
|
510
|
+
-- 37
|
|
511
|
+
select is(pg_temp.v('W.scope') ~ 'f2717-c:private' and pg_temp.v('W.scope') ~ 'f2717-m:f2717-b', true,
|
|
512
|
+
'mixed task control: C holds a private delivery and M a board one');
|
|
513
|
+
-- 38 -- mixed: C is BOTH explicit and on no board; replay with the same mix is accepted
|
|
514
|
+
select is(
|
|
515
|
+
pg_temp.val('A', replace(pg_temp.ctd('A', 'T', 'w', pg_temp.ids('C'), 'B', 'kw'), 'select *', 'select task_id::text')),
|
|
516
|
+
pg_temp.v('W.task'),
|
|
517
|
+
'mixed task: same explicit set and same board is accepted');
|
|
518
|
+
|
|
519
|
+
-- Same board, new member: N joins B after Y; a same-board replay reaches N under live fan-out.
|
|
520
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
521
|
+
values (pg_temp.u('B'), pg_temp.u('N'), 'member', 'joined', pg_temp.u('A'));
|
|
522
|
+
-- 39
|
|
523
|
+
select is(
|
|
524
|
+
pg_temp.val('A', replace(pg_temp.ctd('A', 'T', 'y', null, 'B', 'kb'), 'select *', 'select task_id::text'))
|
|
525
|
+
|| ' ' || (pg_temp.scope('Y.task') ~ 'f2717-n:f2717-b')::text || ' ' || pg_temp.sees('N', 'Y.task'),
|
|
526
|
+
pg_temp.v('Y.task') || ' true 1/0/0/0/0',
|
|
527
|
+
'same-board replay after N joined: same task, N receives the board delivery and can read the task');
|
|
528
|
+
|
|
529
|
+
-- New key: a deliberate board task is a NEW task with no copied private history.
|
|
530
|
+
select pg_temp.put('Y2.task', split_part(pg_temp.val('A',
|
|
531
|
+
replace(pg_temp.ctd('A', 'T', 'F2-NEW-BOARD-TASK', null, 'B', 'k1-board'), 'select *', 'select task_id::text')), ' ', 1));
|
|
532
|
+
-- 40
|
|
533
|
+
select is(
|
|
534
|
+
(pg_temp.v('Y2.task') <> pg_temp.v('X.task'))::text
|
|
535
|
+
|| ' ' || (select count(*) from agent_board.task_events where task_id = pg_temp.u('Y2.task'))
|
|
536
|
+
|| '/' || (select count(*) from agent_board.claims where task_id = pg_temp.u('Y2.task'))
|
|
537
|
+
|| '/' || (select count(*) from agent_board.escalations where task_id = pg_temp.u('Y2.task'))
|
|
538
|
+
|| '/' || (select count(*) from agent_board.tasks where id = pg_temp.u('Y2.task') and assigned_agent_id is null),
|
|
539
|
+
'true 1/0/0/1',
|
|
540
|
+
'a new key with a board target creates a new task: one created event, no claim, escalation or assignee copied');
|
|
541
|
+
-- 41 -- positive control for the delegate predicate: D reads the new board task
|
|
542
|
+
select is(pg_temp.sees('D', 'Y2.task') || ' ' || pg_temp.sees('E', 'Y2.task'),
|
|
543
|
+
'1/1/0/0/0 0/0/0/0/0',
|
|
544
|
+
'delegate control: D (on B) reads the new board task and its event; E (not on B) does not');
|
|
545
|
+
-- 42 -- ordinary members are a separate policy: M reads the board task, not its history
|
|
546
|
+
select is(pg_temp.sees('M', 'Y2.task'),
|
|
547
|
+
'1/0/0/0/0',
|
|
548
|
+
'ordinary member M reads the board task row but, under the authenticated policy, none of its events');
|
|
549
|
+
|
|
550
|
+
-- Another resolved creator with the same key gets its own task and cannot reach X.
|
|
551
|
+
-- Same audience as X (C, private), so a lookup widened across creators would hand A2 task X.
|
|
552
|
+
select pg_temp.put('A2.task', pg_temp.val('A2', replace(pg_temp.ctd('A2', 'T', 'A2 task', pg_temp.ids('C'), null, 'k1'), 'select *', 'select task_id::text')));
|
|
553
|
+
-- 43
|
|
554
|
+
select is(
|
|
555
|
+
(pg_temp.v('A2.task') <> pg_temp.v('X.task'))::text
|
|
556
|
+
|| ' ' || coalesce((select a.handle || '/' || t.title from agent_board.tasks t join agent_board.agents a on a.id = t.created_by_agent_id
|
|
557
|
+
where t.id::text = pg_temp.v('A2.task')), pg_temp.v('A2.task'))
|
|
558
|
+
|| ' ' || pg_temp.scope('X.task') || ' ' || (pg_temp.history('X.task') = pg_temp.v('X.history'))::text,
|
|
559
|
+
'true f2717-a2/A2 task f2717-c:private true',
|
|
560
|
+
'same key by another creator: A2 gets its own new task; X''s scope and history are untouched');
|
|
561
|
+
|
|
562
|
+
-- Existing authorization still applies: a creator who lost its send role cannot replay to the board.
|
|
563
|
+
update agent_board.board_memberships set role = 'observer' where board_id = pg_temp.u('B') and agent_id = pg_temp.u('A');
|
|
564
|
+
-- 44
|
|
565
|
+
select is(pg_temp.refuse('A', pg_temp.ctd('A', 'T', 'y', null, 'B', 'kb')),
|
|
566
|
+
'42501:board send requires a non-observer board role | side-effects=none',
|
|
567
|
+
'a creator demoted to observer cannot replay its board task (existing send authority)');
|
|
568
|
+
update agent_board.board_memberships set role = 'owner' where board_id = pg_temp.u('B') and agent_id = pg_temp.u('A');
|
|
569
|
+
|
|
570
|
+
-- ===========================================================================
|
|
571
|
+
-- After every attempt: X is exactly as it was, and invisible to the board
|
|
572
|
+
-- ===========================================================================
|
|
573
|
+
-- 45
|
|
574
|
+
select is(pg_temp.scope('X.task'), 'f2717-c:private', 'after all attempts: X is still delivered to C only');
|
|
575
|
+
-- 46
|
|
576
|
+
select is(pg_temp.history('X.task'), pg_temp.v('X.history'),
|
|
577
|
+
'after all attempts: X and its history keep their ids, authors and content');
|
|
578
|
+
-- 47
|
|
579
|
+
select is(pg_temp.sees('M', 'X.task'), '0/0/0/0/0', 'after all attempts: board member M sees nothing of X');
|
|
580
|
+
-- 48
|
|
581
|
+
select is(pg_temp.sees('D', 'X.task'), '0/0/0/0/0',
|
|
582
|
+
'after all attempts: board delegate D sees neither X nor C''s event, claim or escalation');
|
|
583
|
+
-- 49
|
|
584
|
+
select is(pg_temp.sees('E', 'X.task'), '0/0/0/0/0', 'after all attempts: off-board delegate E sees nothing of X');
|
|
585
|
+
-- 50
|
|
586
|
+
-- The two signed normal retries (33, 34) each recorded A's signature evidence on X's created
|
|
587
|
+
-- event; C reads it as the assignee. Nothing else changed for C.
|
|
588
|
+
select is(pg_temp.sees('C', 'X.task'), '1/5/1/1/2', 'after all attempts: C keeps exactly its own access to X (plus the two signed-retry evidence rows)');
|
|
589
|
+
-- 51
|
|
590
|
+
select is(
|
|
591
|
+
(select count(*)::int from agent_board.inbox_items where source_task_id = pg_temp.u('X.task'))
|
|
592
|
+
|| '/' || (select count(*)::int from agent_board.callback_outbox_events where source_task_id = pg_temp.u('X.task')),
|
|
593
|
+
'1/1',
|
|
594
|
+
'after all attempts: X has one inbox item and one callback, both C''s (retries queued no duplicate)');
|
|
595
|
+
|
|
596
|
+
-- ===========================================================================
|
|
597
|
+
-- The core's catalog surface, and the replay class enumerated from the catalog
|
|
598
|
+
-- ===========================================================================
|
|
599
|
+
-- Lock identity, lock order and refusal placement are pinned BEHAVIOURALLY, not by reading the
|
|
600
|
+
-- source: tests/isolation/specs/tasking_replay_scope.spec (a lock keyed or ordered differently
|
|
601
|
+
-- lets the racing board send create a second task), 30/43 here (same identity reuses, another
|
|
602
|
+
-- creator does not), and every refusal above reporting side-effects=none.
|
|
603
|
+
-- 52
|
|
604
|
+
select is(
|
|
605
|
+
(select prosecdef::text || '/' || coalesce(array_to_string(proconfig, ','), '') from pg_proc
|
|
606
|
+
where oid = 'agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)'::regprocedure)
|
|
607
|
+
|| ' ' || (select string_agg(r || '=' || has_function_privilege(r, 'agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)', 'EXECUTE')::text, ' ' order by r)
|
|
608
|
+
from unnest(array['anon', 'authenticated', 'agent_board_delegate', 'service_role']) r),
|
|
609
|
+
'true/search_path=agent_board, public agent_board_delegate=false anon=false authenticated=true service_role=true',
|
|
610
|
+
'the core stays SECURITY DEFINER with its search_path, EXECUTE for authenticated and service_role only');
|
|
611
|
+
-- 53
|
|
612
|
+
select ok(obj_description('agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)'::regprocedure, 'pg_proc')
|
|
613
|
+
~ 'cannot widen the task''s audience',
|
|
614
|
+
'the core''s comment states the replay ceiling');
|
|
615
|
+
|
|
616
|
+
-- THE REPLAY CLASS: every agent_board routine that serializes an idempotent replay through
|
|
617
|
+
-- _lock_idempotency_key, read from pg_proc. A new site fails here and names itself. Each
|
|
618
|
+
-- disposition is a claim pinned by behaviour elsewhere, named in the value:
|
|
619
|
+
-- create_tasking_delivery ceiling -- this file
|
|
620
|
+
-- post_board_message one-board -- association pins the thread to one board (56 here,
|
|
621
|
+
-- #667 boundary/writers tests), so a replay can only
|
|
622
|
+
-- re-reach that board's current members
|
|
623
|
+
-- send_direct_message fixed-717-F2b-740 -- message replay ceiling (#740): a replay may only
|
|
624
|
+
-- send_multicast_message fixed-717-F2b-740 re-reach recipients already holding a private
|
|
625
|
+
-- delivery of the source in the core's own mode;
|
|
626
|
+
-- pinned behaviourally and by mutation in
|
|
627
|
+
-- agent_board_717_message_replay_scope.test.sql
|
|
628
|
+
create temp table _replay_class(signature text primary key, disposition text not null);
|
|
629
|
+
insert into _replay_class values
|
|
630
|
+
('create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)', 'ceiling'),
|
|
631
|
+
('post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'one-board'),
|
|
632
|
+
('send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'fixed-717-F2b-740'),
|
|
633
|
+
('send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)', 'fixed-717-F2b-740');
|
|
634
|
+
-- 54
|
|
635
|
+
select is(
|
|
636
|
+
(select coalesce(string_agg(s, ' ' order by s), '(none)') from (
|
|
637
|
+
select p.proname || '(' || replace(oidvectortypes(p.proargtypes), ' ', '') || ')' as s
|
|
638
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
639
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
640
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_lock_idempotency_key')
|
|
641
|
+
except
|
|
642
|
+
select replace(signature, ' ', '') from _replay_class
|
|
643
|
+
) d),
|
|
644
|
+
'(none)',
|
|
645
|
+
'CLASS: every caller of _lock_idempotency_key has a disposition (a new replay site names itself)');
|
|
646
|
+
-- 55
|
|
647
|
+
select is(
|
|
648
|
+
(select coalesce(string_agg(c.signature, ' ' order by c.signature), '(none)') from _replay_class c
|
|
649
|
+
where not exists (select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
650
|
+
where n.nspname = 'agent_board'
|
|
651
|
+
and p.proname || '(' || replace(oidvectortypes(p.proargtypes), ' ', '') || ')' = replace(c.signature, ' ', '')
|
|
652
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_lock_idempotency_key'))),
|
|
653
|
+
'(none)',
|
|
654
|
+
'CLASS: every dispositioned signature still exists and still calls _lock_idempotency_key (no stale exemption)');
|
|
655
|
+
-- 56 -- the one-board disposition, behaviourally: a same-key board post cannot move to another board
|
|
656
|
+
select is(pg_temp.refuse('A', format($$ select * from agent_board.post_board_message(%L, %L, %L, 's', 'moved', 'comment', 'medium', 'pk1') $$,
|
|
657
|
+
pg_temp.v('T'), pg_temp.v('A'), pg_temp.v('B2'))),
|
|
658
|
+
'42501:thread belongs to a different board | side-effects=none',
|
|
659
|
+
'one-board disposition: a board post on T cannot target another board');
|
|
660
|
+
select * from finish();
|
|
661
|
+
rollback;
|