@consiliency/agent-board-schema 1.40.0 → 1.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-schema",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
5
5
  "repository": "https://github.com/Consiliency/message-board.git",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "schema": "agent_board_schema_manifest.v1",
3
3
  "interface": "IF-0-MBPLANE-BUNDLE-1",
4
4
  "package": "@consiliency/agent-board-schema",
5
- "packageVersion": "1.40.0",
5
+ "packageVersion": "1.41.0",
6
6
  "schemaVersion": 8,
7
7
  "minClientVersion": "1.1.0",
8
8
  "source": {
@@ -46,8 +46,8 @@
46
46
  "usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
47
47
  }
48
48
  ],
49
- "migrationCount": 138,
50
- "aggregateSha256": "e5e6870533c69d20b446431b6fb19693588d9eb4bb55e1fb3f7f239afcfa76c0",
49
+ "migrationCount": 139,
50
+ "aggregateSha256": "0c0daf769baedbae5243a7345d44df09613494c5ea2495b342222a7248dde6d0",
51
51
  "migrations": [
52
52
  {
53
53
  "order": 0,
@@ -1290,6 +1290,15 @@
1290
1290
  "sourceRef": "message-board",
1291
1291
  "bytes": 7556,
1292
1292
  "sha256": "670d6e97ca84ef74ebd10a6ee102c328005a28aa48f8668ba444ce31b714cf02"
1293
+ },
1294
+ {
1295
+ "order": 138,
1296
+ "id": "20260921000100_agent_board_task_event_origin_posture",
1297
+ "filename": "20260921000100_agent_board_task_event_origin_posture.sql",
1298
+ "origin": "message-board-forward",
1299
+ "sourceRef": "message-board",
1300
+ "bytes": 48237,
1301
+ "sha256": "c515e9cd08b73c118de64125da94735979add5d1128d3ff070ff904af77fef81"
1293
1302
  }
1294
1303
  ],
1295
1304
  "excludedBridge": {
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
38
38
  expect(manifest.schemaVersion).toBe(8);
39
39
  expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
40
40
  });
41
- it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 138)', () => {
41
+ it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 139)', () => {
42
42
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
43
- expect(manifest.migrationCount).toBe(138);
43
+ expect(manifest.migrationCount).toBe(139);
44
44
  // MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
45
45
  // schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
46
46
  // enrollment-only additive surface (identity_providers registry, enrollment audit,
@@ -233,6 +233,18 @@ describe('manifest integrity', () => {
233
233
  // sources still takes the permissive branch. Definition-only: CREATE OR REPLACE with the
234
234
  // identical signature, grants and COMMENT restated in the same migration.
235
235
  '20260920000100_agent_board_realtime_credential_claim.sql',
236
+ // #288 ORIGINPOSTURE. v_task_tree.verification_status was NULL for every task_event
237
+ // except the two written by a signed wrapper, and NULL cannot be told apart from
238
+ // "evidence was lost", "the writer forgot" and "unsigned on purpose". The class was
239
+ // derived by CALL CLOSURE (a member iff its closure reaches an INSERT INTO task_events):
240
+ // 12 members, where the issue body said 8 and its own triage comment said 5-6 -- neither
241
+ // naming create_tasking_delivery or signed_create_tasking_delivery. task_events gains
242
+ // origin_posture NOT NULL with NO DEFAULT, so a new direct-INSERT writer cannot insert at
243
+ // all; the funnel body moves behind an ungranted _append_task_event whose posture
244
+ // parameter is FIRST and undefaulted, which is what makes "no default" structural --
245
+ // PostgreSQL refuses a default before non-defaulted parameters. Nothing is DROPped: both
246
+ // public signatures stay byte-identical, so ACLs and COMMENTs survive.
247
+ '20260921000100_agent_board_task_event_origin_posture.sql',
236
248
  ]) {
237
249
  expect(names.has(f)).toBe(true);
238
250
  }
@@ -72,10 +72,15 @@ begin
72
72
  (v_task_a, v_thread_a, v_agent_a, v_agent_a, v_task_a, 'Auth RLS Task A', 'medium', 'distributed'),
73
73
  (v_task_b, v_thread_b, v_agent_b, v_agent_b, v_task_b, 'Auth RLS Task B', 'medium', 'distributed');
74
74
 
