@consiliency/agent-board-schema 1.63.0 → 1.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/fixtures/already-applied.json +20 -0
- package/migrations/20261014000200_agent_board_last_owner.sql +239 -0
- package/migrations/20261015000100_agent_board_session_provenance.sql +367 -0
- package/migrations/20261015000200_agent_board_request_preserve_removed.sql +87 -0
- package/migrations/20261015000300_agent_board_rls_correlation.sql +132 -0
- package/migrations/20261015000400_agent_board_resolution_authority.sql +146 -0
- package/package.json +1 -1
- package/schema-manifest.json +48 -3
- package/src/deployment-drift-main.test.mjs +156 -0
- package/src/deployment-drift-script.test.mjs +14 -1
- package/src/policy-comment.test.mjs +17 -0
- package/src/tooling.test.mjs +10 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +3 -1
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
- package/tests/agent_board_consent_gating.test.sql +4 -4
- package/tests/agent_board_fk_lock_edges.test.sql +3 -2
- package/tests/agent_board_gatenull_membstate.test.sql +7 -4
- package/tests/agent_board_grant_target_role.test.sql +4 -4
- package/tests/agent_board_hosted_delegates.test.sql +5 -3
- package/tests/agent_board_invite_no_downgrade.test.sql +5 -3
- package/tests/agent_board_ledger_authority.test.sql +7 -0
- package/tests/agent_board_policy_correlation.test.sql +189 -0
- package/tests/agent_board_request_removed.test.sql +96 -0
- package/tests/agent_board_resolution_authority.test.sql +190 -0
- package/tests/agent_board_role_guard.test.sql +319 -0
- package/tests/agent_board_rolereset.test.sql +17 -27
- package/tests/agent_board_roomdeleg.test.sql +8 -0
- package/tests/agent_board_roomown.test.sql +3 -1
- package/tests/agent_board_rowid.test.sql +4 -1
- package/tests/agent_board_session_provenance.test.sql +106 -0
- package/tests/catalog/function_execute_privileges.json +11 -2
- package/tests/catalog/p_metadata_parameters.json +4 -0
- package/tests/isolation/expected/consent_approval_lock.out +1 -1
- package/tests/isolation/expected/last_owner_departure.out +562 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
- package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
- package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
- package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
- package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
- package/tests/isolation/specs/consent_approval_lock.spec +13 -8
- package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
- package/tests/isolation/specs/grant_target_role_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
- package/tests/isolation/specs/last_owner_departure.spec +137 -0
- package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
- package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
- package/tests/isolation/specs/session_identity_links.spec +3 -1
- package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +0 -1
- package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- Consiliency/message-board#680: a join request cannot erase an eviction.
|
|
2
|
+
|
|
3
|
+
create or replace function agent_board.request_join_board(p_board_id uuid, p_agent_id uuid, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
4
|
+
RETURNS TABLE(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
5
|
+
LANGUAGE plpgsql
|
|
6
|
+
SECURITY DEFINER
|
|
7
|
+
SET search_path TO 'agent_board', 'public'
|
|
8
|
+
as $$
|
|
9
|
+
declare
|
|
10
|
+
v_board agent_board.boards%rowtype;
|
|
11
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
12
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
13
|
+
begin
|
|
14
|
+
-- GATENULL (#58): only service_role may proceed with an unresolved actor.
|
|
15
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
16
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
17
|
+
end if;
|
|
18
|
+
|
|
19
|
+
select * into v_board
|
|
20
|
+
from agent_board.boards
|
|
21
|
+
where id = p_board_id
|
|
22
|
+
and status = 'active'
|
|
23
|
+
and visibility in ('open', 'invite_only');
|
|
24
|
+
|
|
25
|
+
if v_board.id is null then
|
|
26
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
27
|
+
end if;
|
|
28
|
+
if v_actor is not null and v_actor <> p_agent_id then
|
|
29
|
+
raise exception 'board request actor mismatch' using errcode = '42501';
|
|
30
|
+
end if;
|
|
31
|
+
if not exists (select 1 from agent_board.agents where id = p_agent_id) then
|
|
32
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
33
|
+
end if;
|
|
34
|
+
|
|
35
|
+
-- Lock standing before deciding whether this request may replace a row.
|
|
36
|
+
perform 1 from agent_board.board_memberships bm
|
|
37
|
+
where bm.board_id = p_board_id and bm.agent_id = p_agent_id
|
|
38
|
+
order by bm.id
|
|
39
|
+
for update;
|
|
40
|
+
|
|
41
|
+
-- OPTION 3 ROOT CAUSE (#109 / Fable B5): an already-ACTIVE member requesting is a NO-OP.
|
|
42
|
+
-- The sink's active-preference UPDATE would otherwise DEMOTE their joined row to `requested`
|
|
43
|
+
-- (B5), or -- via other paths -- leave an (active, pending) duplicate that the resolver then
|
|
44
|
+
-- has to untangle. They are already in; requesting changes nothing. This is what stops the
|
|
45
|
+
-- dangerous duplicate arising from the request path in the first place.
|
|
46
|
+
if exists (
|
|
47
|
+
select 1 from agent_board.board_memberships bm
|
|
48
|
+
where bm.board_id = p_board_id
|
|
49
|
+
and bm.agent_id = p_agent_id
|
|
50
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
51
|
+
) then
|
|
52
|
+
return query
|
|
53
|
+
select bm.id, bm.board_id, bm.agent_id, bm.membership_state, bm.created_at, bm.updated_at
|
|
54
|
+
from agent_board.board_memberships bm
|
|
55
|
+
where bm.board_id = p_board_id
|
|
56
|
+
and bm.agent_id = p_agent_id
|
|
57
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
58
|
+
order by bm.created_at desc, bm.id desc
|
|
59
|
+
limit 1;
|
|
60
|
+
return;
|
|
61
|
+
end if;
|
|
62
|
+
|
|
63
|
+
if exists (
|
|
64
|
+
select 1 from agent_board.board_memberships bm
|
|
65
|
+
where bm.board_id = p_board_id
|
|
66
|
+
and bm.agent_id = p_agent_id
|
|
67
|
+
and bm.membership_state = 'removed'
|
|
68
|
+
) then
|
|
69
|
+
raise exception 'board join request refused for removed standing' using errcode = '42501';
|
|
70
|
+
end if;
|
|
71
|
+
|
|
72
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
73
|
+
p_board_id,
|
|
74
|
+
p_agent_id,
|
|
75
|
+
'requested',
|
|
76
|
+
p_agent_id,
|
|
77
|
+
p_metadata
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return query
|
|
81
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
|
|
82
|
+
end;
|
|
83
|
+
$$;
|
|
84
|
+
|
|
85
|
+
revoke all on function agent_board.request_join_board(uuid,uuid,jsonb) from public, anon;
|
|
86
|
+
grant execute on function agent_board.request_join_board(uuid,uuid,jsonb) to authenticated, service_role;
|
|
87
|
+
comment on function agent_board.request_join_board(uuid,uuid,jsonb) is 'Self-service join request. Locks the subject membership rows before checking standing; active members are unchanged, and removed standing is refused for every caller without erasing eviction evidence.';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
-- Consiliency/message-board#589: correlate policy subqueries to their protected row.
|
|
2
|
+
|
|
3
|
+
drop policy if exists agent_board_authenticated_select_threads on agent_board.threads;
|
|
4
|
+
create policy agent_board_authenticated_select_threads
|
|
5
|
+
on agent_board.threads
|
|
6
|
+
as permissive
|
|
7
|
+
for select
|
|
8
|
+
to authenticated
|
|
9
|
+
using (
|
|
10
|
+
created_by_agent_id = agent_board.current_agent_id()
|
|
11
|
+
or exists (
|
|
12
|
+
select 1
|
|
13
|
+
from agent_board.messages m
|
|
14
|
+
where m.thread_id = threads.id
|
|
15
|
+
and (
|
|
16
|
+
m.author_agent_id = agent_board.current_agent_id()
|
|
17
|
+
or exists (
|
|
18
|
+
select 1
|
|
19
|
+
from agent_board.delivery_recipients dr
|
|
20
|
+
where dr.source_message_id = m.id
|
|
21
|
+
and (
|
|
22
|
+
dr.recipient_agent_id = agent_board.current_agent_id()
|
|
23
|
+
or (
|
|
24
|
+
dr.board_id is not null
|
|
25
|
+
and exists (
|
|
26
|
+
select 1
|
|
27
|
+
from agent_board.board_memberships bm
|
|
28
|
+
where bm.board_id = dr.board_id
|
|
29
|
+
and bm.agent_id = agent_board.current_agent_id()
|
|
30
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
or exists (
|
|
38
|
+
select 1
|
|
39
|
+
from agent_board.tasks t
|
|
40
|
+
where t.thread_id = threads.id
|
|
41
|
+
and (
|
|
42
|
+
t.created_by_agent_id = agent_board.current_agent_id()
|
|
43
|
+
or t.assigned_agent_id = agent_board.current_agent_id()
|
|
44
|
+
or exists (
|
|
45
|
+
select 1
|
|
46
|
+
from agent_board.delivery_recipients dr
|
|
47
|
+
where dr.source_task_id = t.id
|
|
48
|
+
and (
|
|
49
|
+
dr.recipient_agent_id = agent_board.current_agent_id()
|
|
50
|
+
or (
|
|
51
|
+
dr.board_id is not null
|
|
52
|
+
and exists (
|
|
53
|
+
select 1
|
|
54
|
+
from agent_board.board_memberships bm
|
|
55
|
+
where bm.board_id = dr.board_id
|
|
56
|
+
and bm.agent_id = agent_board.current_agent_id()
|
|
57
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
or exists (
|
|
65
|
+
select 1
|
|
66
|
+
from agent_board.read_receipts rr
|
|
67
|
+
where rr.thread_id = threads.id
|
|
68
|
+
and rr.agent_id = agent_board.current_agent_id()
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
comment on policy agent_board_authenticated_select_threads on agent_board.threads is 'Correlates visibility to the protected threads row; inner aliases cannot capture its key columns.';
|
|
73
|
+
|
|
74
|
+
drop policy if exists agent_board_authenticated_select_delegations on agent_board.delegations;
|
|
75
|
+
create policy agent_board_authenticated_select_delegations
|
|
76
|
+
on agent_board.delegations
|
|
77
|
+
as permissive
|
|
78
|
+
for select
|
|
79
|
+
to authenticated
|
|
80
|
+
using (
|
|
81
|
+
delegated_by_agent_id = agent_board.current_agent_id()
|
|
82
|
+
or delegated_to_agent_id = agent_board.current_agent_id()
|
|
83
|
+
or exists (
|
|
84
|
+
select 1
|
|
85
|
+
from agent_board.tasks t
|
|
86
|
+
where t.id in (delegations.parent_task_id, delegations.child_task_id)
|
|
87
|
+
and (
|
|
88
|
+
t.created_by_agent_id = agent_board.current_agent_id()
|
|
89
|
+
or t.assigned_agent_id = agent_board.current_agent_id()
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
comment on policy agent_board_authenticated_select_delegations on agent_board.delegations is 'Correlates visibility to the protected delegations row; inner aliases cannot capture its key columns.';
|
|
95
|
+
|
|
96
|
+
revoke all on function agent_board._current_agent_has_board_access(uuid) from public, anon;
|
|
97
|
+
|
|
98
|
+
grant execute on function agent_board._current_agent_has_board_access(uuid) to authenticated, service_role;
|
|
99
|
+
|
|
100
|
+
comment on function agent_board._current_agent_has_board_access(uuid) is 'Session-derived read predicate used by authenticated RLS policies. Retains its existing access checks and service-role behavior; callable by authenticated and service_role.';
|
|
101
|
+
|
|
102
|
+
revoke all on function agent_board._current_agent_can_access_thread(uuid) from public, anon;
|
|
103
|
+
|
|
104
|
+
grant execute on function agent_board._current_agent_can_access_thread(uuid) to authenticated, service_role;
|
|
105
|
+
|
|
106
|
+
comment on function agent_board._current_agent_can_access_thread(uuid) is
|
|
107
|
+
'Thread access for the CURRENT session: service_role short-circuits true, otherwise '
|
|
108
|
+
'_agent_can_access_thread(p_thread_id, current_agent_id()). The disjunct set lives in that '
|
|
109
|
+
'helper and is not restated here. '
|
|
110
|
+
'ACCESS ONCE HELD IS NEVER WITHDRAWN, AND THAT IS DELIBERATE (message-board#287, extended and '
|
|
111
|
+
'RATIFIED for claims on message-board#291). The read_receipts, claims and inbox_items '
|
|
112
|
+
'disjuncts are all PERMANENT: no role holds DELETE or TRUNCATE on any of those tables and no '
|
|
113
|
+
'function body removes a row from them, so LEAVING A BOARD, LOSING A TASK ASSIGNMENT, OR '
|
|
114
|
+
'RELEASING A CLAIM DOES NOT REVOKE ACCESS TO A THREAD THE AGENT ALREADY HELD. The rule is '
|
|
115
|
+
'"you keep what you saw". For claims this is sharper than it looks and was ruled on '
|
|
116
|
+
'explicitly: the claims disjunct filters on (thread_id, agent_id) and NOT on status or '
|
|
117
|
+
'lease_expires_at, so a RELEASED or LONG-EXPIRED claim still grants access indefinitely. '
|
|
118
|
+
'Two alternatives were considered and REJECTED on #291: (a) filtering the disjunct on an '
|
|
119
|
+
'active, unexpired lease -- rejected because it reverses the #287 ruling for this one case '
|
|
120
|
+
'and would strip access at deploy time from every holder of a released or expired claim; '
|
|
121
|
+
'(b) recording the access separately when the claim is taken, so a stale row is not itself '
|
|
122
|
+
'the key -- acknowledged as the more honest design, rejected for now on cost and because '
|
|
123
|
+
'agents holding historical released claims would need backfilling or would silently lose '
|
|
124
|
+
'access. A future migration that adds a status or lease filter here is REVERSING A RULING, '
|
|
125
|
+
'not fixing a bug. What IS closed is the mint: mark_read (#287) and acquire_claim (#291) both '
|
|
126
|
+
'gate the NAMED subject through _agent_can_access_thread, so a receipt or a claim can only '
|
|
127
|
+
'ever record access already held. '
|
|
128
|
+
'access_mode = ''restricted'' is UNREACHABLE through the shipped API -- no function writes '
|
|
129
|
+
'threads.access_mode (it defaults to ''inherit'') and authenticated/service_role hold SELECT '
|
|
130
|
+
'and only SELECT on agent_board.threads -- so the inherit disjunction governs every thread in '
|
|
131
|
+
'production and the per-thread ACL added in #27 is dormant.'
|
|
132
|
+
' Callable by authenticated for its RLS policies and by service_role.';
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
-- Consiliency/message-board#668: resolution uses the pending reader's flat authority gate.
|
|
2
|
+
--
|
|
3
|
+
-- Consiliency/message-board#665 (integration): this migration sorts after
|
|
4
|
+
-- 20261015000100_agent_board_session_provenance, so on every fresh apply THIS body is the one
|
|
5
|
+
-- that survives. It therefore carries #665's session-derived attribution as well: both sink calls
|
|
6
|
+
-- (the redundant-pending retirement and the resolution write) pass v_actor, never the caller's
|
|
7
|
+
-- p_created_by_agent_id, which is retained for signature compatibility only. Because the gate
|
|
8
|
+
-- below refuses an unresolved principal (including an unresolved service-role session), v_actor
|
|
9
|
+
-- is always a resolved owner/admin when a sink is reached.
|
|
10
|
+
|
|
11
|
+
create or replace function agent_board.resolve_board_request(
|
|
12
|
+
p_board_id uuid,
|
|
13
|
+
p_agent_id uuid,
|
|
14
|
+
p_membership_state text,
|
|
15
|
+
p_created_by_agent_id uuid,
|
|
16
|
+
p_metadata jsonb default '{}'::jsonb
|
|
17
|
+
)
|
|
18
|
+
returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamp with time zone, updated_at timestamp with time zone)
|
|
19
|
+
language plpgsql
|
|
20
|
+
security definer
|
|
21
|
+
set search_path to 'agent_board', 'public'
|
|
22
|
+
as $function$
|
|
23
|
+
declare
|
|
24
|
+
v_effective_state text;
|
|
25
|
+
v_board agent_board.boards%rowtype;
|
|
26
|
+
v_membership_id uuid;
|
|
27
|
+
v_active_id uuid;
|
|
28
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
29
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
30
|
+
begin
|
|
31
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
32
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
33
|
+
end if;
|
|
34
|
+
|
|
35
|
+
if p_membership_state not in ('joined', 'subscribed', 'removed') then
|
|
36
|
+
raise exception 'resolve_board_request only accepts joined, subscribed, or removed' using errcode = '22023';
|
|
37
|
+
end if;
|
|
38
|
+
select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
|
|
39
|
+
if v_board.id is null then
|
|
40
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
41
|
+
end if;
|
|
42
|
+
if v_actor is null or not exists (
|
|
43
|
+
select 1 from agent_board.board_memberships bm
|
|
44
|
+
where bm.board_id = p_board_id
|
|
45
|
+
and bm.agent_id = v_actor
|
|
46
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
47
|
+
and bm.role in ('owner', 'admin')
|
|
48
|
+
) then
|
|
49
|
+
raise exception 'board request resolution requires owner or admin role' using errcode = '42501';
|
|
50
|
+
end if;
|
|
51
|
+
|
|
52
|
+
-- Lock the WHOLE (board, agent) set first so a concurrent self-withdrawal on ANY physical row
|
|
53
|
+
-- serialises behind this resolution (#109's race is per-(board,agent), not per-row).
|
|
54
|
+
perform 1
|
|
55
|
+
from agent_board.board_memberships bm
|
|
56
|
+
where bm.board_id = p_board_id
|
|
57
|
+
and bm.agent_id = p_agent_id
|
|
58
|
+
order by bm.id
|
|
59
|
+
for update;
|
|
60
|
+
|
|
61
|
+
-- Select the PENDING row explicitly (not the total-order winner). B2 (#116): with TWO pending
|
|
62
|
+
-- rows (a requested AND an invited) for one subject this resolves the NEWEST; the older dangles.
|
|
63
|
+
-- Inherent to the multi-row model — resolution is one act on one row.
|
|
64
|
+
select bm.id
|
|
65
|
+
into v_membership_id
|
|
66
|
+
from agent_board.board_memberships bm
|
|
67
|
+
where bm.board_id = p_board_id
|
|
68
|
+
and bm.agent_id = p_agent_id
|
|
69
|
+
and bm.membership_state in ('requested', 'invited')
|
|
70
|
+
order by bm.created_at desc, bm.id desc
|
|
71
|
+
limit 1;
|
|
72
|
+
|
|
73
|
+
select bm.id
|
|
74
|
+
into v_active_id
|
|
75
|
+
from agent_board.board_memberships bm
|
|
76
|
+
where bm.board_id = p_board_id
|
|
77
|
+
and bm.agent_id = p_agent_id
|
|
78
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
79
|
+
order by bm.created_at desc, bm.id desc
|
|
80
|
+
limit 1;
|
|
81
|
+
|
|
82
|
+
if v_membership_id is null then
|
|
83
|
+
raise exception 'pending board membership not found' using errcode = 'P0002';
|
|
84
|
+
end if;
|
|
85
|
+
|
|
86
|
+
-- OPTION 1 FALLBACK (#109 A3): approving a subject who is ALREADY active is a no-op success,
|
|
87
|
+
-- not a promotion of the pending row into collision with the active one. Return the active row
|
|
88
|
+
-- untouched. Rejection still falls through to act on the pending row.
|
|
89
|
+
--
|
|
90
|
+
-- B4 (#116): the no-op path intentionally IGNORES the requested target state and p_metadata —
|
|
91
|
+
-- approving with target 'subscribed' a 'joined' active row returns 'joined' (a self-toggle
|
|
92
|
+
-- belongs to update_board_membership, which is where the old path coupled the metadata write to
|
|
93
|
+
-- a demotion). Nothing legitimate is lost.
|
|
94
|
+
if p_membership_state in ('joined', 'subscribed') and v_active_id is not null then
|
|
95
|
+
-- B1 (#116): retire the redundant pending row so it stops showing in pending listings. The
|
|
96
|
+
-- subject is already active via v_active_id (untouched); route the retire through the sink so
|
|
97
|
+
-- the write EMITS (the emission-completeness invariant requires every membership write to hit
|
|
98
|
+
-- the ledger) — a board-authored 'left' event, which is NEUTRAL for consent (not the subject's
|
|
99
|
+
-- own act) and does not affect the subject's consent-present status carried by the active row.
|
|
100
|
+
-- 'left' is recoverable. Previously only a reject cleared this, leaving an owner-visible
|
|
101
|
+
-- dangling request after a no-op approve.
|
|
102
|
+
perform agent_board._upsert_board_membership_state(
|
|
103
|
+
p_board_id, p_agent_id, 'left', v_actor, p_metadata, v_membership_id, 'noop_superseded'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return query
|
|
107
|
+
select bm.id, bm.board_id, bm.agent_id, bm.membership_state, bm.left_at, bm.updated_at
|
|
108
|
+
from agent_board.board_memberships bm
|
|
109
|
+
where bm.id = v_active_id;
|
|
110
|
+
return;
|
|
111
|
+
end if;
|
|
112
|
+
|
|
113
|
+
-- REJECTSEM (#89): a rejection is NOT an eviction — resolve only acts on a PENDING row, so a
|
|
114
|
+
-- refusal lands on 'left' (recoverable), never 'removed' (which bars self-re-entry). Applied to
|
|
115
|
+
-- both requested and invited origins.
|
|
116
|
+
v_effective_state := case when p_membership_state = 'removed' then 'left'
|
|
117
|
+
else p_membership_state end;
|
|
118
|
+
|
|
119
|
+
-- #82: promotion into an active state requires the subject's own consent act (the sole
|
|
120
|
+
-- consent-predicate read site). Every accepted session, including service-role operators,
|
|
121
|
+
-- has a resolved owner/admin principal after the authority gate above; there is no exemption.
|
|
122
|
+
if v_actor is not null
|
|
123
|
+
and v_effective_state in ('joined', 'subscribed')
|
|
124
|
+
and not agent_board._membership_consent_present(p_board_id, p_agent_id) then
|
|
125
|
+
raise exception 'admitting % requires an act by that agent', p_agent_id
|
|
126
|
+
using errcode = '42501';
|
|
127
|
+
end if;
|
|
128
|
+
|
|
129
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
130
|
+
p_board_id,
|
|
131
|
+
p_agent_id,
|
|
132
|
+
v_effective_state,
|
|
133
|
+
v_actor,
|
|
134
|
+
p_metadata,
|
|
135
|
+
v_membership_id,
|
|
136
|
+
case when p_membership_state = 'removed' then 'reject_withdrawn' else null end
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return query
|
|
140
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
|
|
141
|
+
end;
|
|
142
|
+
$function$;
|
|
143
|
+
|
|
144
|
+
revoke all on function agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb) from public, anon;
|
|
145
|
+
grant execute on function agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb) to authenticated, service_role;
|
|
146
|
+
comment on function agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb) is 'Resolves pending memberships only for a resolved active owner/admin principal, independent of role_gate_enforced and creator identity. Matches list_board_pending_memberships, including refusal of an unresolved service-role session. Consent, rejection mapping and redundant-request retirement remain in force. Membership attribution is derived from current_agent_id(); p_created_by_agent_id is retained for signature compatibility only.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.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",
|
package/schema-manifest.json
CHANGED
|
@@ -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.
|
|
5
|
+
"packageVersion": "1.68.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":
|
|
50
|
-
"aggregateSha256": "
|
|
49
|
+
"migrationCount": 166,
|
|
50
|
+
"aggregateSha256": "c59fcf6ae0ce9cf567f68a27abee5c8a3a11d0d98ebc4d5cb07a496e7832964c",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1497,6 +1497,51 @@
|
|
|
1497
1497
|
"sourceRef": "message-board",
|
|
1498
1498
|
"bytes": 14704,
|
|
1499
1499
|
"sha256": "4a6c00546a1d2837f88eece1de082cc76eec1f85cce8cf9ac7998e42bf32a5c5"
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
"order": 161,
|
|
1503
|
+
"id": "20261014000200_agent_board_last_owner",
|
|
1504
|
+
"filename": "20261014000200_agent_board_last_owner.sql",
|
|
1505
|
+
"origin": "message-board-forward",
|
|
1506
|
+
"sourceRef": "message-board",
|
|
1507
|
+
"bytes": 13141,
|
|
1508
|
+
"sha256": "2fed8fc5a2c7c0ba9acbb12872b329db3f0902d96597c52cc50e21e4682c2fff"
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
"order": 162,
|
|
1512
|
+
"id": "20261015000100_agent_board_session_provenance",
|
|
1513
|
+
"filename": "20261015000100_agent_board_session_provenance.sql",
|
|
1514
|
+
"origin": "message-board-forward",
|
|
1515
|
+
"sourceRef": "message-board",
|
|
1516
|
+
"bytes": 16704,
|
|
1517
|
+
"sha256": "b5e3c7fc859dff7c3a0ccd4c0a7fb22d6ada1df68cb028be138035b586e1462a"
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
"order": 163,
|
|
1521
|
+
"id": "20261015000200_agent_board_request_preserve_removed",
|
|
1522
|
+
"filename": "20261015000200_agent_board_request_preserve_removed.sql",
|
|
1523
|
+
"origin": "message-board-forward",
|
|
1524
|
+
"sourceRef": "message-board",
|
|
1525
|
+
"bytes": 3736,
|
|
1526
|
+
"sha256": "9bad7438909e9c02cc034b59af67fa962e7251bedb53d691e762e651ba1c343c"
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
"order": 164,
|
|
1530
|
+
"id": "20261015000300_agent_board_rls_correlation",
|
|
1531
|
+
"filename": "20261015000300_agent_board_rls_correlation.sql",
|
|
1532
|
+
"origin": "message-board-forward",
|
|
1533
|
+
"sourceRef": "message-board",
|
|
1534
|
+
"bytes": 6348,
|
|
1535
|
+
"sha256": "aafe4a4b6eb1b6a5e3f0b5fbd8b4e4d395399de079c6a7b2eb2535ef4514bdc4"
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
"order": 165,
|
|
1539
|
+
"id": "20261015000400_agent_board_resolution_authority",
|
|
1540
|
+
"filename": "20261015000400_agent_board_resolution_authority.sql",
|
|
1541
|
+
"origin": "message-board-forward",
|
|
1542
|
+
"sourceRef": "message-board",
|
|
1543
|
+
"bytes": 7396,
|
|
1544
|
+
"sha256": "bfeb7bda1eb41bc431416502db69c33156ff72acfd79cefdfd556d933f79d809"
|
|
1500
1545
|
}
|
|
1501
1546
|
],
|
|
1502
1547
|
"excludedBridge": {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Run the shipped checker with REAL divergent Git history. Only npm/board I/O is stubbed.
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
const ROOT = fileURLToPath(new URL("../../../", import.meta.url));
|
|
10
|
+
const PKG = "packages/agent-board-schema";
|
|
11
|
+
const roots = [];
|
|
12
|
+
afterEach((context) => { context.task.meta.drift_assertions = expect.getState().assertionCalls; for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
|
13
|
+
const stamp = "2026-01-02T00:00:00+00:00";
|
|
14
|
+
function git(cwd, args, env = {}) {
|
|
15
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", env: { ...process.env, ...env } });
|
|
16
|
+
if (r.status !== 0) throw new Error(`fixture git failed: ${r.stderr}`);
|
|
17
|
+
return r.stdout.trim();
|
|
18
|
+
}
|
|
19
|
+
function installChecker(dir) {
|
|
20
|
+
mkdirSync(join(dir, "scripts/lib"), { recursive: true });
|
|
21
|
+
for (const file of ["check-deployment-drift.mjs", "lib/drift-evaluate.mjs"]) {
|
|
22
|
+
copyFileSync(join(ROOT, "scripts", file), join(dir, "scripts", file));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function fixture() {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), "drift-main-")); roots.push(root);
|
|
27
|
+
const dir = join(root, "checkout"); mkdirSync(join(dir, PKG), { recursive: true });
|
|
28
|
+
git(dir, ["init", "-q", "-b", "main"]);
|
|
29
|
+
const commit = (version, date, description = "original") => {
|
|
30
|
+
writeFileSync(join(dir, PKG, "package.json"), JSON.stringify({ name: "@consiliency/agent-board-schema", version, description }, null, 2));
|
|
31
|
+
writeFileSync(join(dir, PKG, "schema-manifest.json"), JSON.stringify({ schemaVersion: 8, aggregateSha256: `digest-${version}` }));
|
|
32
|
+
git(dir, ["add", PKG]);
|
|
33
|
+
git(dir, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "commit", "-qm", version + description],
|
|
34
|
+
{ GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date });
|
|
35
|
+
return git(dir, ["rev-parse", "HEAD"]);
|
|
36
|
+
};
|
|
37
|
+
commit("1.63.0", "2026-01-01T00:00:00Z");
|
|
38
|
+
commit("1.64.0", stamp);
|
|
39
|
+
const main = commit("1.64.0", "2026-01-04T00:00:00Z", "adjacent edit");
|
|
40
|
+
git(dir, ["switch", "-qc", "proposal"]);
|
|
41
|
+
const proposal = commit("1.65.0", "2026-01-05T00:00:00Z", "unmerged proposal");
|
|
42
|
+
// A cached tracking branch deliberately lies. CI must use the event's base SHA.
|
|
43
|
+
git(dir, ["update-ref", "refs/remotes/origin/main", proposal]);
|
|
44
|
+
// A real local remote supplies operator truth without any network/service access.
|
|
45
|
+
const remote = join(root, "remote.git"); git(root, ["clone", "--bare", "-q", dir, remote]);
|
|
46
|
+
git(dir, ["remote", "add", "origin", remote]);
|
|
47
|
+
installChecker(dir);
|
|
48
|
+
const shims = join(root, "bin"); mkdirSync(shims);
|
|
49
|
+
writeFileSync(join(shims, "npm"), '#!/bin/sh\nprintf "%s\\n" "$PUBLISHED"\n'); chmodSync(join(shims, "npm"), 0o755);
|
|
50
|
+
const preload = join(root, "no-network.mjs");
|
|
51
|
+
writeFileSync(preload, 'globalThis.fetch = async () => ({ ok: true, json: async () => [JSON.parse(process.env.BOARD_ROW)] });');
|
|
52
|
+
const eventPath = join(root, "event.json");
|
|
53
|
+
const event = { pull_request: { base: { ref: "main", sha: main, repo: { full_name: "Consiliency/message-board" } } } };
|
|
54
|
+
const run = ({ env = {}, payload = event, cwd = dir, args = [] } = {}) => {
|
|
55
|
+
writeFileSync(eventPath, JSON.stringify(payload));
|
|
56
|
+
const r = spawnSync(process.execPath, [join(cwd, "scripts/check-deployment-drift.mjs"), "--json", ...args], {
|
|
57
|
+
cwd, encoding: "utf8", timeout: 15_000, env: { ...process.env,
|
|
58
|
+
PATH: `${shims}:${process.env.PATH}`, NODE_OPTIONS: `--import ${pathToFileURL(preload).href}`,
|
|
59
|
+
GITHUB_ACTIONS: "true", GITHUB_EVENT_NAME: "pull_request", GITHUB_EVENT_PATH: eventPath,
|
|
60
|
+
GITHUB_REF: "refs/pull/1/merge", GITHUB_SHA: proposal, GITHUB_REPOSITORY: "Consiliency/message-board",
|
|
61
|
+
PUBLISHED: "1.64.0", SUPABASE_ACCESS_TOKEN: "disposable-canary-never-authenticates", ...env },
|
|
62
|
+
});
|
|
63
|
+
expect(r.error).toBeUndefined();
|
|
64
|
+
const out = r.stdout + r.stderr;
|
|
65
|
+
const doc = JSON.parse(r.stdout);
|
|
66
|
+
return { ...r, out, doc };
|
|
67
|
+
};
|
|
68
|
+
return { root, dir, remote, main, proposal, event, run };
|
|
69
|
+
}
|
|
70
|
+
function expectMain(r, main) {
|
|
71
|
+
expect(r.status).toBe(0);
|
|
72
|
+
expect(r.doc.status).toBe("ok");
|
|
73
|
+
expect(r.doc.facts.merged).toBe("1.64.0");
|
|
74
|
+
expect(r.doc.facts.mergedCommit).toBe(main);
|
|
75
|
+
expect(new Date(r.doc.facts.bumpIso).toISOString()).toBe(new Date(stamp).toISOString());
|
|
76
|
+
expect(r.doc.facts.expectedDigest).toBe("digest-1.64.0");
|
|
77
|
+
}
|
|
78
|
+
function expectUnknown(r) {
|
|
79
|
+
expect(r.status).toBe(2);
|
|
80
|
+
expect(r.doc.status).toBe("unknown");
|
|
81
|
+
expect(r.doc.facts.mergedSourceError).toMatch(/authoritative main\/history unavailable/);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("authoritative main, independent of the checked-out proposal", () => {
|
|
85
|
+
it.each(["proposal", "merge"])("PR %s: main 1.64, old unmerged 1.65, poisoned origin/main", (checkout) => {
|
|
86
|
+
const f = fixture();
|
|
87
|
+
if (checkout === "merge") {
|
|
88
|
+
// An actual two-parent PR-style merge, still containing the unmerged 1.65 proposal.
|
|
89
|
+
git(f.dir, ["switch", "-qc", "merge", f.main]);
|
|
90
|
+
git(f.dir, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "merge", "--no-ff", "-qm", "PR merge", "proposal"]);
|
|
91
|
+
}
|
|
92
|
+
expect(JSON.parse(readFileSync(join(f.dir, PKG, "package.json"))).version).toBe("1.65.0");
|
|
93
|
+
expectMain(f.run({ env: { GITHUB_SHA: git(f.dir, ["rev-parse", "HEAD"]) } }), f.main);
|
|
94
|
+
});
|
|
95
|
+
it.each(["push", "schedule", "workflow_dispatch"])("%s uses its main event SHA, with another ref checked out", (event) => {
|
|
96
|
+
const f = fixture();
|
|
97
|
+
expectMain(f.run({ env: { GITHUB_EVENT_NAME: event, GITHUB_REF: "refs/heads/main", GITHUB_SHA: f.main } }), f.main);
|
|
98
|
+
});
|
|
99
|
+
it("operator resolves live origin main despite poisoned tracking ref and proposal checkout", () => {
|
|
100
|
+
const f = fixture(); expectMain(f.run({ env: { GITHUB_ACTIONS: "false" } }), f.main);
|
|
101
|
+
});
|
|
102
|
+
it("a real merged publication gap still fails its age threshold", () => {
|
|
103
|
+
const f = fixture(); const r = f.run({ env: { PUBLISHED: "1.63.0" } });
|
|
104
|
+
expect(r.status).toBe(1); expect(r.doc.status).toBe("drift");
|
|
105
|
+
expect(new Date(r.doc.facts.bumpIso).toISOString()).toBe(new Date(stamp).toISOString());
|
|
106
|
+
expect(r.out).toMatch(/1 schema version\(s\) merged but NOT published/);
|
|
107
|
+
});
|
|
108
|
+
it("version threshold remains independent of the age threshold", () => {
|
|
109
|
+
const f = fixture(); const r = f.run({ env: { PUBLISHED: "1.60.0" }, args: ["--max-days", "10000"] });
|
|
110
|
+
expect(r.status).toBe(1); expect(r.out).toMatch(/4 schema version\(s\)/);
|
|
111
|
+
expect(f.run({ env: { PUBLISHED: "1.63.0" }, args: ["--max-days", "10000"] }).status).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
it("board comparisons use main's manifest along with main's version", () => {
|
|
114
|
+
const f = fixture(); const row = { package_version: "1.64.0", schema_version: 8, bundle_digest: "digest-1.64.0", installed_at: "2026-01-06T00:00:00Z" };
|
|
115
|
+
const options = { args: ["--board", "fixture-only"], env: { BOARD_ROW: JSON.stringify(row) } };
|
|
116
|
+
expectMain(f.run(options), f.main);
|
|
117
|
+
const mismatch = f.run({ ...options, env: { BOARD_ROW: JSON.stringify({ ...row, bundle_digest: "digest-1.65.0" }) } });
|
|
118
|
+
expect(mismatch.status).toBe(1); expect(mismatch.out).toMatch(/digest/);
|
|
119
|
+
});
|
|
120
|
+
it.each([{}, { pull_request: { base: { ref: "proposal", sha: "a".repeat(40) } } },
|
|
121
|
+
{ pull_request: { base: { ref: "main", sha: "a".repeat(40), repo: { full_name: "other/repo" } } } }])("missing/untrusted PR base is UNKNOWN, never HEAD fallback: %j", (payload) => {
|
|
122
|
+
const f = fixture(); expectUnknown(f.run({ payload }));
|
|
123
|
+
});
|
|
124
|
+
it("an unavailable authoritative commit is UNKNOWN even when HEAD could pass", () => {
|
|
125
|
+
const f = fixture(); f.event.pull_request.base.sha = "f".repeat(40); expectUnknown(f.run());
|
|
126
|
+
});
|
|
127
|
+
it.each(["push", "schedule", "workflow_dispatch", "unrecognized"])("%s outside main cannot fall back to checkout or cached main", (event) => {
|
|
128
|
+
const f = fixture(); expectUnknown(f.run({ env: { GITHUB_EVENT_NAME: event, GITHUB_REF: "refs/heads/proposal" } }));
|
|
129
|
+
});
|
|
130
|
+
it("missing event SHA is UNKNOWN", () => {
|
|
131
|
+
const f = fixture(); expectUnknown(f.run({ env: { GITHUB_EVENT_NAME: "push", GITHUB_REF: "refs/heads/main", GITHUB_SHA: "" } }));
|
|
132
|
+
});
|
|
133
|
+
it("unreadable PR event is UNKNOWN", () => {
|
|
134
|
+
const f = fixture(); expectUnknown(f.run({ env: { GITHUB_EVENT_PATH: join(f.root, "absent.json") } }));
|
|
135
|
+
});
|
|
136
|
+
it("shallow main is UNKNOWN even when its version equals published", () => {
|
|
137
|
+
const f = fixture(); const shallow = join(f.root, "shallow");
|
|
138
|
+
git(f.root, ["clone", "-q", "--depth=1", "--branch=main", pathToFileURL(f.remote).href, shallow]);
|
|
139
|
+
expect(git(shallow, ["rev-parse", "--is-shallow-repository"])).toBe("true");
|
|
140
|
+
installChecker(shallow); expectUnknown(f.run({ cwd: shallow }));
|
|
141
|
+
});
|
|
142
|
+
it("operator refuses missing live main; credential-bearing git errors are never echoed", () => {
|
|
143
|
+
const f = fixture(); const canary = "do-not-echo-this-credential";
|
|
144
|
+
git(f.dir, ["remote", "set-url", "origin", `file://${join(f.root, canary, "absent.git")}`]);
|
|
145
|
+
const r = f.run({ env: { GITHUB_ACTIONS: "false" } }); expectUnknown(r); expect(r.out).not.toContain(canary);
|
|
146
|
+
});
|
|
147
|
+
it("operator refuses a live main object unavailable in its checkout", () => {
|
|
148
|
+
const f = fixture();
|
|
149
|
+
// Create a remote-only descendant without fetching it into the checked-out repo.
|
|
150
|
+
const other = join(f.root, "writer"); git(f.root, ["clone", "-q", "--branch=main", f.remote, other]);
|
|
151
|
+
writeFileSync(join(other, "new.txt"), "remote-only"); git(other, ["add", "new.txt"]);
|
|
152
|
+
git(other, ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "commit", "-qm", "remote-only"]);
|
|
153
|
+
git(other, ["push", "-q", "origin", "main"]);
|
|
154
|
+
expectUnknown(f.run({ env: { GITHUB_ACTIONS: "false" } }));
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -67,6 +67,8 @@ let PRELOAD;
|
|
|
67
67
|
beforeAll(() => {
|
|
68
68
|
SHIMS = mkdtempSync(join(tmpdir(), "drift-shims-"));
|
|
69
69
|
ARGV_LOG = join(SHIMS, "argv.log");
|
|
70
|
+
writeFileSync(join(SHIMS, "package.json"), readFileSync(PKG_JSON));
|
|
71
|
+
writeFileSync(join(SHIMS, "manifest.json"), readFileSync(MANIFEST));
|
|
70
72
|
|
|
71
73
|
// One arg per line, `--` between invocations, so an assertion can read exact argv rather
|
|
72
74
|
// than a space-joined approximation.
|
|
@@ -78,7 +80,7 @@ beforeAll(() => {
|
|
|
78
80
|
);
|
|
79
81
|
writeFileSync(
|
|
80
82
|
join(SHIMS, "git"),
|
|
81
|
-
`#!/bin/sh\n${logArgv}\ncase " $* " in\n *"
|
|
83
|
+
`#!/bin/sh\n${logArgv}\ncase " $* " in\n *" --is-shallow-repository "*) printf "%s\n" "\${FAKE_SHALLOW:-false}" ;;\n *" cat-file "*) exit 0 ;;\n *" show "*"/package.json "*) cat "$DRIFT_SHIMS/package.json" ;;\n *" show "*"/schema-manifest.json "*) cat "$DRIFT_SHIMS/manifest.json" ;;\n *" log "*) [ -n "\${FAKE_BUMP_ISO:-}" ] || exit 1; printf "%s\n" "$FAKE_BUMP_ISO" ;;\n *) exit 1 ;;\nesac\n`,
|
|
82
84
|
);
|
|
83
85
|
chmodSync(join(SHIMS, "npm"), 0o755);
|
|
84
86
|
chmodSync(join(SHIMS, "git"), 0o755);
|
|
@@ -139,6 +141,11 @@ function runScript(args = [], env = {}) {
|
|
|
139
141
|
PATH: `${SHIMS}:${process.env.PATH}`,
|
|
140
142
|
NODE_OPTIONS: `--import ${pathToFileURL(PRELOAD).href}`,
|
|
141
143
|
DRIFT_ARGV_LOG: ARGV_LOG,
|
|
144
|
+
DRIFT_SHIMS: SHIMS,
|
|
145
|
+
GITHUB_ACTIONS: "true",
|
|
146
|
+
GITHUB_EVENT_NAME: "push",
|
|
147
|
+
GITHUB_REF: "refs/heads/main",
|
|
148
|
+
GITHUB_SHA: "a".repeat(40),
|
|
142
149
|
FAKE_BUMP_ISO: FRESH_BUMP,
|
|
143
150
|
FAKE_SHALLOW: "false",
|
|
144
151
|
FAKE_FETCH_MODE: "",
|
|
@@ -186,6 +193,12 @@ describe("the shipped script's exit mapping", () => {
|
|
|
186
193
|
expect(r.status).toBe(2);
|
|
187
194
|
});
|
|
188
195
|
|
|
196
|
+
it("missing bump history is UNKNOWN even when merged and published agree", () => {
|
|
197
|
+
const r = runScript([], { FAKE_NPM_VERSION: MERGED, FAKE_BUMP_ISO: "" });
|
|
198
|
+
expect(r.status).toBe(2);
|
|
199
|
+
expect(r.out).toMatch(/authoritative main\/history unavailable/);
|
|
200
|
+
});
|
|
201
|
+
|
|
189
202
|
it("refuses to date the bump on a shallow clone, so an unpublished gap is UNKNOWN", () => {
|
|
190
203
|
// FAKE_SHALLOW was never true in any test, so the shallow-clone refusal the previous
|
|
191
204
|
// round added was driven by nothing. Making isShallowClone() return false
|