feat(device-core): add control resource ledger

This commit is contained in:
Codex
2026-08-10 19:05:48 +03:00
parent 422ddb020f
commit 43dc9b1f45
15 changed files with 2214 additions and 0 deletions
@@ -0,0 +1,610 @@
begin;
create table if not exists device_resource_bindings (
id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
binding_key text not null
check (binding_key ~ '^[a-z][a-z0-9-]{1,62}$'),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
source_kind text not null
check (source_kind in ('device', 'collection')),
device_id uuid references device_instances(id),
collection_id uuid references device_collections(id),
target_kind text not null
check (target_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
target_ref text not null
check (length(btrim(target_ref)) between 3 and 256),
capabilities text[] not null default '{}'
check (
cardinality(capabilities) between 1 and 16
and array_position(capabilities, null) is null
),
lifecycle_state text not null default 'pending_external_approval'
check (lifecycle_state in ('pending_external_approval', 'active', 'revoked')),
source_approved_by_ref text not null
check (length(btrim(source_approved_by_ref)) between 3 and 256),
source_approved_at timestamptz not null default now(),
external_approval_ref text
check (
external_approval_ref is null
or length(btrim(external_approval_ref)) between 3 and 256
),
external_approval_digest text
check (
external_approval_digest is null
or external_approval_digest ~ '^sha256:[a-f0-9]{64}$'
),
external_approved_at timestamptz,
revoked_at timestamptz,
revoked_by_ref text
check (
revoked_by_ref is null
or length(btrim(revoked_by_ref)) between 3 and 256
),
revocation_code text
check (
revocation_code is null
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, binding_key),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id),
foreign key (collection_id, project_id)
references device_collections(id, project_id),
check (
(source_kind = 'device' and device_id is not null and collection_id is null)
or
(source_kind = 'collection' and device_id is null and collection_id is not null)
),
check (
(
lifecycle_state = 'pending_external_approval'
and external_approval_ref is null
and external_approval_digest is null
and external_approved_at is null
and revoked_at is null
and revoked_by_ref is null
and revocation_code is null
)
or
(
lifecycle_state = 'active'
and external_approval_ref is not null
and external_approval_digest is not null
and external_approved_at is not null
and revoked_at is null
and revoked_by_ref is null
and revocation_code is null
)
or
(
lifecycle_state = 'revoked'
and revoked_at is not null
and revoked_by_ref is not null
and revocation_code is not null
)
)
);
create index if not exists device_resource_bindings_project_state_idx
on device_resource_bindings (project_id, lifecycle_state, updated_at desc);
create index if not exists device_resource_bindings_device_state_idx
on device_resource_bindings (device_id, lifecycle_state, updated_at desc)
where device_id is not null;
create or replace function device_assert_binding_source_scope()
returns trigger
language plpgsql
as $$
begin
if new.source_kind = 'device' and not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using
message = 'device_binding_source_scope_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_resource_bindings_source_guard
on device_resource_bindings;
create trigger device_resource_bindings_source_guard
before insert or update of owner_scope_id, project_id, source_kind, device_id, collection_id
on device_resource_bindings
for each row
execute function device_assert_binding_source_scope();
create table if not exists device_configuration_revisions (
id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
device_id uuid not null references device_instances(id),
revision_number bigint not null check (revision_number > 0),
model_profile_ref text not null references device_model_profiles(profile_ref),
schema_artifact_ref text not null
check (length(btrim(schema_artifact_ref)) between 3 and 256),
configuration_digest text not null
check (configuration_digest ~ '^sha256:[a-f0-9]{64}$'),
configuration jsonb not null
check (
jsonb_typeof(configuration) = 'object'
and octet_length(configuration::text) <= 65536
),
change_summary text
check (change_summary is null or length(change_summary) <= 1000),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
unique (device_id, revision_number),
unique (id, device_id, project_id),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id)
);
create index if not exists device_configuration_revisions_project_idx
on device_configuration_revisions (project_id, device_id, revision_number desc);
create or replace function device_assert_configuration_revision_scope()
returns trigger
language plpgsql
as $$
begin
if not exists (
select 1
from device_instances di
join device_model_profiles dmp
on dmp.profile_ref = di.model_profile_ref
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
and di.model_profile_ref = new.model_profile_ref
and dmp.schema_artifact_ref = new.schema_artifact_ref
and dmp.lifecycle_state = 'active'
) then
raise foreign_key_violation using
message = 'device_configuration_revision_scope_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_configuration_revisions_scope_guard
on device_configuration_revisions;
create trigger device_configuration_revisions_scope_guard
before insert
on device_configuration_revisions
for each row
execute function device_assert_configuration_revision_scope();
create table if not exists device_configuration_state (
device_id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
desired_revision_id uuid,
applied_revision_id uuid,
applied_at timestamptz,
applied_by_ref text,
updated_at timestamptz not null default now(),
foreign key (device_id, project_id, owner_scope_id)
references device_instances(id, project_id, owner_scope_id),
foreign key (desired_revision_id, device_id, project_id)
references device_configuration_revisions(id, device_id, project_id),
foreign key (applied_revision_id, device_id, project_id)
references device_configuration_revisions(id, device_id, project_id),
check (desired_revision_id is not null or applied_revision_id is not null),
check (
(applied_revision_id is null and applied_at is null and applied_by_ref is null)
or
(applied_revision_id is not null and applied_at is not null and applied_by_ref is not null)
)
);
create table if not exists device_commands (
id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
device_id uuid not null references device_instances(id),
command_key text not null
check (command_key ~ '^[a-z][a-z0-9-]{1,62}$'),
command_catalog_ref text not null
check (length(btrim(command_catalog_ref)) between 3 and 256),
command_type text not null
check (command_type ~ '^[a-z][a-z0-9._:-]{1,63}$'),
risk_class text not null
check (risk_class in ('low', 'moderate', 'high', 'critical')),
parameters_digest text not null
check (parameters_digest ~ '^sha256:[a-f0-9]{64}$'),
parameters_projection jsonb not null
check (
jsonb_typeof(parameters_projection) = 'object'
and octet_length(parameters_projection::text) <= 16384
),
lifecycle_state text not null default 'draft'
check (lifecycle_state in (
'draft',
'planned',
'awaiting_confirmation',
'queued',
'dispatched',
'acknowledged',
'verified',
'failed',
'expired',
'unknown'
)),
planned_by_ref text not null
check (length(btrim(planned_by_ref)) between 3 and 256),
planned_at timestamptz not null default now(),
expires_at timestamptz not null,
confirmed_by_ref text,
confirmed_at timestamptz,
dispatched_at timestamptz,
transport_message_ref text
check (
transport_message_ref is null
or length(btrim(transport_message_ref)) between 3 and 256
),
acknowledged_at timestamptz,
terminal_at timestamptz,
terminal_reason_code text
check (
terminal_reason_code is null
or terminal_reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, command_key),
unique (id, device_id, project_id),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id),
check (expires_at > planned_at),
check (
(confirmed_at is null and confirmed_by_ref is null)
or
(confirmed_at is not null and confirmed_by_ref is not null)
),
check (
(
lifecycle_state in ('dispatched', 'acknowledged', 'verified', 'unknown')
and dispatched_at is not null
and transport_message_ref is not null
)
or lifecycle_state not in ('dispatched', 'acknowledged', 'verified', 'unknown')
),
check (
(lifecycle_state in ('acknowledged', 'verified') and acknowledged_at is not null)
or lifecycle_state not in ('acknowledged', 'verified')
),
check (
(
lifecycle_state in ('verified', 'failed', 'expired', 'unknown')
and terminal_at is not null
and terminal_reason_code is not null
)
or
(
lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
and terminal_at is null
and terminal_reason_code is null
)
)
);
create index if not exists device_commands_project_state_idx
on device_commands (project_id, lifecycle_state, updated_at desc);
create index if not exists device_commands_device_state_idx
on device_commands (device_id, lifecycle_state, updated_at desc);
create table if not exists device_command_events (
id uuid primary key,
command_id uuid not null,
device_id uuid not null,
project_id uuid not null,
sequence_number bigint not null check (sequence_number > 0),
from_state text,
to_state text not null
check (to_state in (
'draft',
'planned',
'awaiting_confirmation',
'queued',
'dispatched',
'acknowledged',
'verified',
'failed',
'expired',
'unknown'
)),
actor_ref text not null
check (length(btrim(actor_ref)) between 3 and 256),
reason_code text not null
check (reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'),
evidence_ref text
check (
evidence_ref is null
or length(btrim(evidence_ref)) between 3 and 256
),
occurred_at timestamptz not null default now(),
unique (command_id, sequence_number),
foreign key (command_id, device_id, project_id)
references device_commands(id, device_id, project_id),
check (sequence_number = 1 or from_state is not null),
check (sequence_number <> 1 or from_state is null),
check (
from_state is null
or from_state in (
'draft',
'planned',
'awaiting_confirmation',
'queued',
'dispatched',
'acknowledged',
'verified',
'failed',
'expired',
'unknown'
)
)
);
create index if not exists device_command_events_command_idx
on device_command_events (command_id, sequence_number);
create or replace function device_assert_command_scope()
returns trigger
language plpgsql
as $$
begin
if not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using message = 'device_command_scope_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_commands_scope_guard on device_commands;
create trigger device_commands_scope_guard
before insert
on device_commands
for each row
execute function device_assert_command_scope();
create or replace function device_assert_command_event_sequence()
returns trigger
language plpgsql
as $$
declare
previous_state text;
begin
if new.sequence_number = 1 then
if new.from_state is not null or new.to_state <> 'draft' then
raise check_violation using message = 'device_command_initial_event_invalid';
end if;
return new;
end if;
select dce.to_state into previous_state
from device_command_events dce
where dce.command_id = new.command_id
and dce.sequence_number = new.sequence_number - 1;
if previous_state is null or previous_state <> new.from_state then
raise check_violation using message = 'device_command_event_sequence_invalid';
end if;
if not (
(new.from_state = 'draft' and new.to_state in ('planned', 'expired'))
or (new.from_state = 'planned' and new.to_state in ('awaiting_confirmation', 'queued', 'expired'))
or (new.from_state = 'awaiting_confirmation' and new.to_state in ('queued', 'expired'))
or (new.from_state = 'queued' and new.to_state in ('dispatched', 'failed', 'expired'))
or (new.from_state = 'dispatched' and new.to_state in ('acknowledged', 'failed', 'unknown'))
or (new.from_state = 'acknowledged' and new.to_state in ('verified', 'failed', 'unknown'))
) then
raise check_violation using message = 'device_command_event_transition_invalid';
end if;
return new;
end
$$;
drop trigger if exists device_command_events_sequence_guard
on device_command_events;
create trigger device_command_events_sequence_guard
before insert
on device_command_events
for each row
execute function device_assert_command_event_sequence();
create or replace function device_assert_command_transition()
returns trigger
language plpgsql
as $$
begin
if old.lifecycle_state = new.lifecycle_state then
return new;
end if;
if not (
(old.lifecycle_state = 'draft' and new.lifecycle_state in ('planned', 'expired'))
or (old.lifecycle_state = 'planned' and new.lifecycle_state in ('awaiting_confirmation', 'queued', 'expired'))
or (old.lifecycle_state = 'awaiting_confirmation' and new.lifecycle_state in ('queued', 'expired'))
or (old.lifecycle_state = 'queued' and new.lifecycle_state in ('dispatched', 'failed', 'expired'))
or (old.lifecycle_state = 'dispatched' and new.lifecycle_state in ('acknowledged', 'failed', 'unknown'))
or (old.lifecycle_state = 'acknowledged' and new.lifecycle_state in ('verified', 'failed', 'unknown'))
) then
raise check_violation using message = 'device_command_transition_invalid';
end if;
return new;
end
$$;
drop trigger if exists device_commands_transition_guard on device_commands;
create trigger device_commands_transition_guard
before update of lifecycle_state
on device_commands
for each row
execute function device_assert_command_transition();
create or replace function device_assert_command_event_projection()
returns trigger
language plpgsql
as $$
declare
current_state text;
latest_event_state text;
begin
select dc.lifecycle_state into current_state
from device_commands dc
where dc.id = new.id;
select dce.to_state into latest_event_state
from device_command_events dce
where dce.command_id = new.id
order by dce.sequence_number desc
limit 1;
if current_state is null or latest_event_state is distinct from current_state then
raise check_violation using message = 'device_command_event_projection_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_commands_event_projection_guard on device_commands;
create constraint trigger device_commands_event_projection_guard
after insert or update of lifecycle_state
on device_commands
deferrable initially deferred
for each row
execute function device_assert_command_event_projection();
create or replace function device_assert_command_current_projection()
returns trigger
language plpgsql
as $$
declare
current_state text;
latest_event_state text;
begin
select dc.lifecycle_state into current_state
from device_commands dc
where dc.id = new.command_id;
select dce.to_state into latest_event_state
from device_command_events dce
where dce.command_id = new.command_id
order by dce.sequence_number desc
limit 1;
if current_state is null or latest_event_state is distinct from current_state then
raise check_violation using message = 'device_command_current_projection_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_command_events_current_projection_guard
on device_command_events;
create constraint trigger device_command_events_current_projection_guard
after insert
on device_command_events
deferrable initially deferred
for each row
execute function device_assert_command_current_projection();
create or replace function device_reject_immutable_mutation()
returns trigger
language plpgsql
as $$
begin
raise check_violation using message = 'device_immutable_record_mutation_forbidden';
end
$$;
drop trigger if exists device_configuration_revisions_immutable_guard
on device_configuration_revisions;
create trigger device_configuration_revisions_immutable_guard
before update or delete or truncate
on device_configuration_revisions
for each statement
execute function device_reject_immutable_mutation();
drop trigger if exists device_command_events_immutable_guard
on device_command_events;
create trigger device_command_events_immutable_guard
before update or delete or truncate
on device_command_events
for each statement
execute function device_reject_immutable_mutation();
drop trigger if exists device_audit_events_immutable_guard
on device_audit_events;
create trigger device_audit_events_immutable_guard
before update or delete or truncate
on device_audit_events
for each statement
execute function device_reject_immutable_mutation();
create or replace function device_require_control_resources_clear_before_transfer()
returns trigger
language plpgsql
as $$
begin
if exists (
select 1 from device_resource_bindings drb
where drb.device_id = old.id
and drb.lifecycle_state in ('pending_external_approval', 'active')
) then
raise check_violation using message = 'device_transfer_active_resource_binding';
end if;
if exists (
select 1 from device_configuration_state dcs
where dcs.device_id = old.id
and dcs.applied_revision_id is not null
) then
raise check_violation using message = 'device_transfer_applied_configuration';
end if;
if exists (
select 1 from device_commands dc
where dc.device_id = old.id
and dc.lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
) then
raise check_violation using message = 'device_transfer_nonterminal_command';
end if;
return new;
end
$$;
drop trigger if exists device_instances_control_resource_transfer_guard
on device_instances;
create trigger device_instances_control_resource_transfer_guard
before update of owner_scope_id, project_id
on device_instances
for each row
when (
old.owner_scope_id is distinct from new.owner_scope_id
or old.project_id is distinct from new.project_id
)
execute function device_require_control_resources_clear_before_transfer();
commit;
@@ -0,0 +1,31 @@
begin;
alter table device_management_command_receipts
drop constraint if exists device_management_command_receipts_command_kind_check;
alter table device_management_command_receipts
add constraint device_management_command_receipts_command_kind_check
check (command_kind in (
'owner_scope.ensure',
'project.ensure',
'collection.ensure',
'project_grant.upsert',
'adapter_package.ensure',
'adapter_version.register',
'model_profile.register',
'edge.ensure',
'route.ensure',
'enrollment_intent.ensure',
'device.claim',
'device.transfer',
'discovery.reject',
'discovery.expire',
'device_credential_binding.upsert',
'device_credential_binding.revoke',
'device_binding.ensure',
'device_binding.revoke',
'device_configuration_revision.create',
'device_configuration_desired.set'
));
commit;