75
- insert into agent_board.task_events(task_id, thread_id, actor_agent_id, event_sequence, event_type, payload)
75
+ -- #288: origin_posture is NOT NULL with no default, so this raw fixture insert must declare
76
+ -- one like any other writer. It emulates create_task's seq-1 'created' event, so it declares
77
+ -- create_task's own posture rather than a test-only placeholder -- a fixture that labelled
78
+ -- itself differently from the writer it stands in for would make OP-15-style invariants
79
+ -- assert against a shape production never produces.
80
+ insert into agent_board.task_events(task_id, thread_id, actor_agent_id, event_sequence, event_type, payload, origin_posture)
76
81
  values
77
- (v_task_a, v_thread_a, v_agent_a, 1, 'created', '{}'::jsonb),
78
- (v_task_b, v_thread_b, v_agent_b, 1, 'created', '{}'::jsonb);
82
+ (v_task_a, v_thread_a, v_agent_a, 1, 'created', '{}'::jsonb, 'unattested:signed-metadata-rpc'),
83
+ (v_task_b, v_thread_b, v_agent_b, 1, 'created', '{}'::jsonb, 'unattested:signed-metadata-rpc');
79
84
 
80
85
  insert into agent_board.messages(id, thread_id, author_agent_id, message_sequence, message_type, body)
81
86
  values
@@ -511,7 +511,7 @@ select is(
511
511
  where n.nspname = 'agent_board'
512
512
  and p.proname like 'signed\_%'
513
513
  and agent_board_test.stripped_body(p.oid)
514
- ~ '\m(send_direct_message|send_multicast_message|post_board_message|append_task_event|create_tasking_delivery)\s*\('),
514
+ ~ '\m(send_direct_message|send_multicast_message|post_board_message|_?append_task_event|create_tasking_delivery)\s*\('),
515
515
  'signed_append_task_event,signed_create_tasking_delivery,signed_post_board_message,'
516
516
  || 'signed_send_direct_message,signed_send_multicast_message',
517
517
  'the five exempt wrappers each perform a message/task write, which is why they are exempt');
