@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,155 @@
|
|
|
1
|
+
create or replace function agent_board.current_agent_id()
|
|
2
|
+
returns uuid
|
|
3
|
+
language sql
|
|
4
|
+
stable
|
|
5
|
+
security definer
|
|
6
|
+
set search_path = ''
|
|
7
|
+
as $$
|
|
8
|
+
with credential_claim as (
|
|
9
|
+
select nullif(current_setting('request.jwt.claim.agent_credential_id', true), '') as credential_id_text
|
|
10
|
+
)
|
|
11
|
+
select a.id
|
|
12
|
+
from agent_board.agents a
|
|
13
|
+
cross join credential_claim cc
|
|
14
|
+
where a.auth_user_id = auth.uid()
|
|
15
|
+
and a.status = 'active'
|
|
16
|
+
and (
|
|
17
|
+
cc.credential_id_text is null
|
|
18
|
+
or (
|
|
19
|
+
cc.credential_id_text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
20
|
+
and exists (
|
|
21
|
+
select 1
|
|
22
|
+
from agent_board.agent_credentials ac
|
|
23
|
+
where ac.id = cc.credential_id_text::uuid
|
|
24
|
+
and ac.agent_id = a.id
|
|
25
|
+
and ac.revoked_at is null
|
|
26
|
+
and (ac.expires_at is null or ac.expires_at > now())
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
limit 1;
|
|
31
|
+
$$;
|
|
32
|
+
|
|
33
|
+
comment on function agent_board.current_agent_id() is 'Resolves auth.uid() to the active agent_board.agents.id and, when present, requires the JWT agent_credential_id claim to reference a non-revoked, non-expired credential for that agent.';
|
|
34
|
+
|
|
35
|
+
grant usage on schema agent_board to service_role;
|
|
36
|
+
grant select on agent_board.agents to service_role;
|
|
37
|
+
grant select, update(last_used_at, updated_at) on agent_board.agent_credentials to service_role;
|
|
38
|
+
|
|
39
|
+
create or replace function agent_board._signed_write_actor_id(
|
|
40
|
+
p_payload jsonb,
|
|
41
|
+
p_gateway_decision jsonb,
|
|
42
|
+
p_actor_field text
|
|
43
|
+
)
|
|
44
|
+
returns uuid
|
|
45
|
+
language plpgsql
|
|
46
|
+
security definer
|
|
47
|
+
set search_path = agent_board, public
|
|
48
|
+
as $$
|
|
49
|
+
declare
|
|
50
|
+
v_actor uuid;
|
|
51
|
+
v_payload_actor uuid;
|
|
52
|
+
begin
|
|
53
|
+
v_actor := nullif(p_gateway_decision ->> 'agent_id', '')::uuid;
|
|
54
|
+
if v_actor is null then
|
|
55
|
+
raise exception 'actor_mismatch: signed write gateway actor is required' using errcode = '42501';
|
|
56
|
+
end if;
|
|
57
|
+
|
|
58
|
+
v_payload_actor := nullif(p_payload ->> p_actor_field, '')::uuid;
|
|
59
|
+
if v_payload_actor is not null and v_payload_actor <> v_actor then
|
|
60
|
+
raise exception 'actor_mismatch: signed write payload actor must match gateway actor' using errcode = '42501';
|
|
61
|
+
end if;
|
|
62
|
+
|
|
63
|
+
return v_actor;
|
|
64
|
+
end;
|
|
65
|
+
$$;
|
|
66
|
+
|
|
67
|
+
create or replace function agent_board._record_signed_write_evidence(
|
|
68
|
+
p_source_kind text,
|
|
69
|
+
p_source_id uuid,
|
|
70
|
+
p_operation text,
|
|
71
|
+
p_payload jsonb,
|
|
72
|
+
p_gateway_decision jsonb
|
|
73
|
+
)
|
|
74
|
+
returns uuid
|
|
75
|
+
language plpgsql
|
|
76
|
+
security definer
|
|
77
|
+
set search_path = agent_board, public
|
|
78
|
+
as $$
|
|
79
|
+
declare
|
|
80
|
+
v_evidence_id uuid;
|
|
81
|
+
v_agent_id uuid := nullif(p_gateway_decision ->> 'agent_id', '')::uuid;
|
|
82
|
+
v_status text := coalesce(nullif(p_gateway_decision ->> 'verification_status', ''), 'unsigned');
|
|
83
|
+
begin
|
|
84
|
+
if v_agent_id is null then
|
|
85
|
+
raise exception 'actor_mismatch: signed write evidence requires gateway actor' using errcode = '42501';
|
|
86
|
+
end if;
|
|
87
|
+
|
|
88
|
+
if v_status not in ('unsigned', 'observed', 'warn', 'verified', 'failed') then
|
|
89
|
+
v_status := 'failed';
|
|
90
|
+
end if;
|
|
91
|
+
|
|
92
|
+
insert into agent_board.signature_evidence(
|
|
93
|
+
agent_id,
|
|
94
|
+
signing_key_id,
|
|
95
|
+
key_id,
|
|
96
|
+
key_version,
|
|
97
|
+
algorithm,
|
|
98
|
+
source_kind,
|
|
99
|
+
source_message_id,
|
|
100
|
+
source_task_event_id,
|
|
101
|
+
operation,
|
|
102
|
+
payload_digest,
|
|
103
|
+
canonicalization_version,
|
|
104
|
+
signature_digest,
|
|
105
|
+
verification_status,
|
|
106
|
+
failure_class,
|
|
107
|
+
signed_at,
|
|
108
|
+
verified_at,
|
|
109
|
+
nonce,
|
|
110
|
+
idempotency_key,
|
|
111
|
+
external_evidence_ref,
|
|
112
|
+
metadata
|
|
113
|
+
)
|
|
114
|
+
values (
|
|
115
|
+
v_agent_id,
|
|
116
|
+
nullif(p_gateway_decision ->> 'signing_key_id', '')::uuid,
|
|
117
|
+
nullif(p_gateway_decision ->> 'key_id', ''),
|
|
118
|
+
nullif(p_gateway_decision ->> 'key_version', '')::integer,
|
|
119
|
+
coalesce(nullif(p_gateway_decision ->> 'algorithm', ''), 'Ed25519'),
|
|
120
|
+
p_source_kind,
|
|
121
|
+
case when p_source_kind = 'message' then p_source_id else null end,
|
|
122
|
+
case when p_source_kind = 'task_event' then p_source_id else null end,
|
|
123
|
+
p_operation,
|
|
124
|
+
coalesce(nullif(p_gateway_decision ->> 'payload_digest', ''), 'sha256:unsigned'),
|
|
125
|
+
coalesce(nullif(p_gateway_decision ->> 'canonicalization_version', ''), 'agent-provenance-jcs.v1'),
|
|
126
|
+
nullif(p_gateway_decision ->> 'signature_digest', ''),
|
|
127
|
+
v_status,
|
|
128
|
+
nullif(p_gateway_decision ->> 'failure_class', ''),
|
|
129
|
+
nullif(p_gateway_decision ->> 'signed_at', '')::timestamptz,
|
|
130
|
+
nullif(p_gateway_decision ->> 'verified_at', '')::timestamptz,
|
|
131
|
+
nullif(p_gateway_decision ->> 'nonce', ''),
|
|
132
|
+
nullif(p_gateway_decision ->> 'idempotency_key', ''),
|
|
133
|
+
nullif(p_gateway_decision ->> 'external_evidence_ref', ''),
|
|
134
|
+
jsonb_build_object(
|
|
135
|
+
'gateway', 'signed-write',
|
|
136
|
+
'authbound', true,
|
|
137
|
+
'enforcementMode', coalesce(p_gateway_decision ->> 'enforcement_mode', 'observe'),
|
|
138
|
+
'redacted', true,
|
|
139
|
+
'metadata-safe', true
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
returning id into v_evidence_id;
|
|
143
|
+
|
|
144
|
+
update agent_board.signature_replay_state
|
|
145
|
+
set signature_evidence_id = v_evidence_id
|
|
146
|
+
where agent_id = v_agent_id
|
|
147
|
+
and operation = p_operation
|
|
148
|
+
and (
|
|
149
|
+
(nonce is not null and nonce = nullif(p_gateway_decision ->> 'nonce', ''))
|
|
150
|
+
or (idempotency_key is not null and idempotency_key = nullif(p_gateway_decision ->> 'idempotency_key', ''))
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return v_evidence_id;
|
|
154
|
+
end;
|
|
155
|
+
$$;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
create or replace function agent_board.get_schema_version()
|
|
2
|
+
returns table(version integer)
|
|
3
|
+
language sql
|
|
4
|
+
stable
|
|
5
|
+
security definer
|
|
6
|
+
set search_path = agent_board, pg_temp
|
|
7
|
+
as $$
|
|
8
|
+
select schema_version.version
|
|
9
|
+
from agent_board.schema_version
|
|
10
|
+
limit 1;
|
|
11
|
+
$$;
|
|
12
|
+
|
|
13
|
+
revoke all on function agent_board.get_schema_version() from public;
|
|
14
|
+
grant execute on function agent_board.get_schema_version() to authenticated, service_role;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- Non-fleet identity tables for delegation, workload exchange, and invitation APIs.
|
|
2
|
+
-- Forward-only: do not apply linked or production migrations in NONFLEET phase.
|
|
3
|
+
|
|
4
|
+
create table if not exists agent_board.nonfleet_sessions (
|
|
5
|
+
id text primary key,
|
|
6
|
+
session_id text not null,
|
|
7
|
+
identity_class text not null check (identity_class in ('ephemeral_child', 'hosted_workload', 'remote_sandbox', 'invited_external')),
|
|
8
|
+
agent_id uuid references agent_board.agents(id),
|
|
9
|
+
parent_agent_id uuid references agent_board.agents(id),
|
|
10
|
+
parent_credential_id uuid references agent_board.agent_credentials(id),
|
|
11
|
+
issuer text,
|
|
12
|
+
audience text,
|
|
13
|
+
repository text,
|
|
14
|
+
workflow_or_service text,
|
|
15
|
+
trust_domain text,
|
|
16
|
+
invitation_id text,
|
|
17
|
+
board_scope text,
|
|
18
|
+
repo_scope text,
|
|
19
|
+
task_scope text,
|
|
20
|
+
supervisor_allowed boolean not null default false,
|
|
21
|
+
elevated_admin_allowed boolean not null default false,
|
|
22
|
+
max_ttl_seconds integer not null default 3600,
|
|
23
|
+
issued_at timestamptz not null default now(),
|
|
24
|
+
expires_at timestamptz not null,
|
|
25
|
+
revoked_at timestamptz,
|
|
26
|
+
created_at timestamptz not null default now()
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
alter table agent_board.nonfleet_sessions
|
|
30
|
+
add column if not exists session_id text,
|
|
31
|
+
add column if not exists parent_credential_id uuid references agent_board.agent_credentials(id),
|
|
32
|
+
add column if not exists invitation_id text,
|
|
33
|
+
add column if not exists supervisor_allowed boolean not null default false,
|
|
34
|
+
add column if not exists elevated_admin_allowed boolean not null default false;
|
|
35
|
+
|
|
36
|
+
alter table agent_board.nonfleet_sessions
|
|
37
|
+
alter column parent_credential_id type uuid using nullif(parent_credential_id::text, '')::uuid;
|
|
38
|
+
|
|
39
|
+
create unique index if not exists idx_nonfleet_sessions_session_id on agent_board.nonfleet_sessions(session_id);
|
|
40
|
+
create index if not exists idx_nonfleet_sessions_identity_class on agent_board.nonfleet_sessions(identity_class);
|
|
41
|
+
create index if not exists idx_nonfleet_sessions_expires_at on agent_board.nonfleet_sessions(expires_at) where revoked_at is null;
|
|
42
|
+
|
|
43
|
+
alter table agent_board.nonfleet_sessions enable row level security;
|
|
44
|
+
drop policy if exists nonfleet_sessions_deny_anon on agent_board.nonfleet_sessions;
|
|
45
|
+
drop policy if exists nonfleet_sessions_authenticated_read on agent_board.nonfleet_sessions;
|
|
46
|
+
create policy nonfleet_sessions_deny_anon on agent_board.nonfleet_sessions for all to anon using (false);
|
|
47
|
+
create policy nonfleet_sessions_authenticated_read on agent_board.nonfleet_sessions for select to authenticated
|
|
48
|
+
using (agent_id = agent_board.current_agent_id() or parent_agent_id = agent_board.current_agent_id());
|
|
49
|
+
|
|
50
|
+
create table if not exists agent_board.nonfleet_invitations (
|
|
51
|
+
id text primary key,
|
|
52
|
+
trust_domain text not null,
|
|
53
|
+
invited_by_agent_id uuid not null references agent_board.agents(id),
|
|
54
|
+
accepting_agent_id uuid references agent_board.agents(id),
|
|
55
|
+
revoked_by_agent_id uuid references agent_board.agents(id),
|
|
56
|
+
board_scope text,
|
|
57
|
+
receive_policy text,
|
|
58
|
+
signing_posture text not null default 'require_for_external',
|
|
59
|
+
lifecycle_state text not null default 'pending' check (lifecycle_state in ('pending', 'accepted', 'expired', 'revoked', 'rejected')),
|
|
60
|
+
accepted_at timestamptz,
|
|
61
|
+
expires_at timestamptz not null,
|
|
62
|
+
revoked_at timestamptz,
|
|
63
|
+
created_at timestamptz not null default now()
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
alter table agent_board.nonfleet_invitations
|
|
67
|
+
add column if not exists invited_by_agent_id uuid references agent_board.agents(id),
|
|
68
|
+
add column if not exists revoked_by_agent_id uuid references agent_board.agents(id),
|
|
69
|
+
add column if not exists accepted_at timestamptz;
|
|
70
|
+
|
|
71
|
+
alter table agent_board.nonfleet_invitations drop column if exists invited_by;
|
|
72
|
+
|
|
73
|
+
create index if not exists idx_nonfleet_invitations_lifecycle on agent_board.nonfleet_invitations(lifecycle_state) where lifecycle_state = 'pending';
|
|
74
|
+
|
|
75
|
+
alter table agent_board.nonfleet_invitations enable row level security;
|
|
76
|
+
drop policy if exists nonfleet_invitations_deny_anon on agent_board.nonfleet_invitations;
|
|
77
|
+
drop policy if exists nonfleet_invitations_authenticated_read on agent_board.nonfleet_invitations;
|
|
78
|
+
create policy nonfleet_invitations_deny_anon on agent_board.nonfleet_invitations for all to anon using (false);
|
|
79
|
+
create policy nonfleet_invitations_authenticated_read on agent_board.nonfleet_invitations for select to authenticated
|
|
80
|
+
using (invited_by_agent_id = agent_board.current_agent_id() or accepting_agent_id = agent_board.current_agent_id());
|
|
81
|
+
|
|
82
|
+
revoke all on agent_board.nonfleet_sessions, agent_board.nonfleet_invitations from public, anon, authenticated;
|
|
83
|
+
grant select on agent_board.nonfleet_sessions, agent_board.nonfleet_invitations to authenticated;
|
|
84
|
+
grant select, insert, update on agent_board.nonfleet_sessions, agent_board.nonfleet_invitations to service_role;
|
|
85
|
+
|
|
86
|
+
comment on table agent_board.nonfleet_sessions is 'Short-lived sessions for non-fleet identity classes: ephemeral children, hosted workloads, remote sandboxes, and invited external agents.';
|
|
87
|
+
comment on table agent_board.nonfleet_invitations is 'Trust-domain invitations for explicitly invited third-party agents.';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- Workload provider registry for non-fleet identity exchange.
|
|
2
|
+
-- Forward-only: do not apply linked or production migrations in NONFLEET phase.
|
|
3
|
+
|
|
4
|
+
create table if not exists agent_board.workload_providers (
|
|
5
|
+
id text primary key default gen_random_uuid()::text,
|
|
6
|
+
issuer text not null,
|
|
7
|
+
audience text not null,
|
|
8
|
+
repository_claim text not null default 'repository',
|
|
9
|
+
max_session_ttl_seconds integer not null default 3600,
|
|
10
|
+
callback_support boolean not null default false,
|
|
11
|
+
pull_support boolean not null default true,
|
|
12
|
+
supported boolean not null default true,
|
|
13
|
+
policy_approved boolean not null default false,
|
|
14
|
+
allowed_repositories text[] not null default '{}',
|
|
15
|
+
allowed_workflows text[] not null default '{}',
|
|
16
|
+
allowed_board_scopes text[] not null default '{}',
|
|
17
|
+
created_at timestamptz not null default now(),
|
|
18
|
+
unique (issuer, audience)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
alter table agent_board.workload_providers
|
|
22
|
+
add column if not exists policy_approved boolean not null default false,
|
|
23
|
+
add column if not exists allowed_repositories text[] not null default '{}',
|
|
24
|
+
add column if not exists allowed_workflows text[] not null default '{}',
|
|
25
|
+
add column if not exists allowed_board_scopes text[] not null default '{}';
|
|
26
|
+
|
|
27
|
+
insert into agent_board.workload_providers (issuer, audience, repository_claim, max_session_ttl_seconds, callback_support, pull_support, supported)
|
|
28
|
+
values
|
|
29
|
+
('https://token.actions.githubusercontent.com', 'message-board', 'repository', 3600, true, true, true),
|
|
30
|
+
('https://accounts.google.com', 'message-board', 'project_id', 3600, false, true, true),
|
|
31
|
+
('https://sts.windows.net', 'message-board', 'unknown', 0, false, false, false),
|
|
32
|
+
('https://login.microsoftonline.com', 'message-board', 'unknown', 0, false, false, false)
|
|
33
|
+
on conflict (issuer, audience) do nothing;
|
|
34
|
+
|
|
35
|
+
comment on table agent_board.workload_providers is 'Registry of supported and explicitly unsupported workload identity providers for non-fleet exchange.';
|
|
36
|
+
|
|
37
|
+
revoke all on agent_board.workload_providers from public, anon, authenticated;
|
|
38
|
+
grant select on agent_board.workload_providers to authenticated;
|
|
39
|
+
grant select, insert, update on agent_board.workload_providers to service_role;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
-- portal#203 — provision_agent must fail closed for system principals.
|
|
2
|
+
--
|
|
3
|
+
-- `agent_board.provision_agent` previously minted an auth user and issued a
|
|
4
|
+
-- refresh credential for `p_kind = 'system'`, but `trg_sync_agent_principal_columns`
|
|
5
|
+
-- (20260511000000) clears `auth_user_id` for system principals and the
|
|
6
|
+
-- `agents_system_owner_shape` CHECK enforces it. The credential therefore can
|
|
7
|
+
-- never bind an auth session, so `/api/agent-board/auth/exchange` returns
|
|
8
|
+
-- `502 refresh_failed` for a token the RPC reported as valid.
|
|
9
|
+
--
|
|
10
|
+
-- Contract (issue option 1): reject credential issuance for system principals
|
|
11
|
+
-- BEFORE minting anything. The raise sits ahead of `_ensure_agent_auth_user`
|
|
12
|
+
-- and every insert, so no orphan `auth.users` row, agent row, or credential is
|
|
13
|
+
-- created. System principals continue to exist as records created outside this
|
|
14
|
+
-- credential-minting RPC (direct insert / migration); they simply cannot be
|
|
15
|
+
-- issued exchangeable credentials.
|
|
16
|
+
--
|
|
17
|
+
-- Signature is byte-identical to the original definition so this replaces the
|
|
18
|
+
-- function in place (grants survive CREATE OR REPLACE; no re-grant needed).
|
|
19
|
+
--
|
|
20
|
+
-- Defense in depth: the same un-exchangeable-credential class is also reachable
|
|
21
|
+
-- through `rotate_agent_token` (and any future issuer), which likewise calls
|
|
22
|
+
-- `_issue_agent_credential`. Rather than guard each entry point, reject system
|
|
23
|
+
-- principals centrally in `_issue_agent_credential` — the single choke point
|
|
24
|
+
-- every issuer funnels through — while keeping the earlier, clearer rejection in
|
|
25
|
+
-- `provision_agent` (which also avoids creating an auth.users row up front).
|
|
26
|
+
|
|
27
|
+
create or replace function agent_board._issue_agent_credential(
|
|
28
|
+
p_agent_id uuid,
|
|
29
|
+
p_credential_label text,
|
|
30
|
+
p_replaces_credential_id uuid default null,
|
|
31
|
+
p_metadata jsonb default '{}'::jsonb
|
|
32
|
+
)
|
|
33
|
+
returns table(
|
|
34
|
+
credential_id uuid,
|
|
35
|
+
refresh_token text,
|
|
36
|
+
expires_at timestamptz,
|
|
37
|
+
created_at timestamptz,
|
|
38
|
+
replaces_credential_id uuid
|
|
39
|
+
)
|
|
40
|
+
language plpgsql
|
|
41
|
+
security definer
|
|
42
|
+
set search_path = ''
|
|
43
|
+
as $$
|
|
44
|
+
declare
|
|
45
|
+
v_refresh_token text;
|
|
46
|
+
v_expires_at timestamptz := now() + interval '30 days';
|
|
47
|
+
v_created_at timestamptz := now();
|
|
48
|
+
v_credential_id uuid;
|
|
49
|
+
begin
|
|
50
|
+
-- Fail closed for system principals. They have no auth binding (the sync
|
|
51
|
+
-- trigger clears auth_user_id and agents_system_owner_shape enforces it), so
|
|
52
|
+
-- any credential minted here is deterministically un-exchangeable. Rejecting
|
|
53
|
+
-- centrally covers provision_agent, rotate_agent_token, and any future issuer.
|
|
54
|
+
if exists (
|
|
55
|
+
select 1
|
|
56
|
+
from agent_board.agents a
|
|
57
|
+
where a.id = p_agent_id
|
|
58
|
+
and a.kind = 'system'::agent_board.agent_kind
|
|
59
|
+
) then
|
|
60
|
+
raise exception 'cannot issue an exchangeable credential for a system principal (no auth binding)'
|
|
61
|
+
using errcode = '22023';
|
|
62
|
+
end if;
|
|
63
|
+
|
|
64
|
+
v_refresh_token := agent_board._mint_agent_refresh_token();
|
|
65
|
+
|
|
66
|
+
insert into agent_board.agent_credentials(
|
|
67
|
+
agent_id,
|
|
68
|
+
refresh_token_hash,
|
|
69
|
+
hash_version,
|
|
70
|
+
credential_label,
|
|
71
|
+
expires_at,
|
|
72
|
+
replaces_credential_id,
|
|
73
|
+
metadata,
|
|
74
|
+
created_at,
|
|
75
|
+
updated_at
|
|
76
|
+
)
|
|
77
|
+
values (
|
|
78
|
+
p_agent_id,
|
|
79
|
+
extensions.digest(v_refresh_token, 'sha256'),
|
|
80
|
+
1,
|
|
81
|
+
p_credential_label,
|
|
82
|
+
v_expires_at,
|
|
83
|
+
p_replaces_credential_id,
|
|
84
|
+
coalesce(p_metadata, '{}'::jsonb),
|
|
85
|
+
v_created_at,
|
|
86
|
+
v_created_at
|
|
87
|
+
)
|
|
88
|
+
returning id into v_credential_id;
|
|
89
|
+
|
|
90
|
+
if p_replaces_credential_id is not null then
|
|
91
|
+
update agent_board.agent_credentials
|
|
92
|
+
set revoked_at = coalesce(revoked_at, v_created_at),
|
|
93
|
+
rotated_at = coalesce(rotated_at, v_created_at),
|
|
94
|
+
replaced_by_credential_id = v_credential_id,
|
|
95
|
+
updated_at = v_created_at
|
|
96
|
+
where id = p_replaces_credential_id;
|
|
97
|
+
end if;
|
|
98
|
+
|
|
99
|
+
return query
|
|
100
|
+
select v_credential_id, v_refresh_token, v_expires_at, v_created_at, p_replaces_credential_id;
|
|
101
|
+
end;
|
|
102
|
+
$$;
|
|
103
|
+
|
|
104
|
+
create or replace function agent_board.provision_agent(
|
|
105
|
+
p_slug text,
|
|
106
|
+
p_kind text,
|
|
107
|
+
p_scopes jsonb default '[]'::jsonb,
|
|
108
|
+
p_owner_user_id uuid default null
|
|
109
|
+
)
|
|
110
|
+
returns table(
|
|
111
|
+
agent_id uuid,
|
|
112
|
+
auth_user_id uuid,
|
|
113
|
+
credential_id uuid,
|
|
114
|
+
refresh_token text,
|
|
115
|
+
expires_at timestamptz,
|
|
116
|
+
created_at timestamptz
|
|
117
|
+
)
|
|
118
|
+
language plpgsql
|
|
119
|
+
security definer
|
|
120
|
+
set search_path = agent_board, public
|
|
121
|
+
as $$
|
|
122
|
+
declare
|
|
123
|
+
v_slug text;
|
|
124
|
+
v_kind text;
|
|
125
|
+
v_owner_user_id uuid;
|
|
126
|
+
v_admin_user_id uuid;
|
|
127
|
+
v_auth_user_id uuid;
|
|
128
|
+
v_agent agent_board.agents%rowtype;
|
|
129
|
+
v_credential record;
|
|
130
|
+
begin
|
|
131
|
+
v_admin_user_id := agent_board._require_platform_admin_user_id();
|
|
132
|
+
v_slug := agent_board._normalize_provision_slug(p_slug);
|
|
133
|
+
v_kind := lower(coalesce(trim(p_kind), ''));
|
|
134
|
+
v_owner_user_id := p_owner_user_id;
|
|
135
|
+
|
|
136
|
+
if v_kind not in ('human', 'agent', 'system') then
|
|
137
|
+
raise exception 'kind must be one of human, agent, or system' using errcode = '22023';
|
|
138
|
+
end if;
|
|
139
|
+
|
|
140
|
+
-- Fail closed: system principals have no auth binding (the sync trigger and
|
|
141
|
+
-- agents_system_owner_shape CHECK force auth_user_id to NULL), so any
|
|
142
|
+
-- credential minted here is deterministically un-exchangeable. Reject before
|
|
143
|
+
-- minting rather than returning an unusable-but-valid-looking token.
|
|
144
|
+
if v_kind = 'system' then
|
|
145
|
+
raise exception 'system principals cannot be issued exchangeable credentials; provision_agent does not support kind=system'
|
|
146
|
+
using errcode = '22023';
|
|
147
|
+
elsif v_owner_user_id is null then
|
|
148
|
+
raise exception 'owner_user_id is required for human and agent principals' using errcode = '22023';
|
|
149
|
+
elsif not exists (select 1 from auth.users where id = v_owner_user_id) then
|
|
150
|
+
raise exception 'owner user not found' using errcode = 'P0002';
|
|
151
|
+
end if;
|
|
152
|
+
|
|
153
|
+
select *
|
|
154
|
+
into v_agent
|
|
155
|
+
from agent_board.agents
|
|
156
|
+
where handle = v_slug
|
|
157
|
+
limit 1
|
|
158
|
+
for update;
|
|
159
|
+
|
|
160
|
+
v_auth_user_id := coalesce(v_agent.auth_user_id, agent_board._ensure_agent_auth_user(v_slug));
|
|
161
|
+
|
|
162
|
+
if v_agent.id is null then
|
|
163
|
+
insert into agent_board.agents(
|
|
164
|
+
agent_type,
|
|
165
|
+
kind,
|
|
166
|
+
handle,
|
|
167
|
+
display_name,
|
|
168
|
+
status,
|
|
169
|
+
auth_user_id,
|
|
170
|
+
owner_user_id,
|
|
171
|
+
metadata
|
|
172
|
+
)
|
|
173
|
+
values (
|
|
174
|
+
v_kind,
|
|
175
|
+
v_kind::agent_board.agent_kind,
|
|
176
|
+
v_slug,
|
|
177
|
+
agent_board._default_provision_display_name(v_slug),
|
|
178
|
+
'active',
|
|
179
|
+
v_auth_user_id,
|
|
180
|
+
v_owner_user_id,
|
|
181
|
+
jsonb_build_object(
|
|
182
|
+
'scopes', coalesce(p_scopes, '[]'::jsonb),
|
|
183
|
+
'provisionedByUserId', v_admin_user_id,
|
|
184
|
+
'provisionedAt', now()
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
returning * into v_agent;
|
|
188
|
+
else
|
|
189
|
+
update agent_board.agents a
|
|
190
|
+
set agent_type = v_kind,
|
|
191
|
+
kind = v_kind::agent_board.agent_kind,
|
|
192
|
+
status = 'active',
|
|
193
|
+
auth_user_id = v_auth_user_id,
|
|
194
|
+
owner_user_id = v_owner_user_id,
|
|
195
|
+
metadata = coalesce(a.metadata, '{}'::jsonb)
|
|
196
|
+
|| jsonb_build_object(
|
|
197
|
+
'scopes', coalesce(p_scopes, '[]'::jsonb),
|
|
198
|
+
'provisionedByUserId', v_admin_user_id,
|
|
199
|
+
'provisionedAt', now()
|
|
200
|
+
),
|
|
201
|
+
updated_at = now()
|
|
202
|
+
where a.id = v_agent.id
|
|
203
|
+
returning * into v_agent;
|
|
204
|
+
end if;
|
|
205
|
+
|
|
206
|
+
select *
|
|
207
|
+
into v_credential
|
|
208
|
+
from agent_board._issue_agent_credential(
|
|
209
|
+
v_agent.id,
|
|
210
|
+
'provisioned',
|
|
211
|
+
(
|
|
212
|
+
select ac.id
|
|
213
|
+
from agent_board.agent_credentials ac
|
|
214
|
+
where ac.agent_id = v_agent.id
|
|
215
|
+
and ac.revoked_at is null
|
|
216
|
+
order by ac.created_at desc
|
|
217
|
+
limit 1
|
|
218
|
+
),
|
|
219
|
+
jsonb_build_object('issuedByUserId', v_admin_user_id, 'issuedAt', now(), 'scopes', coalesce(p_scopes, '[]'::jsonb))
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return query
|
|
223
|
+
select v_agent.id, v_auth_user_id, v_credential.credential_id, v_credential.refresh_token, v_credential.expires_at, v_credential.created_at;
|
|
224
|
+
end;
|
|
225
|
+
$$;
|
|
226
|
+
|
|
227
|
+
-- Data repair (idempotent, scoped): revoke any still-live credentials previously
|
|
228
|
+
-- minted for system principals. They can never bind a session, so keeping them
|
|
229
|
+
-- unrevoked only exposes un-exchangeable tokens. Touches only `revoked_at is null`
|
|
230
|
+
-- credentials of `kind='system'` agents; a zero-row result is expected and fine.
|
|
231
|
+
update agent_board.agent_credentials ac
|
|
232
|
+
set revoked_at = now(),
|
|
233
|
+
updated_at = now(),
|
|
234
|
+
metadata = coalesce(ac.metadata, '{}'::jsonb)
|
|
235
|
+
|| jsonb_build_object(
|
|
236
|
+
'revokedReason', 'system_principal_fail_closed',
|
|
237
|
+
'revokedAt', now()
|
|
238
|
+
)
|
|
239
|
+
from agent_board.agents a
|
|
240
|
+
where a.id = ac.agent_id
|
|
241
|
+
and a.kind = 'system'::agent_board.agent_kind
|
|
242
|
+
and ac.revoked_at is null;
|