@consiliency/agent-board-schema 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +75 -1
  2. package/fixtures/already-applied.json +12 -0
  3. package/migrations/20260905000100_agent_board_consent_gating_reconcile.sql +151 -0
  4. package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
  5. package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
  6. package/package.json +1 -1
  7. package/schema-manifest.json +30 -3
  8. package/src/check.js +23 -0
  9. package/src/index.js +20 -0
  10. package/src/plan.js +79 -5
  11. package/src/tooling.test.mjs +237 -4
  12. package/tests/agent_board_atomic_fleet_onboarding.test.sql +124 -112
  13. package/tests/agent_board_claimkey.test.sql +25 -3
  14. package/tests/agent_board_consent_emission.test.sql +17 -13
  15. package/tests/agent_board_consent_gating.test.sql +55 -11
  16. package/tests/agent_board_delegation_policies.test.sql +3 -1
  17. package/tests/agent_board_enroll_resumption.test.sql +412 -0
  18. package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
  19. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
  20. package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
  21. package/tests/agent_board_mint_harden.test.sql +39 -4
  22. package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
  23. package/tests/agent_board_rejectsem.test.sql +1 -1
  24. package/tests/agent_board_rolereset.test.sql +23 -2
  25. package/tests/agent_board_rowid.test.sql +24 -13
  26. package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
  27. package/tests/agent_board_signed_authority_provenance.test.sql +454 -0
  28. package/tests/agent_board_source_strip_helper.test.sql +285 -0
  29. package/tests/agent_board_wake_recovery.test.sql +58 -40
  30. package/tests/catalog/p_metadata_parameters.json +689 -0
  31. package/tests/fixtures/test-prereqs.sql +313 -0
  32. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
@@ -115,3 +115,316 @@ $$;
115
115
  REVOKE ALL ON FUNCTION public.test_pubkey(text) FROM PUBLIC;
116
116
  REVOKE ALL ON FUNCTION public.test_key_fingerprint(text) FROM PUBLIC;
117
117
  GRANT EXECUTE ON FUNCTION public.test_pubkey(text), public.test_key_fingerprint(text) TO service_role;