@@ -0,0 +1,393 @@
1
+ -- Consiliency/message-board#288 pgTAP: every task_events row declares HOW it was written.
2
+ --
3
+ -- THE DEFECT. v_task_tree.verification_status is NULL for every task_event written by
4
+ -- anything except the two signed wrappers, and NULL cannot be told apart from "evidence was
5
+ -- lost", "the writer forgot" and "unsigned on purpose". The fix stamps the writer's posture
6
+ -- into a DISTINCT column, so the blank becomes a fact.
7
+ --
8
+ -- WHY THE CLASS ASSERTION IS THE CENTREPIECE. The issue body enumerated 8 members and its own
9
+ -- triage comment enumerated 5-6; the catalog says 12, and neither list named
10
+ -- create_tasking_delivery or signed_create_tasking_delivery. Two hand-written lists, both
11
+ -- wrong, in one issue. OP-4/OP-5 therefore ask pg_proc for the class rather than checking a
12
+ -- list off, so a THIRTEENTH writer fails them AND NAMES ITSELF -- but only once OP-20 is read
13
+ -- alongside them. An earlier version of this sentence stopped at "names itself", and three
14
+ -- reviewers independently named the shape it missed: a writer calling the PUBLIC
15
+ -- append_task_event enters the class, compiles, is stamped 'unattested:direct-rpc', and is
16
+ -- named by neither OP-4 (which inspects only _append_task_event call sites) nor OP-5 (which
17
+ -- inspects only direct INSERTs). OP-20 is what closes it.
18
+ --
19
+ -- WHAT EACH LAYER DOES AND DOES NOT BUY -- calling the second one structural would be the
20
+ -- overclaim these tests exist to prevent:
21
+ -- * OP-1/OP-2 are STRUCTURAL. origin_posture is NOT NULL with no default, so a writer that
22
+ -- opens a new DIRECT INSERT path cannot insert a row at all. No test has to remember.
23
+ -- * OP-4 is NAMED, NOT STRUCTURAL. A writer that goes through the funnel and omits the
24
+ -- argument would inherit the funnel's default rather than erroring -- no SQL construct
25
+ -- prevents that, since a caller can always route through a defaulting wrapper. This
26
+ -- assertion is what turns that from silent into named.
27
+ --
28
+ -- LITERALS ARE KEPT ON PURPOSE. agent_board_test.stripped_body strips string literals BY
29
+ -- DEFAULT, and the whole predicate here is "is a literal posture passed at the call site" --
30
+ -- so OP-4 passes p_keep_literals => true. With the default, every call site would read as
31
+ -- `_append_task_event(` with nothing after it and the assertion would pass VACUOUSLY.
32
+ -- Comments are stripped in both modes, so prose naming a call site cannot satisfy it.
33
+ --
34
+ -- OP-6 EXISTS BECAUSE ABSENCE OF FAILURE IS NOT SUCCESS. A derivation that silently matched
35
+ -- nothing would make OP-4 and OP-5 pass with an empty input set. OP-6 pins the closure's
36
+ -- membership positively before the other two are believed.
37
+ create extension if not exists pgtap with schema extensions;
38
+ set search_path = extensions, public, agent_board;
39
+
40
+ begin;
41
+
42
+ select plan(22);
43
+
44
+ -- ===========================================================================
45
+ -- The class, derived once. Membership rule:
46
+ -- a function is a member iff its CALL CLOSURE reaches an INSERT INTO agent_board.task_events
47
+ -- ===========================================================================
48
+ create temp table _op_direct on commit drop as
49
+ select p.oid, p.proname
50
+ from pg_proc p
51
+ join pg_namespace n on n.oid = p.pronamespace
52
+ where n.nspname = 'agent_board'
53
+ and agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+(agent_board\.)?task_events';
54
+
55
+ create temp table _op_class on commit drop as
56
+ with recursive walk as (
57
+ select oid, proname, 0 as depth from _op_direct
58
+ union
59
+ select p.oid, p.proname, w.depth + 1
60
+ from pg_proc p
61
+ join pg_namespace n on n.oid = p.pronamespace
62
+ join walk w
63
+ on agent_board_test.stripped_body(p.oid)
64
+ ~* ('(^|[^a-z0-9_.])(agent_board\.)?' || w.proname || '\s*\(')
65
+ where n.nspname = 'agent_board' and p.oid <> w.oid and w.depth < 5
66
+ )
67
+ select oid, proname, min(depth) as depth from walk group by oid, proname;
68
+
69
+ -- ---------------------------------------------------------------------------
70
+ -- OP-1/OP-2/OP-3: the column itself.
71
+ -- ---------------------------------------------------------------------------
72
+ select is(
73
+ (select attnotnull from pg_attribute
74
+ where attrelid = 'agent_board.task_events'::regclass and attname = 'origin_posture'),
75
+ true,
76
+ 'OP-1: task_events.origin_posture is NOT NULL -- a new direct-INSERT writer cannot insert at all');
77
+
78
+ -- A column DEFAULT would silently satisfy a new direct-INSERT writer with a plausible-looking
79
+ -- label, re-opening exactly the hole this migration closes. The absence is load-bearing.
80
+ select is(
81
+ (select pg_get_expr(d.adbin, d.adrelid)
82
+ from pg_attribute a left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
83
+ where a.attrelid = 'agent_board.task_events'::regclass and a.attname = 'origin_posture'),
84
+ null,
85
+ 'OP-2: origin_posture has NO column default -- a default would relabel a forgetful writer instead of stopping it');
86
+
87
+ select is(
88
+ (select count(*)::int from (
89
+ select unnest(array['attested:evidence-row','unattested:signed-metadata-rpc','unattested:unsigned-deliberate-rpc','unattested:direct-rpc','unattested:preexisting']) v
90
+ ) want
91
+ where not exists (
92
+ select 1 from pg_constraint c
93
+ where c.conrelid = 'agent_board.task_events'::regclass
94
+ and c.conname = 'task_events_origin_posture_check'
95
+ and pg_get_constraintdef(c.oid) like '%''' || want.v || '''%')),
96
+ 0,
97
+ 'OP-3: every vocabulary value is admitted by task_events_origin_posture_check');
98
+
99
+ -- ---------------------------------------------------------------------------
100
+ -- OP-4/OP-5: the class assertions. Catalog-derived, and they NAME the offender.
101
+ -- ---------------------------------------------------------------------------
102
+ -- Compliant iff EVERY _append_task_event call site passes a literal posture as its first
103
+ -- argument. Counting both shapes and comparing catches a body with three compliant calls and
104
+ -- one bare one -- which a bare `does it match at all` predicate would report as clean.
105
+ select is(
106
+ (select coalesce(string_agg(c.proname, ', ' order by c.proname), '')
107
+ from _op_class c
108
+ where (select count(*) from regexp_matches(
109
+ agent_board_test.stripped_body(c.oid, true), '_append_task_event\s*\(', 'g')) <> 0
110
+ and (select count(*) from regexp_matches(
111
+ agent_board_test.stripped_body(c.oid, true), '_append_task_event\s*\(', 'g'))
112
+ <> (select count(*) from regexp_matches(
113
+ agent_board_test.stripped_body(c.oid, true), '_append_task_event\s*\(\s*''', 'g'))),
114
+ '',
115
+ 'OP-4 CLASS: every funnel caller passes an explicit literal posture -- a caller that does not is NAMED here');
116
+
117
+ -- The two INSERT sites must name the column. This is the half OP-4 cannot see: a writer that
118
+ -- inserts directly never calls the funnel at all, which is precisely how create_task escaped
119
+ -- every prior enumeration of this class.
120
+ select is(
121
+ (select coalesce(string_agg(d.proname, ', ' order by d.proname), '')
122
+ from _op_direct d
123
+ where agent_board_test.stripped_body(d.oid)
124
+ !~* 'insert\s+into\s+(agent_board\.)?task_events\s*\([^)]*origin_posture'),
125
+ '',
126
+ 'OP-5 CLASS: every DIRECT task_events INSERT names origin_posture -- one that does not is NAMED here');
127
+
128
+ -- ---------------------------------------------------------------------------
129
+ -- OP-6: the anti-vacuity guard. OP-4 and OP-5 are only worth their green if the derivation
130
+ -- they read actually found the writers. Asserted POSITIVELY (the members are present), never
131
+ -- by "no offenders were found" -- an empty class also produces no offenders.
132
+ -- ---------------------------------------------------------------------------
133
+ select is(
134
+ (select coalesce(string_agg(c.proname, ', ' order by c.proname), '')
135
+ from _op_class c
136
+ where c.proname in ('_append_task_event', 'create_task', 'acquire_claim', 'raise_conflict',
137
+ 'signed_append_task_event', 'signed_create_tasking_delivery')),
138
+ '_append_task_event, acquire_claim, create_task, raise_conflict, signed_append_task_event, signed_create_tasking_delivery',
139
+ 'OP-6: the closure really did find the writers, including the two the issue never named');
140
+
141
+ -- ---------------------------------------------------------------------------
142
+ -- OP-7/OP-8: reachable surface. The funnel moved BEHIND a function no role can invoke; the
143
+ -- public entry point kept the ACL it had before, which is the property CREATE OR REPLACE
144
+ -- buys and DROP FUNCTION would have destroyed.
145
+ -- ---------------------------------------------------------------------------
146
+ select is(
147
+ (select coalesce(string_agg(r, ', ' order by r), '')
148
+ from unnest(array['anon','authenticated','service_role']) r
149
+ where has_function_privilege(r,
150
+ 'agent_board._append_task_event(text,uuid,uuid,text,jsonb,text,uuid,timestamptz)', 'execute')),
151
+ '',
152
+ 'OP-7: no non-owner role can execute _append_task_event -- every caller is SECURITY DEFINER');
153
+
154
+ select is(
155
+ (select coalesce(string_agg(r, ', ' order by r), '')
156
+ from unnest(array['authenticated','service_role']) r
157
+ where has_function_privilege(r,
158
+ 'agent_board.append_task_event(uuid,uuid,text,jsonb,text,uuid,timestamptz)', 'execute')),
159
+ 'authenticated, service_role',
160
+ 'OP-8: the public entry point kept its ACL through CREATE OR REPLACE');
161
+
162
+ -- ---------------------------------------------------------------------------
163
+ -- OP-9: the two columns stay SEPARATE. Folding a stamped posture into verification_status
164
+ -- would let it satisfy a gate written against the evidence vocabulary.
165
+ -- ---------------------------------------------------------------------------
166
+ select is(
167
+ (select count(*)::int from pg_attribute
168
+ where attrelid = 'agent_board.v_task_tree'::regclass
169
+ and attname in ('origin_posture','verification_status') and attnum > 0),
170
+ 2,
171
+ 'OP-9: v_task_tree exposes origin_posture and verification_status as two distinct columns');
172
+
173
+ -- ===========================================================================
174
+ -- Behaviour. A file that only read the catalog would pass against writers that no longer
175
+ -- work. Every RPC runs inside the fixture block, never inside an assertion's value
176
+ -- expression -- a raise there would abort the file with no `not ok` (#113).
177
+ -- ===========================================================================
178
+ create temp table _op_fx (actor uuid, actor_user uuid, thread uuid, task uuid, board uuid) on commit drop;
179
+
180
+ do $$
181
+ declare
182
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
183
+ v_creator uuid := gen_random_uuid();
184
+ v_actor uuid := gen_random_uuid();
185
+ v_actor_user uuid;
186
+ v_board uuid; v_thread uuid := gen_random_uuid(); v_task uuid;
187
+ begin
188
+ v_actor_user := public.ensure_github_user('op-actor-' || v_suffix, 'op-actor-' || v_suffix || '@example.test',
189
+ 'op-actor-' || v_suffix, 'OP Actor');
190
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
191
+ values (v_creator, 'agent', 'op-creator-' || v_suffix, 'OP Creator', 'active', null, null),
192
+ (v_actor, 'agent', 'op-actor-' || v_suffix, 'OP Actor', 'active', v_actor_user, v_actor_user);
193
+
194
+ select board_id into v_board
195
+ from agent_board.create_board(v_creator, 'op-board-' || v_suffix, 'OP Board', 'origin posture', '{}'::jsonb, 'private');
196
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
197
+ values (v_board, v_actor, 'joined', v_creator, 'member');
198
+
199
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
200
+ values (v_thread, 'coordination', v_actor, 'OP thread');
201
+
202
+ perform set_config('request.jwt.claim.role', 'authenticated', true);
203
+ perform set_config('request.jwt.claim.sub', v_actor_user::text, true);
204
+ perform agent_board.post_board_message(v_thread, v_actor, v_board, 'seed', 'seed body');
205
+
206
+ -- One write per posture the live writers can produce.
207
+ select task_id into v_task from agent_board.create_task(v_thread, v_actor, 'op task', 'd');
208
+ perform agent_board.append_task_event(v_task, v_actor, 'commented'); -- direct-rpc
209
+ perform agent_board.raise_conflict(v_thread, v_actor, 'op conflict', 'other', 'medium', v_task);
210
+ perform agent_board.acquire_claim(v_actor, 'task', 'op-' || v_suffix, now() + interval '1 hour', v_task);
211
+
212
+ insert into _op_fx values (v_actor, v_actor_user, v_thread, v_task, v_board);
213
+ end
214
+ $$;
215
+
216
+ select is(
217
+ (select origin_posture from agent_board.task_events
218
+ where task_id = (select task from _op_fx) and event_sequence = 1),
219
+ 'unattested:signed-metadata-rpc',
220
+ 'OP-10: create_task''s own direct insert stamps signed-metadata');
221
+
222
+ select is(
223
+ (select origin_posture from agent_board.task_events
224
+ where task_id = (select task from _op_fx) and event_type = 'commented'),
225
+ 'unattested:direct-rpc',
226
+ 'OP-11: the public append_task_event stamps direct-rpc -- the caller declared nothing');
227
+
228
+ select is(
229
+ (select origin_posture from agent_board.task_events
230
+ where task_id = (select task from _op_fx) and event_type = 'conflict_raised'),
231
+ 'unattested:unsigned-deliberate-rpc',
232
+ 'OP-12: raise_conflict stamps unsigned-deliberate, its #261 step 2 adjudication');
233
+
234
+ select is(
235
+ (select origin_posture from agent_board.task_events
236
+ where task_id = (select task from _op_fx) and event_type = 'claimed'),
237
+ 'unattested:signed-metadata-rpc',
238
+ 'OP-13: acquire_claim stamps signed-metadata -- a signed parent whose derived event is still not itself signed');
239
+
240
+ -- Scoped to this fixture's task: an agent-wide or table-wide count is polluted by earlier
241
+ -- fixtures and would fail looking like a real defect.
242
+ select is(
243
+ (select count(*)::int from agent_board.task_events
244
+ where task_id = (select task from _op_fx) and origin_posture is null),
245
+ 0,
246
+ 'OP-14: no row this run wrote carries a NULL posture');
247
+
248
+ -- M6: an 'attested:evidence-row' stamp is a CLAIM that evidence exists. If it can be stamped without
249
+ -- the row, it is decoration. Table-wide on purpose -- this one is a whole-database invariant,
250
+ -- and a false claim anywhere is a defect regardless of which fixture wrote it.
251
+ select is(
252
+ (select count(*)::int from agent_board.task_events te
253
+ where te.origin_posture = 'attested:evidence-row'
254
+ and not exists (select 1 from agent_board.signature_evidence se
255
+ where se.source_kind = 'task_event' and se.source_task_event_id = te.id)),
256
+ 0,
257
+ 'OP-15: every row stamped evidence-row really has a signature_evidence row -- the stamp is not decoration');
258
+
259
+ -- M8, pinned BEHAVIOURALLY rather than by reading the view's source. OP-9 counts two columns
260
+ -- and a fold keeps both names, so OP-9 alone would stay green through the exact mutation it
261
+ -- was supposed to catch -- found by running the battery against it, not by reading it.
262
+ -- This task has a posture and NO signature_evidence row, so if the two are ever folded,
263
+ -- verification_status starts answering 'unattested:signed-metadata-rpc' and this fails.
264
+ select is(
265
+ (select verification_status || '/' || origin_posture
266
+ from agent_board.v_task_tree where task_id = (select task from _op_fx)),
267
+ null,
268
+ 'OP-16: a stamped posture does NOT leak into verification_status -- it stays NULL with no evidence row');
269
+
270
+ -- M5. A default on p_origin_posture would let an internal caller omit it and inherit a
271
+ -- plausible label. PostgreSQL forbids a defaulted parameter before non-defaulted ones, so
272
+ -- putting the posture FIRST is what makes "no default" structural rather than a convention
273
+ -- -- the database rejects the mutation instead of a test catching it afterwards. This pins
274
+ -- the ORDER, which is the thing actually doing the work.
275
+ select is(
276
+ (select p.proargnames[1] from pg_proc p join pg_namespace n on n.oid = p.pronamespace
277
+ where n.nspname = 'agent_board' and p.proname = '_append_task_event'),
278
+ 'p_origin_posture',
279
+ 'OP-17: p_origin_posture is the FIRST parameter, so PostgreSQL itself forbids giving it a default');
280
+
281
+ -- ---------------------------------------------------------------------------
282
+ -- OP-18: THE FLOOR THE CLASS CLAIM ACTUALLY RESTS ON.
283
+ --
284
+ -- OP-4/OP-5 walk FUNCTION BODIES, and a body walk cannot be complete in the presence of
285
+ -- dynamic SQL -- proven in this very schema: mark_retention_metadata issues an UPDATE against
286
+ -- task_events through a format() template, and an earlier version of this work asserted in
287
+ -- writing that it "issues no DML", because a text scan cannot see a template.
288
+ --
289
+ -- This assertion is what makes the enumeration sound anyway. If no role can INSERT into
290
+ -- task_events, then every row arrives through a SECURITY DEFINER function owned by the schema
291
+ -- owner -- which is precisely the set the closure walks. Bound the claim with
292
+ -- has_table_privilege, not with a longer regex. Grant INSERT to any role and the class becomes
293
+ -- unbounded in a way no body scan would report, so this fails FIRST.
294
+ -- ---------------------------------------------------------------------------
295
+ select is(
296
+ (select coalesce(string_agg(r, ', ' order by r), '')
297
+ from unnest(array['anon','authenticated','service_role']) r
298
+ where has_table_privilege(r, 'agent_board.task_events', 'insert')),
299
+ '',
300
+ 'OP-18: no role holds INSERT on task_events, so every row comes through a definer function -- the bound the body walk cannot supply itself');
301
+
302
+ -- ---------------------------------------------------------------------------
303
+ -- OP-19: the stamp is written once and never rewritten. A posture that can be UPDATEd after
304
+ -- the fact is not evidence about the writer, it is a mutable label -- and rewriting a row via
305
+ -- definer privilege to "fix" it is a move this repo has explicitly ruled out.
306
+ --
307
+ -- Literals are KEPT here for a second reason beyond OP-4's: a format() template is a string
308
+ -- LITERAL in the body, so keeping literals is what lets this see dynamic SQL at all. It sees a
309
+ -- template; it would NOT see SQL concatenated from fragments across variables. That residual is
310
+ -- stated rather than papered over, and OP-18 is what covers it.
311
+ -- ---------------------------------------------------------------------------
312
+ select is(
313
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
314
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
315
+ where n.nspname = 'agent_board'
316
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
317
+ and agent_board_test.stripped_body(p.oid, true) ~* 'set\s[^;]*\morigin_posture\s*='),
318
+ '',
319
+ 'OP-19: nothing UPDATEs origin_posture -- including through a dynamic-SQL template, which keeping literals makes visible');
320
+
321
+ -- ---------------------------------------------------------------------------
322
+ -- OP-20: THE HOLE THREE REVIEWERS FOUND, NOW CLOSED.
323
+ --
324
+ -- OP-4 only inspects bodies that call _append_task_event. A thirteenth in-schema writer that
325
+ -- called the PUBLIC append_task_event instead would enter the class, compile cleanly, be
326
+ -- stamped 'unattested:direct-rpc', and be named by NOTHING. The header of this file claimed a
327
+ -- thirteenth writer "fails them AND NAMES ITSELF"; against that shape it did not, and the
328
+ -- sentence has been corrected rather than defended.
329
+ --
330
+ -- The public entry point exists for callers OUTSIDE the schema. Zero in-schema functions call
331
+ -- it, so requiring that is not a restriction on anything that exists -- it is the assertion
332
+ -- that makes "names itself" true for the one shape where it was false.
333
+ -- ---------------------------------------------------------------------------
334
+ select is(
335
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
336
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
337
+ where n.nspname = 'agent_board'
338
+ and p.proname <> 'append_task_event'
339
+ and agent_board_test.stripped_body(p.oid, true)
340
+ ~ '(^|[^a-z0-9_.])agent_board\.append_task_event\s*\('),
341
+ '',
342
+ 'OP-20: no in-schema function calls the PUBLIC append_task_event -- one that did would be stamped direct-rpc and named by nothing else');
343
+
344
+ -- ---------------------------------------------------------------------------
345
+ -- OP-21: the premise under OP-7. Granting _append_task_event to no role is only safe because
346
+ -- a SECURITY DEFINER caller runs as ITS OWN owner -- so if a caller were owned by a different
347
+ -- role, it would lose EXECUTE and fail at runtime rather than being protected. OP-7 asserts
348
+ -- ordinary roles lack access; it says nothing about owners. Raised by a reviewer as an
349
+ -- unverified premise, and it was: measured true (one owner across the schema), now pinned.
350
+ -- ---------------------------------------------------------------------------
351
+ select is(
352
+ (select count(distinct p.proowner)::int
353
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
354
+ where n.nspname = 'agent_board'),
355
+ 1,
356
+ 'OP-21: every agent_board function shares one owner, which is what makes an ungranted internal funnel reachable by its callers');
357
+
358
+ -- ---------------------------------------------------------------------------
359
+ -- OP-22: OP-15 WAS VACUOUS AND A REVIEWER SAID SO.
360
+ --
361
+ -- OP-15 counts rows stamped attested:% that have no evidence row. In an unmutated run the
362
+ -- fixture creates NO attested row at all, so it was 0 = 0 and green for the wrong reason.
363
+ -- M6 reds it, which is why the property is not unpinned -- but a green that only means "the
364
+ -- set was empty" is the exact shape rule 3 forbids.
365
+ --
366
+ -- This constructs the POSITIVE case: an attested row WITH its evidence, so the invariant is
367
+ -- exercised in the direction that can actually distinguish a correct implementation from an
368
+ -- empty table. Written directly rather than through signed_append_task_event because that
369
+ -- wrapper needs a gateway decision this file has no business forging -- so it is labelled as
370
+ -- a constructed pair, not as a claim about what any writer did.
371
+ -- ---------------------------------------------------------------------------
372
+ do $$
373
+ declare v_ev uuid; v_task uuid; v_actor uuid; v_thread uuid;
374
+ begin
375
+ select task, actor, thread into v_task, v_actor, v_thread from _op_fx;
376
+ insert into agent_board.task_events(task_id, thread_id, actor_agent_id, event_sequence, event_type, payload, origin_posture)
377
+ values (v_task, v_thread, v_actor, 99, 'commented', '{}'::jsonb, 'attested:evidence-row')
378
+ returning id into v_ev;
379
+ insert into agent_board.signature_evidence(agent_id, source_kind, source_task_event_id, operation,
380
+ payload_digest, verification_status)
381
+ values (v_actor, 'task_event', v_ev, 'append_task_event', 'sha256:op22-constructed', 'verified');
382
+ end $$;
383
+
384
+ select is(
385
+ (select count(*)::int from agent_board.task_events te
386
+ where te.origin_posture like 'attested:%'
387
+ and exists (select 1 from agent_board.signature_evidence se
388
+ where se.source_kind = 'task_event' and se.source_task_event_id = te.id)),
389
+ 1,
390
+ 'OP-22: OP-15 is exercised against a REAL attested row, not an empty set -- the positive direction of the same invariant');
391
+
392
+ select * from finish();
393
+ rollback;
@@ -80,6 +80,10 @@ select distinct h.fn_name || ': ' ||
80
80
  select is(
81
81
  (select array_agg(m order by m) from _tt_ord),
82
82
  array[
83
+ -- #288: the funnel body moved here from append_task_event, carrying the #281 R1 order
84
+ -- with it. The public append_task_event still appears below, reaching the same two
85
+ -- acquisitions `via callee` -- so this row is a NEW member, not a relocated one.
86
+ '_append_task_event: threads(key share) then tasks(update)',
83
87
  '_upsert_delivery_recipient: threads(update) then tasks(key share)',
84
88
  'acquire_claim: same statement: threads(key share) + tasks(update)',
85
89
  'add_artifact: same statement: threads(key share) + tasks(update)',
@@ -112,8 +116,8 @@ select is(
112
116
  (select count(*)::text || ' functions acquire both; '
113
117
  || count(*) filter (where tasks_first)::text || ' take tasks before threads'
114
118
  from _tt_ord),
115
- '20 functions acquire both; 0 take tasks before threads',
116
- 'TT-2: 20 functions acquire both relations and NONE takes tasks before threads');
119
+ '21 functions acquire both; 0 take tasks before threads',
120
+ 'TT-2: 21 functions acquire both relations and NONE takes tasks before threads');
117
121
 
118
122
  -- ===========================================================================
119
123
  -- TT-3: append_task_event's acquisitions on the two relations, in path order, with mode
@@ -125,14 +129,14 @@ select is(
125
129
  (select string_agg(e.rel_name || ' ' || e.lock_mode || ' via ' || e.via, ' -> '
126
130
  order by e.path, e.rel_name, e.lock_mode, e.via)
127
131
  from _tt_le e
128
- where e.fn_name = 'append_task_event'
132
+ where e.fn_name = '_append_task_event'
129
133
  and e.rel_name in ('agent_board.tasks', 'agent_board.threads')),
130
134
  'agent_board.threads key share via textual'
131
135
  || ' -> agent_board.tasks update via textual'
132
136
  || ' -> agent_board.tasks key share via fk-insert:task_events_task_id_fkey'
133
137
  || ' -> agent_board.threads key share via fk-insert:task_events_thread_id_fkey'
134
138
  || ' -> agent_board.tasks update via write-update',
135
- 'TT-3: append_task_event takes threads(KEY SHARE, explicit) FIRST, then the tasks FOR UPDATE');
139
+ 'TT-3: _append_task_event takes threads(KEY SHARE, explicit) FIRST, then the tasks FOR UPDATE');
136
140
 
