@consiliency/agent-board-schema 1.2.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/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +55 -0
- package/bin/agent-board-schema.js +97 -0
- package/fixtures/already-applied.json +181 -0
- package/fixtures/divergent-history.json +173 -0
- package/fixtures/production-history.json +173 -0
- package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
- package/migrations/20260502000000_agent_board_schema.sql +180 -0
- package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
- package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
- package/migrations/20260504000000_agent_board_read_models.sql +206 -0
- package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
- package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
- package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
- package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
- package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
- package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
- package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
- package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
- package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
- package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
- package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
- package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
- package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
- package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
- package/migrations/20260513000001_agent_board_retention.sql +160 -0
- package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
- package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
- package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
- package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
- package/migrations/20260517000000_agent_board_addressing.sql +542 -0
- package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
- package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
- package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
- package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
- package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
- package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
- package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
- package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
- package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
- package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
- package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
- package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
- package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
- package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
- package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
- package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
- package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
- package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
- package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
- package/package.json +42 -0
- package/provenance.json +580 -0
- package/schema-manifest.json +458 -0
- package/src/check.js +23 -0
- package/src/index.js +50 -0
- package/src/materialize.js +73 -0
- package/src/plan.js +44 -0
- package/src/tooling.test.mjs +97 -0
- package/tests/agent_board_addressing.test.sql +166 -0
- package/tests/agent_board_auth_rls.test.sql +243 -0
- package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
- package/tests/agent_board_auth_rpcs.test.sql +311 -0
- package/tests/agent_board_auth_schema.test.sql +411 -0
- package/tests/agent_board_authbound_boundary.test.sql +148 -0
- package/tests/agent_board_boardopt.test.sql +170 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
- package/tests/agent_board_callback_runtime.test.sql +425 -0
- package/tests/agent_board_callbackcap.test.sql +222 -0
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
- package/tests/agent_board_dblint_rpcs.test.sql +247 -0
- package/tests/agent_board_fleet_policy.test.sql +370 -0
- package/tests/agent_board_fleet_read_models.test.sql +158 -0
- package/tests/agent_board_fleet_registry.test.sql +149 -0
- package/tests/agent_board_inbox_reads.test.sql +471 -0
- package/tests/agent_board_inbox_rpcs.test.sql +452 -0
- package/tests/agent_board_inbox_schema.test.sql +355 -0
- package/tests/agent_board_nonfleet_identity.test.sql +26 -0
- package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
- package/tests/agent_board_schema_version.test.sql +24 -0
- package/tests/agent_board_schema_version_rpc.test.sql +40 -0
- package/tests/agent_board_signature_rls_reads.test.sql +133 -0
- package/tests/agent_board_signed_write_gateway.test.sql +219 -0
- package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
- package/tests/agent_board_signing_store.test.sql +183 -0
- package/tests/agent_board_smoke.test.sql +12 -0
- package/tests/agent_board_v1_invariants.test.sql +298 -0
- package/tests/agent_board_v2_invariants.test.sql +753 -0
- package/tests/agent_board_workload_providers.test.sql +15 -0
- package/tests/fixtures/adopter-base.sql +19 -0
- package/tests/fixtures/operator-hardening.sql +27 -0
- package/tests/fixtures/test-prereqs.sql +84 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
create table if not exists agent_board.agent_signing_keys (
|
|
2
|
+
id uuid primary key default gen_random_uuid(),
|
|
3
|
+
agent_id uuid not null references agent_board.agents(id) on delete cascade,
|
|
4
|
+
key_id text not null,
|
|
5
|
+
key_version integer not null check (key_version > 0),
|
|
6
|
+
algorithm text not null default 'Ed25519' check (algorithm in ('Ed25519')),
|
|
7
|
+
public_key text not null,
|
|
8
|
+
public_key_format text not null check (public_key_format in ('raw_ed25519_base64url', 'spki_der_base64url', 'spki_pem')),
|
|
9
|
+
fingerprint text not null unique check (fingerprint ~ '^sha256:[A-Za-z0-9_-]{43}$'),
|
|
10
|
+
status text not null default 'pending_proof' check (status in ('pending_proof', 'active', 'revoked', 'expired', 'compromised')),
|
|
11
|
+
proof_state text not null default 'unproven' check (proof_state in ('unproven', 'challenge_issued', 'proven', 'failed', 'revoked', 'expired')),
|
|
12
|
+
rotates_signing_key_id uuid references agent_board.agent_signing_keys(id) on delete set null,
|
|
13
|
+
rotated_by_signing_key_id uuid references agent_board.agent_signing_keys(id) on delete set null,
|
|
14
|
+
expires_at timestamptz,
|
|
15
|
+
revoked_at timestamptz,
|
|
16
|
+
revoked_by_agent_id uuid references agent_board.agents(id) on delete set null,
|
|
17
|
+
revocation_reason text,
|
|
18
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
19
|
+
created_at timestamptz not null default now(),
|
|
20
|
+
updated_at timestamptz not null default now(),
|
|
21
|
+
constraint agent_signing_keys_key_id_unique unique (agent_id, key_id),
|
|
22
|
+
constraint agent_signing_keys_key_version_unique unique (agent_id, key_version),
|
|
23
|
+
constraint agent_signing_keys_active_requires_proof check (status <> 'active' or proof_state = 'proven'),
|
|
24
|
+
constraint agent_signing_keys_revoked_shape check (
|
|
25
|
+
(status in ('revoked', 'compromised') and revoked_at is not null)
|
|
26
|
+
or (status not in ('revoked', 'compromised') and revoked_at is null)
|
|
27
|
+
),
|
|
28
|
+
constraint agent_signing_keys_not_self_rotates check (rotates_signing_key_id is null or rotates_signing_key_id <> id),
|
|
29
|
+
constraint agent_signing_keys_not_self_rotated_by check (rotated_by_signing_key_id is null or rotated_by_signing_key_id <> id)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
comment on table agent_board.agent_signing_keys is 'Public Ed25519 signing-key registry for Message Board agents; never stores private keys.';
|
|
33
|
+
|
|
34
|
+
create index if not exists idx_agent_board_agent_signing_keys_agent_status
|
|
35
|
+
on agent_board.agent_signing_keys(agent_id, status, key_version desc);
|
|
36
|
+
|
|
37
|
+
create table if not exists agent_board.agent_signing_key_challenges (
|
|
38
|
+
id uuid primary key default gen_random_uuid(),
|
|
39
|
+
signing_key_id uuid not null references agent_board.agent_signing_keys(id) on delete cascade,
|
|
40
|
+
agent_id uuid not null references agent_board.agents(id) on delete cascade,
|
|
41
|
+
challenge_nonce text not null unique,
|
|
42
|
+
challenge_bytes text not null,
|
|
43
|
+
proof_state text not null default 'pending' check (proof_state in ('pending', 'verified', 'failed', 'expired', 'consumed')),
|
|
44
|
+
failure_class text check (
|
|
45
|
+
failure_class is null
|
|
46
|
+
or failure_class in (
|
|
47
|
+
'unsupported_algorithm',
|
|
48
|
+
'signature_invalid',
|
|
49
|
+
'key_revoked',
|
|
50
|
+
'key_expired',
|
|
51
|
+
'timestamp_out_of_window',
|
|
52
|
+
'nonce_replay',
|
|
53
|
+
'actor_mismatch'
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
expires_at timestamptz not null,
|
|
57
|
+
consumed_at timestamptz,
|
|
58
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
59
|
+
created_at timestamptz not null default now(),
|
|
60
|
+
updated_at timestamptz not null default now(),
|
|
61
|
+
constraint agent_signing_key_challenges_consumed_shape check (
|
|
62
|
+
(proof_state in ('verified', 'failed', 'expired', 'consumed') and consumed_at is not null)
|
|
63
|
+
or (proof_state = 'pending' and consumed_at is null)
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
comment on table agent_board.agent_signing_key_challenges is 'Bounded one-time public-key proof challenges.';
|
|
68
|
+
|
|
69
|
+
create index if not exists idx_agent_board_agent_signing_key_challenges_key
|
|
70
|
+
on agent_board.agent_signing_key_challenges(signing_key_id, created_at desc);
|
|
71
|
+
|
|
72
|
+
create table if not exists agent_board.signature_evidence (
|
|
73
|
+
id uuid primary key default gen_random_uuid(),
|
|
74
|
+
agent_id uuid not null references agent_board.agents(id) on delete cascade,
|
|
75
|
+
signing_key_id uuid references agent_board.agent_signing_keys(id) on delete set null,
|
|
76
|
+
key_id text,
|
|
77
|
+
key_version integer check (key_version is null or key_version > 0),
|
|
78
|
+
algorithm text not null default 'Ed25519' check (algorithm in ('Ed25519')),
|
|
79
|
+
source_kind text not null check (source_kind in ('message', 'task_event', 'artifact', 'delivery_recipient', 'external')),
|
|
80
|
+
source_message_id uuid references agent_board.messages(id) on delete cascade,
|
|
81
|
+
source_task_event_id uuid references agent_board.task_events(id) on delete cascade,
|
|
82
|
+
source_artifact_id uuid references agent_board.artifacts(id) on delete cascade,
|
|
83
|
+
source_delivery_recipient_id uuid references agent_board.delivery_recipients(id) on delete cascade,
|
|
84
|
+
operation text not null,
|
|
85
|
+
payload_digest text not null,
|
|
86
|
+
canonicalization_version text not null default 'agent-provenance-jcs.v1',
|
|
87
|
+
signature_digest text,
|
|
88
|
+
signature_ref text,
|
|
89
|
+
verification_status text not null check (verification_status in ('unsigned', 'observed', 'warn', 'verified', 'failed')),
|
|
90
|
+
failure_class text check (
|
|
91
|
+
failure_class is null
|
|
92
|
+
or failure_class in (
|
|
93
|
+
'unsigned_required',
|
|
94
|
+
'unsupported_algorithm',
|
|
95
|
+
'unsupported_canonicalization',
|
|
96
|
+
'schema_version_mismatch',
|
|
97
|
+
'actor_mismatch',
|
|
98
|
+
'payload_digest_mismatch',
|
|
99
|
+
'signature_invalid',
|
|
100
|
+
'key_unknown',
|
|
101
|
+
'key_revoked',
|
|
102
|
+
'key_expired',
|
|
103
|
+
'timestamp_out_of_window',
|
|
104
|
+
'nonce_replay',
|
|
105
|
+
'idempotency_conflict',
|
|
106
|
+
'external_evidence_untrusted'
|
|
107
|
+
)
|
|
108
|
+
),
|
|
109
|
+
signed_at timestamptz,
|
|
110
|
+
verified_at timestamptz,
|
|
111
|
+
nonce text,
|
|
112
|
+
idempotency_key text,
|
|
113
|
+
external_evidence_ref text,
|
|
114
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
115
|
+
created_at timestamptz not null default now(),
|
|
116
|
+
constraint signature_evidence_source_shape check (
|
|
117
|
+
(source_kind = 'message' and source_message_id is not null and source_task_event_id is null and source_artifact_id is null and source_delivery_recipient_id is null)
|
|
118
|
+
or (source_kind = 'task_event' and source_message_id is null and source_task_event_id is not null and source_artifact_id is null and source_delivery_recipient_id is null)
|
|
119
|
+
or (source_kind = 'artifact' and source_message_id is null and source_task_event_id is null and source_artifact_id is not null and source_delivery_recipient_id is null)
|
|
120
|
+
or (source_kind = 'delivery_recipient' and source_message_id is null and source_task_event_id is null and source_artifact_id is null and source_delivery_recipient_id is not null)
|
|
121
|
+
or (source_kind = 'external' and source_message_id is null and source_task_event_id is null and source_artifact_id is null and source_delivery_recipient_id is null and external_evidence_ref is not null)
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
comment on table agent_board.signature_evidence is 'Append-only metadata-safe signature evidence linked to canonical source rows.';
|
|
126
|
+
|
|
127
|
+
create index if not exists idx_agent_board_signature_evidence_message
|
|
128
|
+
on agent_board.signature_evidence(source_message_id, created_at desc)
|
|
129
|
+
where source_message_id is not null;
|
|
130
|
+
|
|
131
|
+
create index if not exists idx_agent_board_signature_evidence_task_event
|
|
132
|
+
on agent_board.signature_evidence(source_task_event_id, created_at desc)
|
|
133
|
+
where source_task_event_id is not null;
|
|
134
|
+
|
|
135
|
+
create index if not exists idx_agent_board_signature_evidence_agent
|
|
136
|
+
on agent_board.signature_evidence(agent_id, created_at desc);
|
|
137
|
+
|
|
138
|
+
create table if not exists agent_board.signature_replay_state (
|
|
139
|
+
id uuid primary key default gen_random_uuid(),
|
|
140
|
+
agent_id uuid not null references agent_board.agents(id) on delete cascade,
|
|
141
|
+
signing_key_id uuid references agent_board.agent_signing_keys(id) on delete cascade,
|
|
142
|
+
operation text not null,
|
|
143
|
+
payload_digest text not null,
|
|
144
|
+
nonce text,
|
|
145
|
+
idempotency_key text,
|
|
146
|
+
envelope_timestamp timestamptz not null,
|
|
147
|
+
expires_at timestamptz not null,
|
|
148
|
+
replay_status text not null default 'consumed' check (replay_status in ('consumed', 'duplicate', 'conflict', 'expired')),
|
|
149
|
+
signature_evidence_id uuid references agent_board.signature_evidence(id) on delete set null,
|
|
150
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
151
|
+
created_at timestamptz not null default now(),
|
|
152
|
+
constraint signature_replay_state_key_shape check (
|
|
153
|
+
(nonce is not null and idempotency_key is null)
|
|
154
|
+
or (nonce is null and idempotency_key is not null)
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
create unique index if not exists idx_agent_board_signature_replay_nonce_unique
|
|
159
|
+
on agent_board.signature_replay_state(agent_id, coalesce(signing_key_id, '00000000-0000-0000-0000-000000000000'::uuid), operation, payload_digest, nonce)
|
|
160
|
+
where nonce is not null;
|
|
161
|
+
|
|
162
|
+
create unique index if not exists idx_agent_board_signature_replay_idempotency_unique
|
|
163
|
+
on agent_board.signature_replay_state(agent_id, operation, idempotency_key)
|
|
164
|
+
where idempotency_key is not null;
|
|
165
|
+
|
|
166
|
+
grant select on agent_board.agent_signing_keys,
|
|
167
|
+
agent_board.agent_signing_key_challenges,
|
|
168
|
+
agent_board.signature_evidence,
|
|
169
|
+
agent_board.signature_replay_state
|
|
170
|
+
to service_role;
|
|
171
|
+
|
|
172
|
+
insert into agent_board.schema_version (version)
|
|
173
|
+
values (5)
|
|
174
|
+
on conflict ((true)) do update set version = excluded.version;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
create or replace function agent_board.issue_signing_key_challenge(
|
|
2
|
+
p_agent_id uuid,
|
|
3
|
+
p_key_id text,
|
|
4
|
+
p_expires_in interval default interval '5 minutes'
|
|
5
|
+
)
|
|
6
|
+
returns table (
|
|
7
|
+
challenge_id uuid,
|
|
8
|
+
agent_id uuid,
|
|
9
|
+
key_id text,
|
|
10
|
+
key_version integer,
|
|
11
|
+
fingerprint text,
|
|
12
|
+
challenge text,
|
|
13
|
+
expires_at timestamptz
|
|
14
|
+
)
|
|
15
|
+
language plpgsql
|
|
16
|
+
security definer
|
|
17
|
+
set search_path = agent_board, public
|
|
18
|
+
as $$
|
|
19
|
+
declare
|
|
20
|
+
v_actor uuid;
|
|
21
|
+
v_key agent_board.agent_signing_keys%rowtype;
|
|
22
|
+
v_challenge_id uuid := gen_random_uuid();
|
|
23
|
+
v_nonce text := encode(extensions.gen_random_bytes(24), 'base64');
|
|
24
|
+
v_expires_at timestamptz;
|
|
25
|
+
begin
|
|
26
|
+
if p_expires_in is null or p_expires_in <= interval '0 seconds' or p_expires_in > interval '15 minutes' then
|
|
27
|
+
raise exception 'challenge expiry must be between 0 and 15 minutes' using errcode = '22023';
|
|
28
|
+
end if;
|
|
29
|
+
|
|
30
|
+
v_actor := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
|
|
31
|
+
|
|
32
|
+
select *
|
|
33
|
+
into v_key
|
|
34
|
+
from agent_board.agent_signing_keys ask
|
|
35
|
+
where ask.agent_id = v_actor
|
|
36
|
+
and ask.key_id = p_key_id
|
|
37
|
+
for update;
|
|
38
|
+
|
|
39
|
+
if not found then
|
|
40
|
+
raise exception 'signing key not found' using errcode = 'P0002';
|
|
41
|
+
end if;
|
|
42
|
+
if v_key.status in ('revoked', 'compromised') then
|
|
43
|
+
raise exception 'key_revoked: signing key is revoked' using errcode = '22023';
|
|
44
|
+
end if;
|
|
45
|
+
if v_key.expires_at is not null and v_key.expires_at <= now() then
|
|
46
|
+
update agent_board.agent_signing_keys
|
|
47
|
+
set status = 'expired',
|
|
48
|
+
proof_state = 'expired',
|
|
49
|
+
updated_at = now()
|
|
50
|
+
where id = v_key.id;
|
|
51
|
+
raise exception 'key_expired: signing key is expired' using errcode = '22023';
|
|
52
|
+
end if;
|
|
53
|
+
|
|
54
|
+
v_expires_at := now() + p_expires_in;
|
|
55
|
+
|
|
56
|
+
insert into agent_board.agent_signing_key_challenges(
|
|
57
|
+
id,
|
|
58
|
+
signing_key_id,
|
|
59
|
+
agent_id,
|
|
60
|
+
challenge_nonce,
|
|
61
|
+
challenge_bytes,
|
|
62
|
+
expires_at,
|
|
63
|
+
metadata
|
|
64
|
+
)
|
|
65
|
+
values (
|
|
66
|
+
v_challenge_id,
|
|
67
|
+
v_key.id,
|
|
68
|
+
v_actor,
|
|
69
|
+
v_nonce,
|
|
70
|
+
'agent-signing-key-proof.v1:' || v_challenge_id::text || ':' || v_nonce,
|
|
71
|
+
v_expires_at,
|
|
72
|
+
jsonb_build_object('keyId', v_key.key_id, 'keyVersion', v_key.key_version)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
update agent_board.agent_signing_keys
|
|
76
|
+
set proof_state = 'challenge_issued',
|
|
77
|
+
updated_at = now()
|
|
78
|
+
where id = v_key.id
|
|
79
|
+
and proof_state <> 'proven';
|
|
80
|
+
|
|
81
|
+
return query
|
|
82
|
+
select
|
|
83
|
+
v_challenge_id,
|
|
84
|
+
v_actor,
|
|
85
|
+
v_key.key_id,
|
|
86
|
+
v_key.key_version,
|
|
87
|
+
v_key.fingerprint,
|
|
88
|
+
'agent-signing-key-proof.v1:' || v_challenge_id::text || ':' || v_nonce,
|
|
89
|
+
v_expires_at;
|
|
90
|
+
end;
|
|
91
|
+
$$;
|
|
92
|
+
|
|
93
|
+
create or replace function agent_board.record_signing_key_proof_result(
|
|
94
|
+
p_challenge_id uuid,
|
|
95
|
+
p_agent_id uuid,
|
|
96
|
+
p_key_id text,
|
|
97
|
+
p_verified boolean,
|
|
98
|
+
p_failure_class text default null,
|
|
99
|
+
p_metadata jsonb default '{}'::jsonb
|
|
100
|
+
)
|
|
101
|
+
returns table (
|
|
102
|
+
challenge_id uuid,
|
|
103
|
+
agent_id uuid,
|
|
104
|
+
key_id text,
|
|
105
|
+
key_version integer,
|
|
106
|
+
fingerprint text,
|
|
107
|
+
proof_state text,
|
|
108
|
+
failure_class text,
|
|
109
|
+
verified_at timestamptz
|
|
110
|
+
)
|
|
111
|
+
language plpgsql
|
|
112
|
+
security definer
|
|
113
|
+
set search_path = agent_board, public
|
|
114
|
+
as $$
|
|
115
|
+
declare
|
|
116
|
+
v_actor uuid;
|
|
117
|
+
v_challenge agent_board.agent_signing_key_challenges%rowtype;
|
|
118
|
+
v_key agent_board.agent_signing_keys%rowtype;
|
|
119
|
+
v_failure text;
|
|
120
|
+
v_verified_at timestamptz := now();
|
|
121
|
+
begin
|
|
122
|
+
v_actor := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
|
|
123
|
+
|
|
124
|
+
select *
|
|
125
|
+
into v_challenge
|
|
126
|
+
from agent_board.agent_signing_key_challenges c
|
|
127
|
+
where c.id = p_challenge_id
|
|
128
|
+
for update;
|
|
129
|
+
|
|
130
|
+
if not found then
|
|
131
|
+
raise exception 'signing key challenge not found' using errcode = 'P0002';
|
|
132
|
+
end if;
|
|
133
|
+
if v_challenge.agent_id <> v_actor then
|
|
134
|
+
raise exception 'challenge agent must match current_agent_id()' using errcode = '42501';
|
|
135
|
+
end if;
|
|
136
|
+
if v_challenge.proof_state <> 'pending' or v_challenge.consumed_at is not null then
|
|
137
|
+
raise exception 'nonce_replay: signing key challenge already consumed' using errcode = '23505';
|
|
138
|
+
end if;
|
|
139
|
+
|
|
140
|
+
select *
|
|
141
|
+
into v_key
|
|
142
|
+
from agent_board.agent_signing_keys ask
|
|
143
|
+
where ask.id = v_challenge.signing_key_id
|
|
144
|
+
and ask.agent_id = v_actor
|
|
145
|
+
and ask.key_id = p_key_id
|
|
146
|
+
for update;
|
|
147
|
+
|
|
148
|
+
if not found then
|
|
149
|
+
raise exception 'signing key not found for challenge' using errcode = 'P0002';
|
|
150
|
+
end if;
|
|
151
|
+
|
|
152
|
+
if v_challenge.expires_at <= now() then
|
|
153
|
+
update agent_board.agent_signing_key_challenges
|
|
154
|
+
set proof_state = 'expired',
|
|
155
|
+
failure_class = 'timestamp_out_of_window',
|
|
156
|
+
consumed_at = now(),
|
|
157
|
+
updated_at = now()
|
|
158
|
+
where id = p_challenge_id;
|
|
159
|
+
raise exception 'timestamp_out_of_window: signing key challenge expired' using errcode = '22023';
|
|
160
|
+
end if;
|
|
161
|
+
|
|
162
|
+
if v_key.status in ('revoked', 'compromised') then
|
|
163
|
+
v_failure := 'key_revoked';
|
|
164
|
+
elsif v_key.expires_at is not null and v_key.expires_at <= now() then
|
|
165
|
+
v_failure := 'key_expired';
|
|
166
|
+
elsif p_verified then
|
|
167
|
+
v_failure := null;
|
|
168
|
+
else
|
|
169
|
+
v_failure := coalesce(p_failure_class, 'signature_invalid');
|
|
170
|
+
end if;
|
|
171
|
+
|
|
172
|
+
if v_failure is not null and v_failure not in (
|
|
173
|
+
'unsupported_algorithm',
|
|
174
|
+
'signature_invalid',
|
|
175
|
+
'key_revoked',
|
|
176
|
+
'key_expired',
|
|
177
|
+
'timestamp_out_of_window',
|
|
178
|
+
'nonce_replay',
|
|
179
|
+
'actor_mismatch'
|
|
180
|
+
) then
|
|
181
|
+
raise exception 'unsupported proof failure class' using errcode = '22023';
|
|
182
|
+
end if;
|
|
183
|
+
|
|
184
|
+
update agent_board.agent_signing_key_challenges
|
|
185
|
+
set proof_state = case when v_failure is null then 'verified' else 'failed' end,
|
|
186
|
+
failure_class = v_failure,
|
|
187
|
+
consumed_at = v_verified_at,
|
|
188
|
+
metadata = coalesce(metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
|
|
189
|
+
updated_at = v_verified_at
|
|
190
|
+
where id = p_challenge_id;
|
|
191
|
+
|
|
192
|
+
update agent_board.agent_signing_keys
|
|
193
|
+
set status = case when v_failure is null then 'active' when v_failure = 'key_expired' then 'expired' else status end,
|
|
194
|
+
proof_state = case when v_failure is null then 'proven' when v_failure = 'key_expired' then 'expired' else 'failed' end,
|
|
195
|
+
updated_at = v_verified_at
|
|
196
|
+
where id = v_key.id;
|
|
197
|
+
|
|
198
|
+
return query
|
|
199
|
+
select
|
|
200
|
+
p_challenge_id,
|
|
201
|
+
v_actor,
|
|
202
|
+
v_key.key_id,
|
|
203
|
+
v_key.key_version,
|
|
204
|
+
v_key.fingerprint,
|
|
205
|
+
case when v_failure is null then 'proven' else 'failed' end,
|
|
206
|
+
v_failure,
|
|
207
|
+
case when v_failure is null then v_verified_at else null end;
|
|
208
|
+
end;
|
|
209
|
+
$$;
|
|
210
|
+
|
|
211
|
+
revoke all on function agent_board.issue_signing_key_challenge(uuid, text, interval) from public, anon;
|
|
212
|
+
revoke all on function agent_board.record_signing_key_proof_result(uuid, uuid, text, boolean, text, jsonb) from public, anon;
|
|
213
|
+
|
|
214
|
+
grant execute on function agent_board.issue_signing_key_challenge(uuid, text, interval) to authenticated, service_role;
|
|
215
|
+
grant execute on function agent_board.record_signing_key_proof_result(uuid, uuid, text, boolean, text, jsonb) to authenticated, service_role;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
alter table agent_board.agent_signing_keys enable row level security;
|
|
2
|
+
alter table agent_board.agent_signing_key_challenges enable row level security;
|
|
3
|
+
alter table agent_board.signature_evidence enable row level security;
|
|
4
|
+
alter table agent_board.signature_replay_state enable row level security;
|
|
5
|
+
|
|
6
|
+
do $$
|
|
7
|
+
declare
|
|
8
|
+
v_table text;
|
|
9
|
+
begin
|
|
10
|
+
foreach v_table in array array[
|
|
11
|
+
'agent_signing_keys',
|
|
12
|
+
'agent_signing_key_challenges',
|
|
13
|
+
'signature_evidence',
|
|
14
|
+
'signature_replay_state'
|
|
15
|
+
]
|
|
16
|
+
loop
|
|
17
|
+
execute format('drop policy if exists %I on agent_board.%I', 'agent_board_service_all_' || v_table, v_table);
|
|
18
|
+
execute format('create policy %I on agent_board.%I as permissive for all to service_role using (true) with check (true)', 'agent_board_service_all_' || v_table, v_table);
|
|
19
|
+
end loop;
|
|
20
|
+
end;
|
|
21
|
+
$$;
|
|
22
|
+
|
|
23
|
+
grant select on agent_board.agent_signing_keys,
|
|
24
|
+
agent_board.agent_signing_key_challenges,
|
|
25
|
+
agent_board.signature_evidence,
|
|
26
|
+
agent_board.signature_replay_state
|
|
27
|
+
to authenticated;
|
|
28
|
+
|
|
29
|
+
drop policy if exists agent_board_authenticated_select_agent_signing_keys on agent_board.agent_signing_keys;
|
|
30
|
+
create policy agent_board_authenticated_select_agent_signing_keys
|
|
31
|
+
on agent_board.agent_signing_keys
|
|
32
|
+
as permissive
|
|
33
|
+
for select
|
|
34
|
+
to authenticated
|
|
35
|
+
using (agent_id = agent_board.current_agent_id());
|
|
36
|
+
|
|
37
|
+
drop policy if exists agent_board_authenticated_select_agent_signing_key_challenges on agent_board.agent_signing_key_challenges;
|
|
38
|
+
create policy agent_board_authenticated_select_agent_signing_key_challenges
|
|
39
|
+
on agent_board.agent_signing_key_challenges
|
|
40
|
+
as permissive
|
|
41
|
+
for select
|
|
42
|
+
to authenticated
|
|
43
|
+
using (agent_id = agent_board.current_agent_id());
|
|
44
|
+
|
|
45
|
+
drop policy if exists agent_board_authenticated_select_signature_replay_state on agent_board.signature_replay_state;
|
|
46
|
+
create policy agent_board_authenticated_select_signature_replay_state
|
|
47
|
+
on agent_board.signature_replay_state
|
|
48
|
+
as permissive
|
|
49
|
+
for select
|
|
50
|
+
to authenticated
|
|
51
|
+
using (agent_id = agent_board.current_agent_id());
|
|
52
|
+
|
|
53
|
+
drop policy if exists agent_board_authenticated_select_signature_evidence on agent_board.signature_evidence;
|
|
54
|
+
create policy agent_board_authenticated_select_signature_evidence
|
|
55
|
+
on agent_board.signature_evidence
|
|
56
|
+
as permissive
|
|
57
|
+
for select
|
|
58
|
+
to authenticated
|
|
59
|
+
using (
|
|
60
|
+
agent_id = agent_board.current_agent_id()
|
|
61
|
+
or exists (
|
|
62
|
+
select 1
|
|
63
|
+
from agent_board.messages m
|
|
64
|
+
where m.id = source_message_id
|
|
65
|
+
and (
|
|
66
|
+
m.author_agent_id = agent_board.current_agent_id()
|
|
67
|
+
or exists (
|
|
68
|
+
select 1
|
|
69
|
+
from agent_board.delivery_recipients dr
|
|
70
|
+
where dr.source_message_id = m.id
|
|
71
|
+
and dr.recipient_agent_id = agent_board.current_agent_id()
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
or exists (
|
|
76
|
+
select 1
|
|
77
|
+
from agent_board.task_events te
|
|
78
|
+
join agent_board.tasks t on t.id = te.task_id
|
|
79
|
+
where te.id = source_task_event_id
|
|
80
|
+
and (
|
|
81
|
+
te.actor_agent_id = agent_board.current_agent_id()
|
|
82
|
+
or t.created_by_agent_id = agent_board.current_agent_id()
|
|
83
|
+
or t.assigned_agent_id = agent_board.current_agent_id()
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
or exists (
|
|
87
|
+
select 1
|
|
88
|
+
from agent_board.artifacts ar
|
|
89
|
+
where ar.id = source_artifact_id
|
|
90
|
+
and ar.created_by_agent_id = agent_board.current_agent_id()
|
|
91
|
+
)
|
|
92
|
+
or exists (
|
|
93
|
+
select 1
|
|
94
|
+
from agent_board.delivery_recipients dr
|
|
95
|
+
where dr.id = source_delivery_recipient_id
|
|
96
|
+
and dr.recipient_agent_id = agent_board.current_agent_id()
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
create or replace view agent_board.v_signature_evidence_status
|
|
101
|
+
with (security_invoker = true)
|
|
102
|
+
as
|
|
103
|
+
select
|
|
104
|
+
se.id as signature_evidence_id,
|
|
105
|
+
se.source_kind,
|
|
106
|
+
se.source_message_id,
|
|
107
|
+
se.source_task_event_id,
|
|
108
|
+
se.source_artifact_id,
|
|
109
|
+
se.source_delivery_recipient_id,
|
|
110
|
+
se.agent_id,
|
|
111
|
+
se.key_id,
|
|
112
|
+
se.key_version,
|
|
113
|
+
se.algorithm,
|
|
114
|
+
ask.fingerprint as key_fingerprint,
|
|
115
|
+
se.operation,
|
|
116
|
+
se.verification_status,
|
|
117
|
+
se.failure_class,
|
|
118
|
+
se.verified_at,
|
|
119
|
+
se.created_at
|
|
120
|
+
from agent_board.signature_evidence se
|
|
121
|
+
left join agent_board.agent_signing_keys ask on ask.id = se.signing_key_id;
|
|
122
|
+
|
|
123
|
+
create or replace view agent_board.v_thread_feed
|
|
124
|
+
with (security_invoker = true)
|
|
125
|
+
as
|
|
126
|
+
select
|
|
127
|
+
m.thread_id,
|
|
128
|
+
m.id as message_id,
|
|
129
|
+
m.message_sequence,
|
|
130
|
+
m.created_at as message_created_at,
|
|
131
|
+
m.author_agent_id,
|
|
132
|
+
a.handle as author_handle,
|
|
133
|
+
a.display_name as author_display_name,
|
|
134
|
+
m.message_type,
|
|
135
|
+
m.body,
|
|
136
|
+
m.task_id,
|
|
137
|
+
sig.verification_status,
|
|
138
|
+
sig.key_fingerprint,
|
|
139
|
+
sig.key_version,
|
|
140
|
+
sig.algorithm,
|
|
141
|
+
sig.verified_at,
|
|
142
|
+
sig.failure_class
|
|
143
|
+
from agent_board.messages m
|
|
144
|
+
join agent_board.agents a on a.id = m.author_agent_id
|
|
145
|
+
left join lateral (
|
|
146
|
+
select
|
|
147
|
+
se.verification_status,
|
|
148
|
+
ask.fingerprint as key_fingerprint,
|
|
149
|
+
se.key_version,
|
|
150
|
+
se.algorithm,
|
|
151
|
+
se.verified_at,
|
|
152
|
+
se.failure_class
|
|
153
|
+
from agent_board.signature_evidence se
|
|
154
|
+
left join agent_board.agent_signing_keys ask on ask.id = se.signing_key_id
|
|
155
|
+
where se.source_kind = 'message'
|
|
156
|
+
and se.source_message_id = m.id
|
|
157
|
+
order by se.created_at desc
|
|
158
|
+
limit 1
|
|
159
|
+
) sig on true;
|
|
160
|
+
|
|
161
|
+
create or replace view agent_board.v_task_tree
|
|
162
|
+
with (security_invoker = true)
|
|
163
|
+
as
|
|
164
|
+
select
|
|
165
|
+
coalesce(t.root_task_id, t.id) as root_task_id,
|
|
166
|
+
t.id as task_id,
|
|
167
|
+
t.parent_task_id,
|
|
168
|
+
t.thread_id,
|
|
169
|
+
t.created_by_agent_id,
|
|
170
|
+
t.assigned_agent_id,
|
|
171
|
+
assigned.handle as assigned_agent_handle,
|
|
172
|
+
t.status,
|
|
173
|
+
t.priority,
|
|
174
|
+
t.orchestration_mode,
|
|
175
|
+
t.title,
|
|
176
|
+
t.details,
|
|
177
|
+
t.created_at,
|
|
178
|
+
t.updated_at,
|
|
179
|
+
t.completed_at,
|
|
180
|
+
latest.event_sequence as latest_event_sequence,
|
|
181
|
+
latest.event_type as latest_event_type,
|
|
182
|
+
latest.payload->>'kind' as latest_event_kind,
|
|
183
|
+
sig.verification_status,
|
|
184
|
+
sig.key_fingerprint,
|
|
185
|
+
sig.key_version,
|
|
186
|
+
sig.algorithm,
|
|
187
|
+
sig.verified_at,
|
|
188
|
+
sig.failure_class
|
|
189
|
+
from agent_board.tasks t
|
|
190
|
+
left join agent_board.agents assigned on assigned.id = t.assigned_agent_id
|
|
191
|
+
left join lateral (
|
|
192
|
+
select te.id, te.event_sequence, te.event_type, te.payload
|
|
193
|
+
from agent_board.task_events te
|
|
194
|
+
where te.task_id = t.id
|
|
195
|
+
order by te.event_sequence desc
|
|
196
|
+
limit 1
|
|
197
|
+
) latest on true
|
|
198
|
+
left join lateral (
|
|
199
|
+
select
|
|
200
|
+
se.verification_status,
|
|
201
|
+
ask.fingerprint as key_fingerprint,
|
|
202
|
+
se.key_version,
|
|
203
|
+
se.algorithm,
|
|
204
|
+
se.verified_at,
|
|
205
|
+
se.failure_class
|
|
206
|
+
from agent_board.signature_evidence se
|
|
207
|
+
left join agent_board.agent_signing_keys ask on ask.id = se.signing_key_id
|
|
208
|
+
where se.source_kind = 'task_event'
|
|
209
|
+
and se.source_task_event_id = latest.id
|
|
210
|
+
order by se.created_at desc
|
|
211
|
+
limit 1
|
|
212
|
+
) sig on true;
|
|
213
|
+
|
|
214
|
+
create or replace view agent_board.v_agent_dashboard
|
|
215
|
+
with (security_invoker = true)
|
|
216
|
+
as
|
|
217
|
+
select
|
|
218
|
+
a.id as agent_id,
|
|
219
|
+
a.handle,
|
|
220
|
+
a.display_name,
|
|
221
|
+
a.status as participant_status,
|
|
222
|
+
p.availability,
|
|
223
|
+
p.thread_id as presence_thread_id,
|
|
224
|
+
p.last_heartbeat_at,
|
|
225
|
+
p.expires_at as presence_expires_at,
|
|
226
|
+
coalesce(open_tasks.open_task_count, 0)::bigint as open_task_count,
|
|
227
|
+
coalesce(active_claims.active_claim_count, 0)::bigint as active_claim_count,
|
|
228
|
+
coalesce(open_escalations.assigned_open_escalation_count, 0)::bigint as assigned_open_escalation_count,
|
|
229
|
+
latest.task_id as latest_task_id,
|
|
230
|
+
latest.event_sequence as latest_task_event_sequence,
|
|
231
|
+
latest.event_type as latest_task_event_type,
|
|
232
|
+
latest.payload->>'kind' as latest_task_event_kind,
|
|
233
|
+
latest.updated_at as latest_task_updated_at,
|
|
234
|
+
coalesce(inbox_counts.inbox_unread_count, 0)::bigint as inbox_unread_count,
|
|
235
|
+
coalesce(inbox_counts.inbox_pending_action_count, 0)::bigint as inbox_pending_action_count,
|
|
236
|
+
latest_sig.verification_status as latest_signature_verification_status,
|
|
237
|
+
latest_sig.key_fingerprint as latest_signature_key_fingerprint,
|
|
238
|
+
latest_sig.key_version as latest_signature_key_version,
|
|
239
|
+
latest_sig.algorithm as latest_signature_algorithm,
|
|
240
|
+
latest_sig.verified_at as latest_signature_verified_at,
|
|
241
|
+
latest_sig.failure_class as latest_signature_failure_class
|
|
242
|
+
from agent_board.agents a
|
|
243
|
+
left join agent_board.presence p on p.agent_id = a.id
|
|
244
|
+
left join lateral (
|
|
245
|
+
select count(*) as open_task_count
|
|
246
|
+
from agent_board.tasks t
|
|
247
|
+
where t.assigned_agent_id = a.id
|
|
248
|
+
and t.status in ('pending', 'in_progress', 'blocked')
|
|
249
|
+
) open_tasks on true
|
|
250
|
+
left join lateral (
|
|
251
|
+
select count(*) as active_claim_count
|
|
252
|
+
from agent_board.claims c
|
|
253
|
+
where c.agent_id = a.id
|
|
254
|
+
and c.status = 'active'
|
|
255
|
+
and c.released_at is null
|
|
256
|
+
and c.lease_expires_at > now()
|
|
257
|
+
) active_claims on true
|
|
258
|
+
left join lateral (
|
|
259
|
+
select count(*) as assigned_open_escalation_count
|
|
260
|
+
from agent_board.escalations e
|
|
261
|
+
where e.assigned_agent_id = a.id
|
|
262
|
+
and e.status in ('open', 'acknowledged')
|
|
263
|
+
) open_escalations on true
|
|
264
|
+
left join lateral (
|
|
265
|
+
select
|
|
266
|
+
count(*) filter (where ii.action_state in ('new', 'restored')) as inbox_unread_count,
|
|
267
|
+
count(*) filter (where ii.action_state in ('new', 'restored', 'deferred')) as inbox_pending_action_count
|
|
268
|
+
from agent_board.inbox_items ii
|
|
269
|
+
where ii.recipient_agent_id = a.id
|
|
270
|
+
and ii.visible_in_inbox = true
|
|
271
|
+
) inbox_counts on true
|
|
272
|
+
left join lateral (
|
|
273
|
+
select t.id as task_id, te.id as task_event_id, te.event_sequence, te.event_type, te.payload, t.updated_at
|
|
274
|
+
from agent_board.tasks t
|
|
275
|
+
left join agent_board.task_events te on te.task_id = t.id
|
|
276
|
+
where t.assigned_agent_id = a.id or t.created_by_agent_id = a.id
|
|
277
|
+
order by t.updated_at desc, te.event_sequence desc nulls last
|
|
278
|
+
limit 1
|
|
279
|
+
) latest on true
|
|
280
|
+
left join lateral (
|
|
281
|
+
select
|
|
282
|
+
se.verification_status,
|
|
283
|
+
ask.fingerprint as key_fingerprint,
|
|
284
|
+
se.key_version,
|
|
285
|
+
se.algorithm,
|
|
286
|
+
se.verified_at,
|
|
287
|
+
se.failure_class
|
|
288
|
+
from agent_board.signature_evidence se
|
|
289
|
+
left join agent_board.agent_signing_keys ask on ask.id = se.signing_key_id
|
|
290
|
+
where se.source_kind = 'task_event'
|
|
291
|
+
and se.source_task_event_id = latest.task_event_id
|
|
292
|
+
order by se.created_at desc
|
|
293
|
+
limit 1
|
|
294
|
+
) latest_sig on true;
|
|
295
|
+
|
|
296
|
+
grant select on agent_board.v_signature_evidence_status,
|
|
297
|
+
agent_board.v_thread_feed,
|
|
298
|
+
agent_board.v_task_tree,
|
|
299
|
+
agent_board.v_agent_dashboard
|
|
300
|
+
to authenticated, service_role;
|