118
+
119
+ -- ===========================================================================
120
+ -- Shared SQL source-text strip for pgTAP source assertions
121
+ -- (message-board#196, message-board#199).
122
+ --
123
+ -- WHY THIS EXISTS. A pgTAP assertion that locates a marker in a function body
124
+ -- with `strpos(...)` and compares positions, or that claims a marker is ABSENT
125
+ -- with `!~` / `not like`, reads AUTHOR-CONTROLLED text. Prose that merely
126
+ -- MENTIONS the marker satisfies the assertion while the code does the opposite,
127
+ -- so the guard reports "ordered" / "absent" when it means "my pattern matched
128
+ -- something that is not code". Every mode below was DEMONSTRATED, not reasoned
129
+ -- about; each one left a hardened file fully green with a lock genuinely moved.
130
+ --
131
+ -- ENUMERATED ESCAPE HATCHES, and which of them a regex can close:
132
+ -- 1. a `--` comment naming the marker -- regex can
133
+ -- 2. a block comment whose body ends in `*` (`/* … **/`) -- regex can
134
+ -- 3. a string literal naming the marker, incl. a dollar-quoted
135
+ -- one; a naive two-pass scanner also DESYNCS on an odd
136
+ -- apostrophe count (`mark_retention_metadata`'s $retention$
137
+ -- block survives one today only because its 64 apostrophes
138
+ -- happen to be an even number) -- regex can
139
+ -- 4. a NESTED block comment (message-board#199). Postgres block
140
+ -- comments NEST; `/* note /* x */ <code> */` hides <code>
141
+ -- from any regex that stops at the first `*/` -- REGEX CANNOT
142
+ -- 5. an apostrophe inside a "quoted identifier"; a regex whose
143
+ -- literal alternative does not know about `"…"` desyncs on
144
+ -- it exactly as (3) desyncs -- regex cannot
145
+ --
146
+ -- (4) is why this is a character scanner and not a cleverer regular expression.
147
+ -- Postgres' regex engine has no recursion, so no regular expression can match a
148
+ -- nested block comment: the language is not regular. Rejecting nested comments
149
+ -- instead of handling them was considered (message-board#199 suggests it) and
150
+ -- not taken -- a scanner that handles nesting is exact, is unit-testable
151
+ -- (agent_board_source_strip_helper.test.sql), and does not have to guess which
152
+ -- constructs are "legitimate".
153
+ --
154
+ -- ONE PASS, NOT TWO. Comments-first eats a literal containing `--`;
155
+ -- literals-first eats a comment containing an apostrophe. This scanner consumes
156
+ -- whichever token OPENS first, which is the correct tokenization and the only
157
+ -- one that cannot desync.
158
+ --
159
+ -- FAIL CLOSED. An unterminated comment, literal, dollar quote or quoted
160
+ -- identifier raises rather than returning a truncated body, so the failure is
161
+ -- loud instead of a silently short string that every `!~` passes.
162
+ --
163
+ -- MODES. `p_keep_literals => false` (the default) is the safe one and is what a
164
+ -- positional or negative assertion should use. `p_keep_literals => true` keeps
165
+ -- string literals -- required ONLY where the MARKER ITSELF IS A LITERAL (e.g.
166
+ -- an assertion locating a `raise exception '<message>'`), because stripping the
167
+ -- literals there would make the assertion vacuously true forever. Literals are
168
+ -- still CONSUMED in that mode, so a `--` or `/*` inside one cannot be mistaken
169
+ -- for a comment opener. This choice is NOT machine-checkable -- the lint in
170
+ -- scripts/run-agent-board-schema-tests.sh cannot tell a message anchor from a
171
+ -- code anchor -- so every `p_keep_literals => true` call site carries an inline
172
+ -- justification.
173
+ -- ===========================================================================
174
+ CREATE SCHEMA IF NOT EXISTS agent_board_test;
175
+
176
+ CREATE OR REPLACE FUNCTION agent_board_test.strip_sql(
177
+ p_text text,
178
+ p_keep_literals boolean DEFAULT false
179
+ ) RETURNS text
180
+ LANGUAGE plpgsql
181
+ IMMUTABLE
182
+ SET search_path = pg_catalog
183
+ AS $strip$
184
+ declare
185
+ c text[]; -- p_text as characters; array indexing is O(1),
186
+ -- substr()-per-character on a multibyte text is not
187
+ n int;
188
+ i int := 1;
189
+ span int := 1; -- start of the current KEPT run
190
+ parts text[] := '{}';
191
+ depth int;
192
+ j int;
193
+ tag text;
194
+ close_at int;
195
+ is_estring boolean;
196
+ begin
197
+ if p_text is null then
198
+ return null;
199
+ end if;
200
+
201
+ c := regexp_split_to_array(p_text, '');
202
+ n := coalesce(array_length(c, 1), 0);
203
+ if n = 0 then
204
+ return p_text;
205
+ end if;
206
+
207
+ while i <= n loop
208
+
209
+ -- (1) line comment ------------------------------------------------------
210
+ if c[i] = '-' and i < n and c[i + 1] = '-' then
211
+ parts := parts || substr(p_text, span, i - span);
212
+ while i <= n and c[i] <> chr(10) loop
213
+ i := i + 1;
214
+ end loop;
215
+ span := i; -- the newline itself is code-relevant whitespace; keep it
216
+
217
+ -- (2) block comment, WITH NESTING (message-board#199) --------------------
218
+ elsif c[i] = '/' and i < n and c[i + 1] = '*' then
219
+ parts := parts || substr(p_text, span, i - span);
220
+ depth := 1;
221
+ i := i + 2;
222
+ while i <= n and depth > 0 loop
223
+ if c[i] = '/' and i < n and c[i + 1] = '*' then
224
+ depth := depth + 1; i := i + 2;
225
+ elsif c[i] = '*' and i < n and c[i + 1] = '/' then
226
+ depth := depth - 1; i := i + 2;
227
+ else
228
+ i := i + 1;
229
+ end if;
230
+ end loop;
231
+ if depth > 0 then
232
+ raise exception
233
+ 'agent_board_test.strip_sql: unterminated block comment (depth %) -- refusing to return a truncated body',
234
+ depth;
235
+ end if;
236
+ span := i;
237
+
238
+ -- (3) quoted identifier: CODE, kept verbatim, but CONSUMED so that an
239
+ -- apostrophe inside it cannot desync the literal scanner ------------
240
+ elsif c[i] = '"' then
241
+ i := i + 1;
242
+ loop
243
+ if i > n then
244
+ raise exception
245
+ 'agent_board_test.strip_sql: unterminated quoted identifier -- refusing to return a truncated body';
246
+ end if;
247
+ if c[i] = '"' then
248
+ if i < n and c[i + 1] = '"' then
249
+ i := i + 2; -- doubled quote inside the identifier
250
+ else
251
+ i := i + 1; exit;
252
+ end if;
253
+ else
254
+ i := i + 1;
255
+ end if;
256
+ end loop;
257
+
258
+ -- (4) single-quoted string literal, including the E'' escape form -------
259
+ elsif c[i] = '''' then
260
+ -- E'…' only when the E is a standalone prefix, not the tail of an
261
+ -- identifier (`v_note'…'` is not an escape string).
262
+ is_estring := i > 1
263
+ and upper(c[i - 1]) = 'E'
264
+ and (i = 2 or c[i - 2] !~ '[A-Za-z0-9_$]|[^\x00-\x7F]');
265
+ if not p_keep_literals then
266
+ -- an E prefix is part of the literal, not of the surrounding code
267
+ parts := parts || substr(p_text, span, (case when is_estring then i - 1 else i end) - span);
268
+ end if;
269
+ i := i + 1;
270
+ loop
271
+ if i > n then
272
+ raise exception
273
+ 'agent_board_test.strip_sql: unterminated string literal -- refusing to return a truncated body';
274
+ end if;
275
+ if is_estring and c[i] = '\' then
276
+ i := i + 2; -- backslash escapes one char
277
+ elsif c[i] = '''' then
278
+ if i < n and c[i + 1] = '''' then
279
+ i := i + 2; -- doubled quote inside the literal
280
+ else
281
+ i := i + 1; exit;
282
+ end if;
283
+ else
284
+ i := i + 1;
285
+ end if;
286
+ end loop;
287
+ if not p_keep_literals then
288
+ span := i;
289
+ end if;
290
+
291
+ -- (5) dollar-quoted string -----------------------------------------------
292
+ --
293
+ -- TWO ways this is NOT a dollar quote, and both swallow live code if missed:
294
+ -- a. the tag cannot start with a digit -- `$1$` is the parameter $1
295
+ -- followed by $, not an opener;
296
+ -- b. PostgreSQL permits `$` INSIDE an unquoted identifier, and the lexer
297
+ -- consumes it greedily, so `foo$tag$danger$tag$` is ONE identifier.
298
+ -- A scanner that ignores the preceding character reads `$tag$` as an
299
+ -- opener and deletes `$tag$danger$tag$` from live code. Verified against
300
+ -- the server, not assumed: `do $do$ declare foo$tag$danger$tag$ int := 7;
301
+ -- begin raise notice '%', foo$tag$danger$tag$; end $do$;` runs and prints 7.
302
+ -- c. the identifier grammar is NOT ASCII. PostgreSQL accepts any letter
303
+ -- the server encoding recognises in an unquoted identifier AND in a
304
+ -- dollar-quote tag, so `[A-Za-z0-9_]` (or an \x80-\xff byte range, which
305
+ -- is wrong for multi-byte UTF-8) misreads both directions: `$Ω$ … $Ω$`
306
+ -- was left UNSTRIPPED -- a marker could hide in it -- and
307
+ -- `Ω$tag$danger$tag$` was eaten as if it opened a quote. Both verified
308
+ -- against the server: `do $do$ begin raise notice '%', $Ω$ hello $Ω$; end $do$;`
309
+ -- and `do $do$ declare Ω$tag$danger$tag$ int := 7; ... end $do$;` both run.
310
+ -- The class used here is LOCALE-INDEPENDENT on purpose. `[[:alpha:]]` is
311
+ -- not: under `C` collation it does NOT match `Ω` (measured:
312
+ -- `('Ω' collate "C") ~ '[[:alpha:]]'` is FALSE while the default collation
313
+ -- says true), so a locale-sensitive class makes this scanner -- and the
314
+ -- whole 49-site claim that rests on it -- pass or fail depending on the
315
+ -- runner's collation, and diverge from the JS mirror in scripts/.
316
+ -- For UTF-8 PostgreSQL treats ANY non-ASCII character as an identifier
317
+ -- character, so `[A-Za-z_]|[^\x00-\x7F]` is both exact and collation-free.
318
+ elsif c[i] = '$'
319
+ and (i = 1 or c[i - 1] !~ '[A-Za-z0-9_$]|[^\x00-\x7F]') then
320
+ j := i + 1;
321
+ if j <= n and c[j] ~ '[A-Za-z_]|[^\x00-\x7F]' then
322
+ while j <= n and c[j] ~ '[A-Za-z0-9_]|[^\x00-\x7F]' loop
323
+ j := j + 1;
324
+ end loop;
325
+ end if;
326
+ if j <= n and c[j] = '$' then
327
+ tag := substr(p_text, i, j - i + 1);
328
+ close_at := position(tag in substr(p_text, j + 1));
329
+ if close_at = 0 then
330
+ raise exception
331
+ 'agent_board_test.strip_sql: unterminated dollar-quoted string % -- refusing to return a truncated body',
332
+ tag;
333
+ end if;
334
+ if not p_keep_literals then
335
+ parts := parts || substr(p_text, span, i - span);
336
+ end if;
337
+ i := j + close_at + length(tag); -- first char after the closing tag
338
+ if not p_keep_literals then
339
+ span := i;
340
+ end if;
341
+ else
342
+ i := i + 1; -- a bare `$` / `$1`: ordinary code
343
+ end if;
344
+
345
+ else
346
+ i := i + 1;
347
+ end if;
348
+ end loop;
349
+
350
+ parts := parts || substr(p_text, span);
351
+ -- Each stripped token collapses to a single space so token boundaries
352
+ -- survive: `a/*x*/b` must not become the identifier `ab`.
353
+ return array_to_string(parts, ' ');
354
+ end;
355
+ $strip$;
356
+
357
+ COMMENT ON FUNCTION agent_board_test.strip_sql(text, boolean) IS
358
+ 'One-pass character scanner that removes line comments, NESTED block comments and (unless p_keep_literals) string literals from SQL/PL-pgSQL source text. Test-only; see message-board#196 / #199.';
359
+
360
+ -- The stripped BODY of a function. This is what a positional or negative
361
+ -- assertion should read.
362
+ CREATE OR REPLACE FUNCTION agent_board_test.stripped_body(
363
+ p_fn oid,
364
+ p_keep_literals boolean DEFAULT false
365
+ ) RETURNS text
366
+ LANGUAGE sql
367
+ STABLE
368
+ SET search_path = pg_catalog
369
+ AS $$
370
+ SELECT agent_board_test.strip_sql(p.prosrc, p_keep_literals)
371
+ FROM pg_proc p
372
+ WHERE p.oid = p_fn;
373
+ $$;
374
+
375
+ -- The stripped FULL DEFINITION. The header (`SECURITY DEFINER`, `SET
376
+ -- search_path = …`, the argument list) is regenerated by Postgres from catalog
377
+ -- metadata and cannot carry author prose, so it is left intact -- assertions
378
+ -- that match on it keep working. Only the body is stripped.
379
+ --
380
+ -- Stripping pg_get_functiondef() wholesale would be a self-inflicted wound:
381
+ -- the body is wrapped in a dollar quote (`AS $function$ … $function$`), so a
382
+ -- dollar-quote-aware strip would delete THE ENTIRE BODY and every `!~` over the
383
+ -- result would pass. Splicing avoids that trap by construction.
384
+ CREATE OR REPLACE FUNCTION agent_board_test.stripped_def(
385
+ p_fn oid,
386
+ p_keep_literals boolean DEFAULT false
387
+ ) RETURNS text
388
+ LANGUAGE plpgsql
389
+ STABLE
390
+ SET search_path = pg_catalog
391
+ AS $$
392
+ declare
393
+ v_def text;
394
+ v_src text;
395
+ v_at int;
396
+ begin
397
+ select pg_get_functiondef(p.oid), p.prosrc into v_def, v_src
398
+ from pg_proc p where p.oid = p_fn;
399
+ if v_def is null then
400
+ return null;
401
+ end if;
402
+ if v_src is null or v_src = '' then
403
+ return v_def;
404
+ end if;
405
+ v_at := position(v_src in v_def);
406
+ if v_at = 0 then
407
+ raise exception
408
+ 'agent_board_test.stripped_def: prosrc not found verbatim inside pg_get_functiondef for oid % -- refusing to guess', p_fn;
409
+ end if;
410
+ return substr(v_def, 1, v_at - 1)
411
+ || agent_board_test.strip_sql(v_src, p_keep_literals)
412
+ || substr(v_def, v_at + length(v_src));
413
+ end;
414
+ $$;
415
+
416
+ -- View definitions are REGENERATED by Postgres, so comments are already gone --
417
+ -- but string literals in the select list are not, and a negative assertion over
418
+ -- a view body is maskable by one.
419
+ CREATE OR REPLACE FUNCTION agent_board_test.stripped_viewdef(
420
+ p_rel oid,
421
+ p_keep_literals boolean DEFAULT false
422
+ ) RETURNS text
423
+ LANGUAGE sql
424
+ STABLE
425
+ SET search_path = pg_catalog
426
+ AS $$
427
+ SELECT agent_board_test.strip_sql(pg_get_viewdef(p_rel), p_keep_literals);
428
+ $$;
429
+
430
+ GRANT USAGE ON SCHEMA agent_board_test TO PUBLIC;
@@ -864,7 +864,7 @@ step retry_onboard:
864
864
  <waiting ...>
865
865
  step lifecycle_commit: COMMIT;
866
866
  step retry_onboard: <... completed>
867
- ERROR: enrollment replay conflicts with the frozen identity contract
867
+ ERROR: this signing key is already enrolled to you as agent "atomic-lifecycle-agent", but that identity is not resumable (its key or agent is inactive or unproven, its stored public key differs, or its enrollment audit records no granted scopes); enrollment need not be repeated; report this handle to an operator
868
868
  step retry_commit: COMMIT;
869
869
  step check:
870
870
  select a.status as agent_status,
@@ -929,7 +929,7 @@ step retry_onboard:
929
929
  <waiting ...>
930
930
  step lifecycle_commit: COMMIT;
931
931
  step retry_onboard: <... completed>
932
- ERROR: enrollment replay conflicts with the frozen identity contract
932
+ ERROR: this signing key is already enrolled to you as agent "atomic-lifecycle-agent", but that identity is not resumable (its key or agent is inactive or unproven, its stored public key differs, or its enrollment audit records no granted scopes); enrollment need not be repeated; report this handle to an operator
933
933
  step retry_commit: COMMIT;
934
934
  step check:
935
935
  select a.status as agent_status,
@@ -994,7 +994,7 @@ step retry_onboard:
994
994
  <waiting ...>
995
995
  step lifecycle_commit: COMMIT;
996
996
  step retry_onboard: <... completed>
997
- ERROR: enrollment replay conflicts with the frozen identity contract
997
+ ERROR: this signing key is already enrolled to you as agent "atomic-lifecycle-agent", but that identity is not resumable (its key or agent is inactive or unproven, its stored public key differs, or its enrollment audit records no granted scopes); enrollment need not be repeated; report this handle to an operator
998
998
  step retry_commit: COMMIT;
999
999
  step check:
1000
1000
  select a.status as agent_status,