137
141
  -- ===========================================================================
138
142
  -- TT-4: THE PREMISE THE PRE-READ RESTS ON. append_task_event now reads tasks.thread_id
@@ -179,11 +183,11 @@ select is(
179
183
  select is(
180
184
  (select coalesce(string_agg(distinct e.lock_mode, ', ' order by e.lock_mode), '<none>')
181
185
  from _tt_le e
182
- where e.fn_name = 'append_task_event'
186
+ where e.fn_name = '_append_task_event'
183
187
  and e.rel_name = 'agent_board.tasks'
184
188
  and e.via = 'textual'),
185
189
  'update',
186
- 'TT-6: append_task_event still takes an EXPLICIT FOR UPDATE on tasks -- the sequence guard is intact');
190
+ 'TT-6: _append_task_event still takes an EXPLICIT FOR UPDATE on tasks -- the sequence guard is intact');
187
191
 
188
192
  -- ===========================================================================
189
193
  -- TT-7: the second half of the fix, on its own. `_upsert_delivery_recipient` has no
@@ -29,6 +29,10 @@
29
29
  "signature": "_agent_principal_active(p_agent_id uuid)",
30
30
  "declares_p_metadata": false
31
31
  },
32
+ {
33
+ "signature": "_append_task_event(p_origin_posture text, p_task_id uuid, p_actor_agent_id uuid, p_event_type text, p_payload jsonb, p_status text, p_assigned_agent_id uuid, p_completed_at timestamp with time zone)",
34
+ "declares_p_metadata": false
35
+ },
32
36
  {
33
37
  "signature": "_assert_actor_not_fleet_revoked(p_actor_agent_id uuid, p_operation text)",
34
38
  "declares_p_metadata": false