Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
This commit is contained in:
1 parent
2350f744e3
commit
07d0b4587d
1904 files changed
+1133
-282075
No files matched your search
@@ -0,0 +1,235 @@
|
||||
export const initialEnterpriseSchema = `
|
||||
create table if not exists organizations (
|
||||
id text primary key,
|
||||
name text not null,
|
||||
mission text not null,
|
||||
goal text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists employees (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
display_name text not null,
|
||||
role text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists agents (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
role text not null,
|
||||
department text not null,
|
||||
model_provider_id text,
|
||||
model_id text,
|
||||
budget_cents integer not null,
|
||||
manager_agent_id text references agents(id) on delete set null,
|
||||
lifecycle text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists tasks (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
title text not null,
|
||||
description text not null,
|
||||
status text not null,
|
||||
assignee_agent_id text references agents(id) on delete set null,
|
||||
risk text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create table if not exists approvals (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
kind text not null,
|
||||
title text not null,
|
||||
details text not null,
|
||||
risk text not null,
|
||||
requested_by_type text not null,
|
||||
requested_by_id text,
|
||||
state text not null,
|
||||
decided_by_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null,
|
||||
decided_at text
|
||||
);
|
||||
|
||||
create table if not exists chat_spaces (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
space_type text not null,
|
||||
name text not null,
|
||||
topic text not null,
|
||||
visibility text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists chat_memberships (
|
||||
space_id text not null references chat_spaces(id) on delete cascade,
|
||||
principal_type text not null,
|
||||
principal_id text not null,
|
||||
role text not null,
|
||||
mute_state text not null,
|
||||
primary key (space_id, principal_type, principal_id)
|
||||
);
|
||||
|
||||
create table if not exists chat_messages (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
space_id text not null references chat_spaces(id) on delete cascade,
|
||||
thread_id text,
|
||||
sender_type text not null,
|
||||
sender_id text,
|
||||
body_md text not null,
|
||||
created_at text not null,
|
||||
edited_at text,
|
||||
deleted_at text
|
||||
);
|
||||
|
||||
create table if not exists file_objects (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
kind text not null,
|
||||
size_bytes integer not null,
|
||||
sha256 text not null,
|
||||
storage_path text not null,
|
||||
owner_type text not null,
|
||||
owner_id text,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists providers (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
label text not null,
|
||||
auth_type text not null,
|
||||
model text,
|
||||
credential_id text,
|
||||
enabled integer not null,
|
||||
created_at text not null,
|
||||
unique(org_id, provider_key)
|
||||
);
|
||||
|
||||
create table if not exists credentials (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
kind text not null,
|
||||
ciphertext blob not null,
|
||||
display_hint text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists skill_definitions (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
slug text not null,
|
||||
name text not null,
|
||||
owner_type text not null,
|
||||
status text not null,
|
||||
created_at text not null,
|
||||
unique(org_id, slug)
|
||||
);
|
||||
|
||||
create table if not exists skill_revisions (
|
||||
id text primary key,
|
||||
skill_id text not null references skill_definitions(id) on delete cascade,
|
||||
version_semver text not null,
|
||||
manifest_json text not null,
|
||||
source_type text not null,
|
||||
source_ref text not null,
|
||||
published_at text not null,
|
||||
immutable_checksum text not null,
|
||||
unique(skill_id, version_semver)
|
||||
);
|
||||
|
||||
create table if not exists skill_artifacts (
|
||||
id text primary key,
|
||||
revision_id text not null references skill_revisions(id) on delete cascade,
|
||||
artifact_type text not null,
|
||||
object_key text not null,
|
||||
sha256 text not null,
|
||||
size_bytes integer not null
|
||||
);
|
||||
|
||||
create table if not exists skill_tests (
|
||||
id text primary key,
|
||||
revision_id text not null references skill_revisions(id) on delete cascade,
|
||||
test_type text not null,
|
||||
expected_output_ref text not null,
|
||||
last_result text,
|
||||
last_run_at text
|
||||
);
|
||||
|
||||
create table if not exists skill_bindings (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
skill_id text not null references skill_definitions(id) on delete cascade,
|
||||
pinned_revision_id text not null references skill_revisions(id) on delete cascade,
|
||||
binding_mode text not null,
|
||||
priority integer not null,
|
||||
enabled integer not null,
|
||||
unique(agent_id, skill_id)
|
||||
);
|
||||
|
||||
create table if not exists social_accounts (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
platform text not null,
|
||||
handle text not null,
|
||||
display_name text not null,
|
||||
authorized_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists social_account_grants (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text not null references social_accounts(id) on delete cascade,
|
||||
employee_id text not null references employees(id) on delete cascade,
|
||||
role text not null,
|
||||
scope_json text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists browser_actions (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text not null references social_accounts(id) on delete cascade,
|
||||
action_type text not null,
|
||||
policy_decision text not null,
|
||||
evidence_file_id text references file_objects(id) on delete set null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists migration_reports (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
source_ref text not null,
|
||||
report_json text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists audit_events (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
entity_type text not null,
|
||||
entity_id text not null,
|
||||
actor_type text not null,
|
||||
actor_id text,
|
||||
action text not null,
|
||||
payload_json text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_tasks_org_status on tasks(org_id, status);
|
||||
create index if not exists idx_messages_space_created on chat_messages(space_id, created_at);
|
||||
create index if not exists idx_audit_org_created on audit_events(org_id, created_at);
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (1, 'initial_enterprise_schema', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,117 @@
|
||||
export const collaborationProviderPolicySchema = `
|
||||
create table if not exists skill_dependencies (
|
||||
revision_id text not null references skill_revisions(id) on delete cascade,
|
||||
depends_on_revision_id text not null references skill_revisions(id) on delete cascade,
|
||||
dependency_kind text not null,
|
||||
optional integer not null,
|
||||
primary key (revision_id, depends_on_revision_id)
|
||||
);
|
||||
|
||||
create table if not exists skill_runtime_snapshots (
|
||||
id text primary key,
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
run_id text not null,
|
||||
compiled_prompt text not null,
|
||||
tools_manifest text not null,
|
||||
source_revision_ids text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists skill_catalog_tags (
|
||||
skill_id text not null references skill_definitions(id) on delete cascade,
|
||||
tag text not null,
|
||||
tag_type text not null,
|
||||
primary key (skill_id, tag, tag_type)
|
||||
);
|
||||
|
||||
create table if not exists chat_threads (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
space_id text not null references chat_spaces(id) on delete cascade,
|
||||
root_message_id text references chat_messages(id) on delete set null,
|
||||
title text not null,
|
||||
state text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create table if not exists chat_mentions (
|
||||
message_id text not null references chat_messages(id) on delete cascade,
|
||||
target_type text not null,
|
||||
target_id text not null,
|
||||
offset_start integer not null,
|
||||
offset_end integer not null
|
||||
);
|
||||
|
||||
create table if not exists chat_attachments (
|
||||
message_id text not null references chat_messages(id) on delete cascade,
|
||||
file_id text not null references file_objects(id) on delete cascade,
|
||||
transfer_type text not null,
|
||||
checksum text not null,
|
||||
preview_key text
|
||||
);
|
||||
|
||||
create table if not exists chat_presence (
|
||||
principal_type text not null,
|
||||
principal_id text not null,
|
||||
status text not null,
|
||||
status_text text not null,
|
||||
last_seen_at text not null,
|
||||
primary key (principal_type, principal_id)
|
||||
);
|
||||
|
||||
create table if not exists chat_slash_invocations (
|
||||
id text primary key,
|
||||
message_id text not null references chat_messages(id) on delete cascade,
|
||||
command text not null,
|
||||
args_json text not null,
|
||||
result_ref text,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists provider_requests (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
model text not null,
|
||||
prompt_sha256 text not null,
|
||||
response_text text,
|
||||
raw_response_json text,
|
||||
state text not null,
|
||||
error_message text,
|
||||
created_at text not null,
|
||||
completed_at text
|
||||
);
|
||||
|
||||
create table if not exists policy_rules (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
subject_type text not null,
|
||||
subject_id text,
|
||||
action text not null,
|
||||
effect text not null,
|
||||
risk_ceiling text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_provider_requests_org_created on provider_requests(org_id, created_at);
|
||||
create index if not exists idx_chat_threads_space_updated on chat_threads(space_id, updated_at);
|
||||
insert into policy_rules(id, org_id, subject_type, subject_id, action, effect, risk_ceiling, created_at)
|
||||
select
|
||||
'pol_' || lower(hex(randomblob(16))),
|
||||
id,
|
||||
'organization',
|
||||
id,
|
||||
'task_create',
|
||||
'require_approval',
|
||||
'medium',
|
||||
datetime('now')
|
||||
from organizations
|
||||
where not exists (
|
||||
select 1 from policy_rules
|
||||
where policy_rules.org_id = organizations.id and policy_rules.action = 'task_create'
|
||||
);
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (2, 'collaboration_provider_policy_schema', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,76 @@
|
||||
export const onboardingExecutionSchema = `
|
||||
alter table organizations add column description text not null default '';
|
||||
alter table organizations add column execution_mode text not null default 'ai_ceo';
|
||||
alter table organizations add column auto_approve_hires integer not null default 1;
|
||||
alter table organizations add column review_deliverables integer not null default 0;
|
||||
|
||||
alter table agents add column title text not null default '';
|
||||
alter table agents add column role_level text not null default 'staff';
|
||||
alter table agents add column model_strategy text not null default 'pooled';
|
||||
alter table agents add column workspace_id text;
|
||||
alter table agents add column heartbeat_enabled integer not null default 0;
|
||||
alter table agents add column heartbeat_interval_sec integer not null default 0;
|
||||
|
||||
alter table tasks add column project_id text;
|
||||
alter table tasks add column goal_id text;
|
||||
alter table tasks add column workspace_id text;
|
||||
alter table tasks add column priority text not null default 'normal';
|
||||
alter table tasks add column requires_filesystem integer not null default 0;
|
||||
|
||||
create table if not exists workspaces (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
local_path text not null,
|
||||
repo_url text not null,
|
||||
repo_ref text not null,
|
||||
project_id text,
|
||||
is_primary integer not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create table if not exists goals (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
title text not null,
|
||||
description text not null,
|
||||
status text not null,
|
||||
owner_agent_id text references agents(id) on delete set null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create table if not exists projects (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
goal_id text references goals(id) on delete set null,
|
||||
name text not null,
|
||||
description text not null,
|
||||
lead_agent_id text references agents(id) on delete set null,
|
||||
status text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create table if not exists task_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
task_id text not null references tasks(id) on delete cascade,
|
||||
agent_id text references agents(id) on delete set null,
|
||||
provider_id text references providers(id) on delete set null,
|
||||
trigger text not null,
|
||||
state text not null,
|
||||
created_at text not null,
|
||||
started_at text,
|
||||
completed_at text,
|
||||
error_message text
|
||||
);
|
||||
|
||||
create index if not exists idx_workspaces_org_primary on workspaces(org_id, is_primary);
|
||||
create index if not exists idx_goals_org_status on goals(org_id, status);
|
||||
create index if not exists idx_projects_org_status on projects(org_id, status);
|
||||
create index if not exists idx_task_runs_task_created on task_runs(task_id, created_at);
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (3, 'onboarding_execution_schema', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
export const filesPinnedAndScheduledPosts = `
|
||||
alter table file_objects add column pinned integer not null default 0;
|
||||
alter table file_objects add column space_id text;
|
||||
alter table file_objects add column updated_at text;
|
||||
|
||||
create table if not exists scheduled_posts (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text not null references social_accounts(id) on delete cascade,
|
||||
body_md text not null,
|
||||
scheduled_for text not null,
|
||||
state text not null,
|
||||
approval_id text references approvals(id) on delete set null,
|
||||
created_by_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_files_org_pinned on file_objects(org_id, pinned);
|
||||
create index if not exists idx_scheduled_posts_org_state on scheduled_posts(org_id, state);
|
||||
create index if not exists idx_scheduled_posts_account_date on scheduled_posts(social_account_id, scheduled_for);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (4, 'files_pinned_and_scheduled_posts', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,15 @@
|
||||
export const chatReadState = `
|
||||
create table if not exists chat_read_state (
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
employee_id text not null references employees(id) on delete cascade,
|
||||
space_id text not null references chat_spaces(id) on delete cascade,
|
||||
last_read_message_id text references chat_messages(id) on delete set null,
|
||||
last_read_at text not null,
|
||||
primary key (employee_id, space_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_chat_read_state_org on chat_read_state(org_id, employee_id);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (5, 'chat_read_state', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
export const appStateSchema = `
|
||||
create table if not exists app_state (
|
||||
id integer primary key check (id = 1),
|
||||
active_org_id text references organizations(id) on delete set null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
insert into app_state(id, active_org_id, updated_at)
|
||||
select 1, (select id from organizations order by created_at asc limit 1), datetime('now')
|
||||
where not exists (select 1 from app_state where id = 1);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (6, 'app_state', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,4 @@
|
||||
export const skillHardeningSchema = `
|
||||
create unique index if not exists idx_skill_runtime_snapshots_agent_run
|
||||
on skill_runtime_snapshots(agent_id, run_id);
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const providerHealthSchema = `
|
||||
alter table providers add column auth_state text not null default 'unconfigured';
|
||||
alter table providers add column health_state text not null default 'unchecked';
|
||||
alter table providers add column last_checked_at text;
|
||||
alter table providers add column last_error text;
|
||||
`;
|
||||
@@ -0,0 +1,49 @@
|
||||
export const socialGovernanceSchema = `
|
||||
update social_account_grants
|
||||
set scope_json = '{"actions":["login","draft","approve"],"riskCeiling":"medium"}'
|
||||
where scope_json = '{"actions":["login","draft","approve"]}';
|
||||
|
||||
create temp table if not exists social_post_approval_backfill(
|
||||
post_id text primary key,
|
||||
approval_id text not null
|
||||
);
|
||||
|
||||
delete from social_post_approval_backfill;
|
||||
|
||||
insert into social_post_approval_backfill(post_id, approval_id)
|
||||
select id, 'app_' || lower(hex(randomblob(16)))
|
||||
from scheduled_posts
|
||||
where state = 'scheduled' and approval_id is null;
|
||||
|
||||
insert into approvals(
|
||||
id, org_id, kind, title, details, risk, requested_by_type, requested_by_id,
|
||||
state, decided_by_employee_id, created_at, decided_at
|
||||
)
|
||||
select
|
||||
backfill.approval_id,
|
||||
posts.org_id,
|
||||
'social_publish',
|
||||
'Approve social post',
|
||||
posts.body_md,
|
||||
'medium',
|
||||
case when posts.created_by_employee_id is null then 'system' else 'employee' end,
|
||||
posts.created_by_employee_id,
|
||||
'pending',
|
||||
null,
|
||||
posts.created_at,
|
||||
null
|
||||
from scheduled_posts posts
|
||||
join social_post_approval_backfill backfill on backfill.post_id = posts.id;
|
||||
|
||||
update scheduled_posts
|
||||
set approval_id = (
|
||||
select approval_id from social_post_approval_backfill
|
||||
where social_post_approval_backfill.post_id = scheduled_posts.id
|
||||
)
|
||||
where id in (select post_id from social_post_approval_backfill);
|
||||
|
||||
drop table social_post_approval_backfill;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (9, 'social_governance', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
export const fileHandoffSecuritySchema = `
|
||||
alter table file_objects add column scan_state text not null default 'approved';
|
||||
alter table file_objects add column scan_labels_json text not null default '[]';
|
||||
alter table file_objects add column scanned_at text;
|
||||
|
||||
create table if not exists file_access_grants (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
file_id text not null references file_objects(id) on delete cascade,
|
||||
granted_to_type text not null,
|
||||
granted_to_id text not null,
|
||||
granted_by_type text not null,
|
||||
granted_by_id text,
|
||||
source_message_id text references chat_messages(id) on delete set null,
|
||||
source_thread_id text references chat_threads(id) on delete set null,
|
||||
access_level text not null,
|
||||
created_at text not null,
|
||||
unique(file_id, granted_to_type, granted_to_id, source_message_id, access_level)
|
||||
);
|
||||
|
||||
insert into file_access_grants(
|
||||
id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id,
|
||||
source_message_id, source_thread_id, access_level, created_at
|
||||
)
|
||||
select
|
||||
'fgr_' || lower(hex(randomblob(16))),
|
||||
org_id,
|
||||
id,
|
||||
'org_group',
|
||||
org_id || ':workspace',
|
||||
'system',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'manage',
|
||||
coalesce(created_at, datetime('now'))
|
||||
from file_objects
|
||||
where owner_type = 'system'
|
||||
and not exists (
|
||||
select 1 from file_access_grants
|
||||
where file_access_grants.file_id = file_objects.id
|
||||
and file_access_grants.granted_to_type = 'org_group'
|
||||
and file_access_grants.granted_to_id = file_objects.org_id || ':workspace'
|
||||
);
|
||||
|
||||
create index if not exists idx_file_grants_org_file on file_access_grants(org_id, file_id);
|
||||
create index if not exists idx_file_grants_principal on file_access_grants(org_id, granted_to_type, granted_to_id);
|
||||
create index if not exists idx_files_org_scan on file_objects(org_id, scan_state);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (10, 'file_handoff_security', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,29 @@
|
||||
export const boardInterventionSchema = `
|
||||
alter table chat_threads add column board_state text not null default 'open';
|
||||
alter table chat_threads add column controlled_by_type text;
|
||||
alter table chat_threads add column controlled_by_id text;
|
||||
|
||||
create table if not exists board_interventions (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
thread_id text not null references chat_threads(id) on delete cascade,
|
||||
message_id text not null references chat_messages(id) on delete cascade,
|
||||
mode text not null,
|
||||
action text not null,
|
||||
details text not null,
|
||||
target_type text,
|
||||
target_id text,
|
||||
approval_id text references approvals(id) on delete set null,
|
||||
created_by_type text not null,
|
||||
created_by_id text,
|
||||
created_at text not null,
|
||||
unique(message_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_board_interventions_org_thread on board_interventions(org_id, thread_id, created_at);
|
||||
create index if not exists idx_board_interventions_message on board_interventions(message_id);
|
||||
create index if not exists idx_chat_threads_org_board_state on chat_threads(org_id, board_state);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (11, 'board_interventions', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,47 @@
|
||||
export const complianceControlsSchema = `
|
||||
alter table chat_spaces add column retention_policy text not null default 'standard_365d';
|
||||
|
||||
create table if not exists compliance_exports (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
subject_type text not null,
|
||||
subject_id text not null,
|
||||
export_type text not null,
|
||||
state text not null,
|
||||
payload_json text not null,
|
||||
requested_by_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null,
|
||||
completed_at text
|
||||
);
|
||||
|
||||
create table if not exists legal_holds (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
entity_type text not null,
|
||||
entity_id text not null,
|
||||
reason text not null,
|
||||
created_by_employee_id text references employees(id) on delete set null,
|
||||
active integer not null,
|
||||
created_at text not null,
|
||||
released_at text
|
||||
);
|
||||
|
||||
create table if not exists retention_freezes (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
scope_type text not null,
|
||||
scope_id text not null,
|
||||
reason text not null,
|
||||
created_by_employee_id text references employees(id) on delete set null,
|
||||
active integer not null,
|
||||
created_at text not null,
|
||||
released_at text
|
||||
);
|
||||
|
||||
create index if not exists idx_compliance_exports_org_created on compliance_exports(org_id, created_at);
|
||||
create index if not exists idx_legal_holds_org_entity on legal_holds(org_id, entity_type, entity_id, active);
|
||||
create index if not exists idx_retention_freezes_org_scope on retention_freezes(org_id, scope_type, scope_id, active);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (12, 'compliance_controls', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
export const searchDocumentsSchema = `
|
||||
create virtual table if not exists search_documents using fts5(
|
||||
org_id unindexed,
|
||||
entity_type unindexed,
|
||||
entity_id unindexed,
|
||||
title,
|
||||
body,
|
||||
facet_json unindexed
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (13, 'search_documents', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,31 @@
|
||||
export const providerPolicyUsageSchema = `
|
||||
alter table providers add column data_policy text not null default 'standard';
|
||||
alter table providers add column background_enabled integer not null default 0;
|
||||
alter table providers add column monthly_request_limit integer not null default 1000;
|
||||
alter table providers add column workspace_scope text not null default 'organization';
|
||||
|
||||
alter table provider_requests add column input_char_count integer not null default 0;
|
||||
alter table provider_requests add column output_char_count integer not null default 0;
|
||||
alter table provider_requests add column execution_mode text not null default 'foreground';
|
||||
alter table provider_requests add column policy_decision text not null default 'allowed:standard';
|
||||
|
||||
create table if not exists provider_usage_windows (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
window_start text not null,
|
||||
window_end text not null,
|
||||
request_count integer not null,
|
||||
input_char_count integer not null,
|
||||
output_char_count integer not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(provider_id, window_start)
|
||||
);
|
||||
|
||||
create index if not exists idx_provider_usage_windows_org_start
|
||||
on provider_usage_windows(org_id, window_start);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (14, 'provider_policy_usage', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,90 @@
|
||||
export const identityAccessSchema = `
|
||||
create table if not exists employee_identities (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
employee_id text not null references employees(id) on delete cascade,
|
||||
source text not null,
|
||||
external_subject text not null,
|
||||
email text,
|
||||
active integer not null,
|
||||
created_at text not null,
|
||||
last_seen_at text,
|
||||
unique(org_id, source, external_subject)
|
||||
);
|
||||
|
||||
create table if not exists service_identities (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
name text not null,
|
||||
scope_json text not null,
|
||||
created_at text not null,
|
||||
unique(org_id, agent_id)
|
||||
);
|
||||
|
||||
create table if not exists role_grants (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
principal_type text not null,
|
||||
principal_id text not null,
|
||||
role_key text not null,
|
||||
scope_type text not null,
|
||||
scope_id text not null,
|
||||
granted_by_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null,
|
||||
unique(org_id, principal_type, principal_id, role_key, scope_type, scope_id)
|
||||
);
|
||||
|
||||
create table if not exists access_decisions (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
principal_type text not null,
|
||||
principal_id text not null,
|
||||
action text not null,
|
||||
resource_type text not null,
|
||||
resource_id text not null,
|
||||
decision text not null,
|
||||
reason text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_role_grants_principal on role_grants(org_id, principal_type, principal_id);
|
||||
create index if not exists idx_access_decisions_org_created on access_decisions(org_id, created_at);
|
||||
|
||||
insert into employee_identities(id, org_id, employee_id, source, external_subject, email, active, created_at, last_seen_at)
|
||||
select 'eid_' || lower(hex(randomblob(16))), org_id, id, 'bootstrap', id, null, 1, datetime('now'), datetime('now')
|
||||
from employees
|
||||
where not exists (
|
||||
select 1 from employee_identities
|
||||
where employee_identities.org_id = employees.org_id and employee_identities.employee_id = employees.id
|
||||
);
|
||||
|
||||
insert into role_grants(id, org_id, principal_type, principal_id, role_key, scope_type, scope_id, granted_by_employee_id, created_at)
|
||||
select 'rgr_' || lower(hex(randomblob(16))), org_id, 'employee', id, seed_role_key, 'organization', org_id, null, datetime('now')
|
||||
from employees
|
||||
cross join (
|
||||
select 'org_admin' as seed_role_key
|
||||
union all select 'approval_decider'
|
||||
union all select 'hiring_manager'
|
||||
)
|
||||
where not exists (
|
||||
select 1 from role_grants
|
||||
where role_grants.org_id = employees.org_id
|
||||
and role_grants.principal_type = 'employee'
|
||||
and role_grants.principal_id = employees.id
|
||||
and role_grants.role_key = seed_role_key
|
||||
and role_grants.scope_type = 'organization'
|
||||
and role_grants.scope_id = employees.org_id
|
||||
);
|
||||
|
||||
insert into service_identities(id, org_id, agent_id, name, scope_json, created_at)
|
||||
select 'sid_' || lower(hex(randomblob(16))), org_id, id, name || ' service identity', json_object('agentId', id), datetime('now')
|
||||
from agents
|
||||
where not exists (
|
||||
select 1 from service_identities
|
||||
where service_identities.org_id = agents.org_id and service_identities.agent_id = agents.id
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (15, 'identity_access', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,89 @@
|
||||
export const orgGovernanceSchema = `
|
||||
create table if not exists org_departments (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
lead_agent_id text references agents(id) on delete set null,
|
||||
headcount_limit integer not null,
|
||||
monthly_budget_limit_cents integer not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, name)
|
||||
);
|
||||
|
||||
create table if not exists hiring_policies (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
department_name text not null,
|
||||
max_headcount integer not null,
|
||||
monthly_budget_limit_cents integer not null,
|
||||
require_approval_above_cents integer not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, department_name)
|
||||
);
|
||||
|
||||
create table if not exists approval_routing_rules (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
kind text not null,
|
||||
minimum_risk text not null,
|
||||
route_to_type text not null,
|
||||
route_to_id text not null,
|
||||
created_at text not null,
|
||||
unique(org_id, kind, minimum_risk)
|
||||
);
|
||||
|
||||
create table if not exists approval_assignments (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
approval_id text not null references approvals(id) on delete cascade,
|
||||
assignee_type text not null,
|
||||
assignee_id text not null,
|
||||
routing_rule_id text references approval_routing_rules(id) on delete set null,
|
||||
created_at text not null,
|
||||
unique(approval_id, assignee_type, assignee_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_org_departments_org_name on org_departments(org_id, name);
|
||||
create index if not exists idx_hiring_policies_org_department on hiring_policies(org_id, department_name);
|
||||
create index if not exists idx_approval_assignments_org_approval on approval_assignments(org_id, approval_id);
|
||||
|
||||
insert into org_departments(id, org_id, name, lead_agent_id, headcount_limit, monthly_budget_limit_cents, created_at, updated_at)
|
||||
select 'dep_' || lower(hex(randomblob(16))), organizations.id, seed.name, null, 12, 2500000, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'Executive' as name union all select 'Engineering' union all select 'Product' union all select 'Design'
|
||||
union all select 'Marketing' union all select 'Operations' union all select 'Support'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from org_departments
|
||||
where org_departments.org_id = organizations.id and org_departments.name = seed.name
|
||||
);
|
||||
|
||||
insert into hiring_policies(id, org_id, department_name, max_headcount, monthly_budget_limit_cents, require_approval_above_cents, created_at, updated_at)
|
||||
select 'hpl_' || lower(hex(randomblob(16))), org_id, name, headcount_limit, monthly_budget_limit_cents, 500000, datetime('now'), datetime('now')
|
||||
from org_departments
|
||||
where not exists (
|
||||
select 1 from hiring_policies
|
||||
where hiring_policies.org_id = org_departments.org_id and hiring_policies.department_name = org_departments.name
|
||||
);
|
||||
|
||||
insert into approval_routing_rules(id, org_id, kind, minimum_risk, route_to_type, route_to_id, created_at)
|
||||
select 'arr_' || lower(hex(randomblob(16))), organizations.id, '*', 'low', 'employee', employees.id, datetime('now')
|
||||
from organizations
|
||||
join employees on employees.org_id = organizations.id
|
||||
where employees.id = (
|
||||
select first_employee.id from employees first_employee
|
||||
where first_employee.org_id = organizations.id
|
||||
order by first_employee.created_at asc
|
||||
limit 1
|
||||
)
|
||||
and not exists (
|
||||
select 1 from approval_routing_rules
|
||||
where approval_routing_rules.org_id = organizations.id and approval_routing_rules.kind = '*' and approval_routing_rules.minimum_risk = 'low'
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (16, 'org_governance', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,109 @@
|
||||
export const operationsControlsSchema = `
|
||||
create table if not exists feature_flags (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists slo_targets (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
signal_key text not null,
|
||||
label text not null,
|
||||
target_basis_points integer not null,
|
||||
window_minutes integer not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, signal_key)
|
||||
);
|
||||
|
||||
create table if not exists operational_events (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
signal_key text not null,
|
||||
source text not null,
|
||||
success integer not null check(success in (0, 1)),
|
||||
duration_ms integer,
|
||||
detail_json text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists chaos_drills (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
scenario_key text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists chaos_drill_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
drill_id text not null references chaos_drills(id) on delete cascade,
|
||||
state text not null,
|
||||
result_json text not null,
|
||||
started_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_operational_events_org_signal_created
|
||||
on operational_events(org_id, signal_key, created_at);
|
||||
|
||||
create index if not exists idx_chaos_drill_runs_org_drill_started
|
||||
on chaos_drill_runs(org_id, drill_id, started_at);
|
||||
|
||||
insert into feature_flags(id, org_id, key, label, enabled, created_at, updated_at)
|
||||
select 'ffg_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.enabled, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'provider_background_execution' as key, 'Provider background execution' as label, 1 as enabled
|
||||
union all select 'social_publish_approval', 'Social publish approval', 1
|
||||
union all select 'board_intervention', 'Board intervention', 1
|
||||
union all select 'chaos_drills', 'Chaos drills', 1
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from feature_flags
|
||||
where feature_flags.org_id = organizations.id and feature_flags.key = seed.key
|
||||
);
|
||||
|
||||
insert into slo_targets(id, org_id, signal_key, label, target_basis_points, window_minutes, created_at, updated_at)
|
||||
select 'slo_' || lower(hex(randomblob(16))), organizations.id, seed.signal_key, seed.label, seed.target_basis_points, seed.window_minutes, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'provider_request_success' as signal_key, 'Provider requests' as label, 9950 as target_basis_points, 43200 as window_minutes
|
||||
union all select 'approval_route_coverage', 'Approval routing', 10000, 43200
|
||||
union all select 'browser_session_boundary', 'Browser session boundary', 10000, 43200
|
||||
union all select 'skill_resolve_success', 'Skill resolve', 9950, 43200
|
||||
union all select 'migration_validation_success', 'Migration validation', 10000, 43200
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from slo_targets
|
||||
where slo_targets.org_id = organizations.id and slo_targets.signal_key = seed.signal_key
|
||||
);
|
||||
|
||||
insert into chaos_drills(id, org_id, key, label, scenario_key, enabled, created_at, updated_at)
|
||||
select 'cdr_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.scenario_key, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'provider-outage-policy' as key, 'Provider outage policy' as label, 'provider_outage_policy_gate' as scenario_key
|
||||
union all select 'approval-backlog-routing', 'Approval backlog routing', 'approval_backlog_gate'
|
||||
union all select 'browser-session-boundary', 'Browser session boundary', 'browser_session_boundary'
|
||||
union all select 'migration-rollback-pack', 'Migration rollback pack', 'migration_rollback_pack'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from chaos_drills
|
||||
where chaos_drills.org_id = organizations.id and chaos_drills.key = seed.key
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (17, 'operations_controls', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,137 @@
|
||||
export const sdlcControlsSchema = `
|
||||
create table if not exists sdlc_review_agents (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
specialty text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists sdlc_review_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
review_agent_id text not null references sdlc_review_agents(id) on delete cascade,
|
||||
target_type text not null,
|
||||
target_ref text not null,
|
||||
state text not null,
|
||||
result_json text not null,
|
||||
created_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create table if not exists eval_suites (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
scope text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create table if not exists eval_cases (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
suite_id text not null references eval_suites(id) on delete cascade,
|
||||
title text not null,
|
||||
source_type text not null,
|
||||
source_ref text not null,
|
||||
expected_signal text not null,
|
||||
created_at text not null,
|
||||
unique(suite_id, title)
|
||||
);
|
||||
|
||||
create table if not exists eval_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
suite_id text not null references eval_suites(id) on delete cascade,
|
||||
state text not null,
|
||||
passed_count integer not null,
|
||||
failed_count integer not null,
|
||||
result_json text not null,
|
||||
created_at text not null,
|
||||
completed_at text not null
|
||||
);
|
||||
|
||||
create table if not exists pr_bot_gates (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
label text not null,
|
||||
gate_type text not null,
|
||||
enabled integer not null check(enabled in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique(org_id, key)
|
||||
);
|
||||
|
||||
create index if not exists idx_sdlc_review_runs_org_agent_created
|
||||
on sdlc_review_runs(org_id, review_agent_id, created_at);
|
||||
|
||||
create index if not exists idx_eval_runs_org_suite_created
|
||||
on eval_runs(org_id, suite_id, created_at);
|
||||
|
||||
insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at)
|
||||
select 'sra_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.specialty, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'code_reviewer' as key, 'Code Review Agent' as label, 'static quality and architecture' as specialty
|
||||
union all select 'migration_reviewer', 'Migration Review Agent', 'schema diff and rollback evidence'
|
||||
union all select 'policy_reviewer', 'Policy Review Agent', 'provider, approval, and access policy'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from sdlc_review_agents
|
||||
where sdlc_review_agents.org_id = organizations.id and sdlc_review_agents.key = seed.key
|
||||
);
|
||||
|
||||
insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at)
|
||||
select 'evs_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.scope, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'contract' as key, 'Contract checks' as label, 'provider adapters, event schemas, slash commands' as scope
|
||||
union all select 'integration', 'Integration checks', 'chat, approval, file, skill resolution flows'
|
||||
union all select 'replay', 'Replay checks', 'audit timeline and historical decision regression'
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from eval_suites
|
||||
where eval_suites.org_id = organizations.id and eval_suites.key = seed.key
|
||||
);
|
||||
|
||||
insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at)
|
||||
select 'evc_' || lower(hex(randomblob(16))), eval_suites.org_id, eval_suites.id, seed.title, seed.source_type, seed.source_ref, seed.expected_signal, datetime('now')
|
||||
from eval_suites
|
||||
join (
|
||||
select 'contract' as suite_key, 'Provider catalog has adapters' as title, 'provider_gateway' as source_type, 'providers' as source_ref, 'adapter_coverage' as expected_signal
|
||||
union all select 'contract', 'Schema ledger is current', 'database', 'schema_migrations', 'migration_ledger'
|
||||
union all select 'integration', 'Approval routes are assigned', 'org_governance', 'approval_assignments', 'route_coverage'
|
||||
union all select 'integration', 'Skills can resolve runtime snapshots', 'skills_platform', 'skill_runtime_snapshots', 'skill_resolution'
|
||||
union all select 'replay', 'Audit timeline has domain events', 'audit', 'audit_events', 'replayable_timeline'
|
||||
union all select 'replay', 'Operational signals are captured', 'operations', 'operational_events', 'slo_signal'
|
||||
) seed on seed.suite_key = eval_suites.key
|
||||
where not exists (
|
||||
select 1 from eval_cases
|
||||
where eval_cases.suite_id = eval_suites.id and eval_cases.title = seed.title
|
||||
);
|
||||
|
||||
insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at)
|
||||
select 'pbg_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.gate_type, 1, datetime('now'), datetime('now')
|
||||
from organizations
|
||||
cross join (
|
||||
select 'static_checks' as key, 'Static checks' as label, 'typecheck_lint_test' as gate_type
|
||||
union all select 'migration_diff', 'Migration diff' as label, 'schema_review' as gate_type
|
||||
union all select 'policy_diff', 'Policy diff' as label, 'policy_review' as gate_type
|
||||
union all select 'human_approval', 'Human approval' as label, 'approval_required' as gate_type
|
||||
) seed
|
||||
where not exists (
|
||||
select 1 from pr_bot_gates
|
||||
where pr_bot_gates.org_id = organizations.id and pr_bot_gates.key = seed.key
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (18, 'sdlc_controls', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,18 @@
|
||||
export const auditReplaySchema = `
|
||||
create table if not exists audit_replay_runs (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
thread_id text not null references chat_threads(id) on delete cascade,
|
||||
event_count integer not null,
|
||||
event_digest text not null,
|
||||
replay_json text not null,
|
||||
created_by_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_audit_replay_runs_org_created on audit_replay_runs(org_id, created_at);
|
||||
create index if not exists idx_audit_replay_runs_thread on audit_replay_runs(thread_id, created_at);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (19, 'audit_replay', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
export const rollbackSnapshotsSchema = `
|
||||
create table if not exists rollback_snapshots (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
migration_report_id text not null references migration_reports(id) on delete cascade,
|
||||
source_database_path text not null,
|
||||
source_fingerprint text not null,
|
||||
source_database_sha256 text not null,
|
||||
workspace_manifest_json text not null,
|
||||
workspace_manifest_sha256 text not null,
|
||||
delta_journal_json text not null,
|
||||
restore_plan_json text not null,
|
||||
active integer not null,
|
||||
created_by_employee_id text references employees(id) on delete set null,
|
||||
created_at text not null,
|
||||
activated_at text
|
||||
);
|
||||
|
||||
create index if not exists idx_rollback_snapshots_org_created on rollback_snapshots(org_id, created_at);
|
||||
create index if not exists idx_rollback_snapshots_report on rollback_snapshots(migration_report_id);
|
||||
create unique index if not exists idx_rollback_snapshots_one_active on rollback_snapshots(org_id) where active = 1;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (20, 'rollback_snapshots', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,59 @@
|
||||
export const providerAuthBrokerSchema = `
|
||||
create table if not exists provider_auth_profiles (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
credential_id text not null references credentials(id) on delete restrict,
|
||||
provider_key text not null,
|
||||
auth_type text not null,
|
||||
profile_kind text not null,
|
||||
display_hint text not null,
|
||||
source_type text not null,
|
||||
source_ref text,
|
||||
capability_json text not null,
|
||||
active integer not null check(active in (0, 1)),
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_provider_auth_profiles_org_provider
|
||||
on provider_auth_profiles(org_id, provider_id, created_at);
|
||||
create index if not exists idx_provider_auth_profiles_credential
|
||||
on provider_auth_profiles(credential_id);
|
||||
create unique index if not exists idx_provider_auth_profiles_active
|
||||
on provider_auth_profiles(provider_id) where active = 1;
|
||||
|
||||
insert into provider_auth_profiles(
|
||||
id, org_id, provider_id, credential_id, provider_key, auth_type,
|
||||
profile_kind, display_hint, source_type, source_ref, capability_json,
|
||||
active, created_at
|
||||
)
|
||||
select
|
||||
'authp_' || lower(hex(randomblob(16))),
|
||||
providers.org_id,
|
||||
providers.id,
|
||||
credentials.id,
|
||||
providers.provider_key,
|
||||
providers.auth_type,
|
||||
providers.auth_type,
|
||||
credentials.display_hint,
|
||||
'migrated_credential',
|
||||
null,
|
||||
'{"providerKey":"' || providers.provider_key || '","authType":"' || providers.auth_type || '","capabilityTags":[]}',
|
||||
1,
|
||||
credentials.created_at
|
||||
from providers
|
||||
join credentials
|
||||
on credentials.id = providers.credential_id
|
||||
and credentials.org_id = providers.org_id
|
||||
and credentials.provider_key = providers.provider_key
|
||||
where providers.credential_id is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from provider_auth_profiles existing_profiles
|
||||
where existing_profiles.provider_id = providers.id
|
||||
and existing_profiles.active = 1
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (21, 'provider_auth_broker', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
export const providerRateLimitObservationSchema = `
|
||||
create table if not exists provider_rate_limit_observations (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
model text,
|
||||
request_limit integer,
|
||||
remaining_requests integer,
|
||||
reset_at text,
|
||||
retry_after_ms integer,
|
||||
raw_state_json text not null,
|
||||
observed_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_provider_rate_limit_observations_org_provider
|
||||
on provider_rate_limit_observations(org_id, provider_id, observed_at desc);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (22, 'provider_rate_limit_observations', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
export const providerRequestRuntimeSchema = `
|
||||
alter table provider_requests add column external_response_id text;
|
||||
alter table provider_requests add column stream_event_count integer not null default 0;
|
||||
alter table provider_requests add column cancelled_at text;
|
||||
|
||||
create index if not exists idx_provider_requests_external_response
|
||||
on provider_requests(provider_id, external_response_id)
|
||||
where external_response_id is not null;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (23, 'provider_request_runtime', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
export const taskRunProviderRequestSchema = `
|
||||
alter table task_runs add column provider_request_id text references provider_requests(id) on delete set null;
|
||||
|
||||
create index if not exists idx_task_runs_provider_request
|
||||
on task_runs(provider_request_id)
|
||||
where provider_request_id is not null;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (24, 'task_run_provider_request', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
export const chatMessageDeliverySchema = `
|
||||
alter table chat_messages add column body_structured_json text not null default '{}';
|
||||
alter table chat_messages add column delivery_state text not null default 'sent';
|
||||
|
||||
create index if not exists idx_chat_messages_delivery_state
|
||||
on chat_messages(org_id, delivery_state, created_at);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (25, 'chat_message_delivery', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,127 @@
|
||||
export const browserPlatformGovernanceSchema = `
|
||||
create table if not exists browser_session_containers (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text not null references social_accounts(id) on delete cascade,
|
||||
platform text not null,
|
||||
partition_key text not null,
|
||||
host_allowlist_json text not null,
|
||||
permission_policy text not null,
|
||||
sandbox_enabled integer not null,
|
||||
created_at text not null,
|
||||
last_used_at text,
|
||||
unique(social_account_id)
|
||||
);
|
||||
|
||||
create table if not exists browser_action_policies (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text not null references social_accounts(id) on delete cascade,
|
||||
action_type text not null,
|
||||
content_category text not null,
|
||||
risk_level text not null,
|
||||
approval_required integer not null,
|
||||
approval_kind text,
|
||||
created_at text not null,
|
||||
unique(social_account_id, action_type, content_category)
|
||||
);
|
||||
|
||||
create table if not exists social_approval_routes (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text not null references social_accounts(id) on delete cascade,
|
||||
action_type text not null,
|
||||
risk_level text not null,
|
||||
approval_kind text not null,
|
||||
route_to_type text not null,
|
||||
route_to_id text,
|
||||
created_at text not null,
|
||||
unique(social_account_id, action_type, risk_level)
|
||||
);
|
||||
|
||||
insert into browser_session_containers(
|
||||
id, org_id, social_account_id, platform, partition_key, host_allowlist_json,
|
||||
permission_policy, sandbox_enabled, created_at, last_used_at
|
||||
)
|
||||
select
|
||||
'bsc_' || lower(hex(randomblob(16))),
|
||||
org_id,
|
||||
id,
|
||||
platform,
|
||||
'persist:agentcompany-social-' || id,
|
||||
case platform
|
||||
when 'x' then '["x.com","twitter.com"]'
|
||||
when 'linkedin' then '["linkedin.com","www.linkedin.com"]'
|
||||
when 'youtube' then '["accounts.google.com","youtube.com","www.youtube.com"]'
|
||||
when 'instagram' then '["instagram.com","www.instagram.com"]'
|
||||
else '[]'
|
||||
end,
|
||||
'deny_all',
|
||||
1,
|
||||
datetime('now'),
|
||||
null
|
||||
from social_accounts
|
||||
where not exists (
|
||||
select 1 from browser_session_containers
|
||||
where browser_session_containers.social_account_id = social_accounts.id
|
||||
);
|
||||
|
||||
insert into browser_action_policies(
|
||||
id, org_id, social_account_id, action_type, content_category, risk_level,
|
||||
approval_required, approval_kind, created_at
|
||||
)
|
||||
select
|
||||
'bap_' || lower(hex(randomblob(16))),
|
||||
social_accounts.org_id,
|
||||
social_accounts.id,
|
||||
policy.action_type,
|
||||
policy.content_category,
|
||||
policy.risk_level,
|
||||
policy.approval_required,
|
||||
policy.approval_kind,
|
||||
datetime('now')
|
||||
from social_accounts
|
||||
join (
|
||||
select 'login' action_type, 'auth' content_category, 'low' risk_level, 0 approval_required, null approval_kind
|
||||
union all select 'draft', 'content', 'low', 0, null
|
||||
union all select 'approve', 'content', 'medium', 1, 'social_publish'
|
||||
) policy
|
||||
where not exists (
|
||||
select 1 from browser_action_policies
|
||||
where browser_action_policies.social_account_id = social_accounts.id
|
||||
and browser_action_policies.action_type = policy.action_type
|
||||
and browser_action_policies.content_category = policy.content_category
|
||||
);
|
||||
|
||||
insert into social_approval_routes(
|
||||
id, org_id, social_account_id, action_type, risk_level, approval_kind,
|
||||
route_to_type, route_to_id, created_at
|
||||
)
|
||||
select
|
||||
'sar_' || lower(hex(randomblob(16))),
|
||||
org_id,
|
||||
id,
|
||||
'approve',
|
||||
'medium',
|
||||
'social_publish',
|
||||
'authorized_employee',
|
||||
authorized_employee_id,
|
||||
datetime('now')
|
||||
from social_accounts
|
||||
where not exists (
|
||||
select 1 from social_approval_routes
|
||||
where social_approval_routes.social_account_id = social_accounts.id
|
||||
and social_approval_routes.action_type = 'approve'
|
||||
and social_approval_routes.risk_level = 'medium'
|
||||
);
|
||||
|
||||
create index if not exists idx_browser_session_containers_org
|
||||
on browser_session_containers(org_id, social_account_id);
|
||||
create index if not exists idx_browser_action_policies_org_account
|
||||
on browser_action_policies(org_id, social_account_id, action_type);
|
||||
create index if not exists idx_social_approval_routes_org_account
|
||||
on social_approval_routes(org_id, social_account_id, action_type);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (26, 'browser_platform_governance', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,62 @@
|
||||
export const socialAuditRecordsSchema = `
|
||||
create table if not exists social_audit_records (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text references social_accounts(id) on delete set null,
|
||||
source_type text not null,
|
||||
source_id text,
|
||||
event_type text not null,
|
||||
actor_type text not null,
|
||||
actor_id text,
|
||||
policy_decision text not null,
|
||||
evidence_file_id text references file_objects(id) on delete set null,
|
||||
metadata_json text not null,
|
||||
created_at text not null,
|
||||
unique(source_type, source_id, event_type)
|
||||
);
|
||||
|
||||
insert into social_audit_records(
|
||||
id, org_id, social_account_id, source_type, source_id, event_type,
|
||||
actor_type, actor_id, policy_decision, evidence_file_id, metadata_json, created_at
|
||||
)
|
||||
select
|
||||
'saud_' || lower(hex(randomblob(16))),
|
||||
org_id,
|
||||
social_account_id,
|
||||
'browser_action',
|
||||
id,
|
||||
action_type,
|
||||
'system',
|
||||
null,
|
||||
policy_decision,
|
||||
evidence_file_id,
|
||||
json_object('source', 'browser_actions'),
|
||||
created_at
|
||||
from browser_actions
|
||||
where not exists (
|
||||
select 1 from social_audit_records
|
||||
where social_audit_records.source_type = 'browser_action'
|
||||
and social_audit_records.source_id = browser_actions.id
|
||||
and social_audit_records.event_type = browser_actions.action_type
|
||||
);
|
||||
|
||||
create index if not exists idx_social_audit_records_org_created
|
||||
on social_audit_records(org_id, created_at);
|
||||
create index if not exists idx_social_audit_records_account_created
|
||||
on social_audit_records(social_account_id, created_at);
|
||||
|
||||
create trigger if not exists trg_social_audit_records_no_update
|
||||
before update on social_audit_records
|
||||
begin
|
||||
select raise(abort, 'Social audit records are immutable.');
|
||||
end;
|
||||
|
||||
create trigger if not exists trg_social_audit_records_no_delete
|
||||
before delete on social_audit_records
|
||||
begin
|
||||
select raise(abort, 'Social audit records are immutable.');
|
||||
end;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (27, 'social_audit_records', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,62 @@
|
||||
export const migrationCutoverJournalSchema = `
|
||||
create table if not exists migration_cutover_events (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
migration_report_id text not null references migration_reports(id) on delete cascade,
|
||||
rollback_snapshot_id text references rollback_snapshots(id) on delete set null,
|
||||
event_type text not null,
|
||||
actor_type text not null,
|
||||
actor_id text,
|
||||
payload_json text not null,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create table if not exists migration_reconciliation_items (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
migration_report_id text not null references migration_reports(id) on delete cascade,
|
||||
rollback_snapshot_id text references rollback_snapshots(id) on delete set null,
|
||||
source_event_id text not null,
|
||||
target_type text not null,
|
||||
target_id text not null,
|
||||
reason text not null,
|
||||
state text not null,
|
||||
payload_json text not null,
|
||||
created_at text not null,
|
||||
resolved_at text,
|
||||
resolved_by_employee_id text references employees(id) on delete set null,
|
||||
unique(rollback_snapshot_id, source_event_id)
|
||||
);
|
||||
|
||||
insert into migration_cutover_events(
|
||||
id, org_id, migration_report_id, rollback_snapshot_id, event_type,
|
||||
actor_type, actor_id, payload_json, created_at
|
||||
)
|
||||
select
|
||||
'mce_' || lower(hex(randomblob(16))),
|
||||
org_id,
|
||||
migration_report_id,
|
||||
id,
|
||||
case active when 1 then 'rollback_snapshot_activated' else 'rollback_snapshot_created' end,
|
||||
'system',
|
||||
null,
|
||||
json_object('sourceFingerprint', source_fingerprint, 'sourceDatabaseSha256', source_database_sha256),
|
||||
created_at
|
||||
from rollback_snapshots
|
||||
where not exists (
|
||||
select 1 from migration_cutover_events
|
||||
where migration_cutover_events.rollback_snapshot_id = rollback_snapshots.id
|
||||
);
|
||||
|
||||
create index if not exists idx_migration_cutover_events_org_created
|
||||
on migration_cutover_events(org_id, created_at);
|
||||
create index if not exists idx_migration_cutover_events_report
|
||||
on migration_cutover_events(migration_report_id, created_at);
|
||||
create index if not exists idx_migration_reconciliation_items_org_state
|
||||
on migration_reconciliation_items(org_id, state, created_at);
|
||||
create index if not exists idx_migration_reconciliation_items_report
|
||||
on migration_reconciliation_items(migration_report_id, created_at);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (28, 'migration_cutover_journal', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,65 @@
|
||||
export const fileHandoffRecordsSchema = `
|
||||
create table if not exists file_handoff_records (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
message_id text not null references chat_messages(id) on delete cascade,
|
||||
file_id text not null references file_objects(id) on delete cascade,
|
||||
source_principal_type text not null,
|
||||
source_principal_id text,
|
||||
target_principal_type text not null,
|
||||
target_principal_id text not null,
|
||||
transfer_type text not null,
|
||||
checksum text not null,
|
||||
scan_state text not null,
|
||||
scan_labels_json text not null,
|
||||
policy_decision text not null,
|
||||
object_ref text not null,
|
||||
created_at text not null,
|
||||
unique(message_id, file_id, target_principal_type, target_principal_id)
|
||||
);
|
||||
|
||||
insert into file_handoff_records(
|
||||
id, org_id, message_id, file_id, source_principal_type, source_principal_id,
|
||||
target_principal_type, target_principal_id, transfer_type, checksum, scan_state,
|
||||
scan_labels_json, policy_decision, object_ref, created_at
|
||||
)
|
||||
select
|
||||
'fhr_' || lower(hex(randomblob(16))),
|
||||
messages.org_id,
|
||||
attachments.message_id,
|
||||
attachments.file_id,
|
||||
messages.sender_type,
|
||||
messages.sender_id,
|
||||
coalesce(grants.granted_to_type, 'org_group'),
|
||||
coalesce(grants.granted_to_id, messages.org_id || ':workspace'),
|
||||
attachments.transfer_type,
|
||||
attachments.checksum,
|
||||
files.scan_state,
|
||||
files.scan_labels_json,
|
||||
case
|
||||
when files.scan_state = 'approved' then 'approved_for_handoff'
|
||||
else 'blocked_by_scan'
|
||||
end,
|
||||
files.storage_path,
|
||||
messages.created_at
|
||||
from chat_attachments attachments
|
||||
join chat_messages messages on messages.id = attachments.message_id
|
||||
join file_objects files on files.id = attachments.file_id
|
||||
left join file_access_grants grants
|
||||
on grants.file_id = attachments.file_id
|
||||
and grants.source_message_id = attachments.message_id
|
||||
where not exists (
|
||||
select 1 from file_handoff_records records
|
||||
where records.message_id = attachments.message_id
|
||||
and records.file_id = attachments.file_id
|
||||
and records.target_principal_type = coalesce(grants.granted_to_type, 'org_group')
|
||||
and records.target_principal_id = coalesce(grants.granted_to_id, messages.org_id || ':workspace')
|
||||
);
|
||||
|
||||
create index if not exists idx_file_handoff_org_created on file_handoff_records(org_id, created_at);
|
||||
create index if not exists idx_file_handoff_file on file_handoff_records(org_id, file_id);
|
||||
create index if not exists idx_file_handoff_target on file_handoff_records(org_id, target_principal_type, target_principal_id);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (29, 'file_handoff_records', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,49 @@
|
||||
export const approvalSourceRefsSchema = `
|
||||
alter table approvals add column source_entity_type text;
|
||||
alter table approvals add column source_entity_id text;
|
||||
|
||||
update approvals
|
||||
set
|
||||
source_entity_type = 'task',
|
||||
source_entity_id = (
|
||||
select tasks.id
|
||||
from tasks
|
||||
where tasks.org_id = approvals.org_id
|
||||
and tasks.title = approvals.title
|
||||
and tasks.description = approvals.details
|
||||
order by tasks.created_at desc
|
||||
limit 1
|
||||
)
|
||||
where kind = 'task_risk'
|
||||
and source_entity_id is null
|
||||
and exists (
|
||||
select 1
|
||||
from tasks
|
||||
where tasks.org_id = approvals.org_id
|
||||
and tasks.title = approvals.title
|
||||
and tasks.description = approvals.details
|
||||
);
|
||||
|
||||
update approvals
|
||||
set
|
||||
source_entity_type = 'scheduled_post',
|
||||
source_entity_id = (
|
||||
select scheduled_posts.id
|
||||
from scheduled_posts
|
||||
where scheduled_posts.approval_id = approvals.id
|
||||
limit 1
|
||||
)
|
||||
where kind = 'social_publish'
|
||||
and source_entity_id is null
|
||||
and exists (
|
||||
select 1
|
||||
from scheduled_posts
|
||||
where scheduled_posts.approval_id = approvals.id
|
||||
);
|
||||
|
||||
create index if not exists idx_approvals_source
|
||||
on approvals(org_id, source_entity_type, source_entity_id, kind, state);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (30, 'approval_source_refs', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
export const providerRequestWorkspaceSchema = `
|
||||
alter table provider_requests add column workspace_id text references workspaces(id) on delete set null;
|
||||
|
||||
create index if not exists idx_provider_requests_workspace
|
||||
on provider_requests(org_id, workspace_id, created_at)
|
||||
where workspace_id is not null;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (31, 'provider_request_workspace', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const browserActionMetadataSchema = `
|
||||
alter table browser_actions add column metadata_json text not null default '{}';
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (32, 'browser_action_metadata', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
export const auditEventImmutabilitySchema = `
|
||||
create trigger if not exists trg_audit_events_no_update
|
||||
before update on audit_events
|
||||
begin
|
||||
select raise(abort, 'Audit events are immutable.');
|
||||
end;
|
||||
|
||||
create trigger if not exists trg_audit_events_no_delete
|
||||
before delete on audit_events
|
||||
begin
|
||||
select raise(abort, 'Audit events are immutable.');
|
||||
end;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (33, 'audit_event_immutability', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,27 @@
|
||||
export const providerAuthSessionsSchema = `
|
||||
create table if not exists provider_auth_sessions (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
employee_id text not null references employees(id) on delete cascade,
|
||||
state text not null,
|
||||
nonce text not null,
|
||||
code_verifier_sha256 text not null,
|
||||
redirect_uri text not null,
|
||||
sign_in_url text not null,
|
||||
partition text not null,
|
||||
created_at text not null,
|
||||
expires_at text not null,
|
||||
completed_at text,
|
||||
unique(provider_id, state)
|
||||
);
|
||||
|
||||
create index if not exists idx_provider_auth_sessions_org_provider
|
||||
on provider_auth_sessions(org_id, provider_id, created_at);
|
||||
create index if not exists idx_provider_auth_sessions_employee
|
||||
on provider_auth_sessions(org_id, employee_id, created_at);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (34, 'provider_auth_sessions', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
export const taskRunIsolationSchema = `
|
||||
alter table task_runs add column execution_context_id text;
|
||||
alter table task_runs add column service_identity_id text references service_identities(id) on delete set null;
|
||||
alter table task_runs add column workspace_mount_json text not null default '[]';
|
||||
alter table task_runs add column secret_scope_json text not null default '{}';
|
||||
|
||||
create unique index if not exists idx_task_runs_execution_context
|
||||
on task_runs(execution_context_id)
|
||||
where execution_context_id is not null;
|
||||
create index if not exists idx_task_runs_service_identity
|
||||
on task_runs(service_identity_id)
|
||||
where service_identity_id is not null;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (35, 'task_run_isolation', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,20 @@
|
||||
export const additionalConnectorsSchema = `
|
||||
insert or ignore into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
|
||||
select 'prv_' || lower(hex(randomblob(16))), organizations.id, 'claude-pro', 'Claude Pro/Max', 'oauth_pkce', 'claude-sonnet-4-5', null, 0, datetime('now')
|
||||
from organizations;
|
||||
|
||||
insert or ignore into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
|
||||
select 'prv_' || lower(hex(randomblob(16))), organizations.id, 'mistral', 'Mistral', 'api_key', 'mistral-large-latest', null, 0, datetime('now')
|
||||
from organizations;
|
||||
|
||||
insert or ignore into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
|
||||
select 'prv_' || lower(hex(randomblob(16))), organizations.id, 'cerebras', 'Cerebras', 'api_key', 'qwen-3-235b-a22b-instruct-2507', null, 0, datetime('now')
|
||||
from organizations;
|
||||
|
||||
insert or ignore into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
|
||||
select 'prv_' || lower(hex(randomblob(16))), organizations.id, 'openrouter', 'OpenRouter', 'api_key', 'anthropic/claude-sonnet-4.5', null, 0, datetime('now')
|
||||
from organizations;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (36, 'additional_connectors', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
export const customProvidersSchema = `
|
||||
insert or ignore into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
|
||||
select 'prv_' || lower(hex(randomblob(16))), organizations.id, 'custom-openai', 'Custom OpenAI-compatible', 'cloud', null, null, 0, datetime('now')
|
||||
from organizations;
|
||||
|
||||
insert or ignore into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
|
||||
select 'prv_' || lower(hex(randomblob(16))), organizations.id, 'custom-anthropic', 'Custom Anthropic-compatible', 'cloud', null, null, 0, datetime('now')
|
||||
from organizations;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (37, 'custom_providers', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,46 @@
|
||||
// Deleting an organization cascades into its `credentials`, but
|
||||
// `provider_auth_profiles.credential_id` referenced `credentials(id)` with
|
||||
// `on delete restrict`, which blocked the cascade and made the whole DELETE
|
||||
// throw — so deleting an organization silently failed. This rebuilds the
|
||||
// (leaf) table with `on delete cascade`, preserving every existing row and
|
||||
// index. Revocation only deactivates a profile (active = 0) and never deletes
|
||||
// a credential row, so cascade has no effect on normal operation.
|
||||
export const providerAuthProfileCascadeSchema = `
|
||||
create table provider_auth_profiles_rebuild (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
provider_id text not null references providers(id) on delete cascade,
|
||||
credential_id text not null references credentials(id) on delete cascade,
|
||||
provider_key text not null,
|
||||
auth_type text not null,
|
||||
profile_kind text not null,
|
||||
display_hint text not null,
|
||||
source_type text not null,
|
||||
source_ref text,
|
||||
capability_json text not null,
|
||||
active integer not null check(active in (0, 1)),
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
insert into provider_auth_profiles_rebuild(
|
||||
id, org_id, provider_id, credential_id, provider_key, auth_type,
|
||||
profile_kind, display_hint, source_type, source_ref, capability_json, active, created_at
|
||||
)
|
||||
select
|
||||
id, org_id, provider_id, credential_id, provider_key, auth_type,
|
||||
profile_kind, display_hint, source_type, source_ref, capability_json, active, created_at
|
||||
from provider_auth_profiles;
|
||||
|
||||
drop table provider_auth_profiles;
|
||||
alter table provider_auth_profiles_rebuild rename to provider_auth_profiles;
|
||||
|
||||
create index if not exists idx_provider_auth_profiles_org_provider
|
||||
on provider_auth_profiles(org_id, provider_id, created_at);
|
||||
create index if not exists idx_provider_auth_profiles_credential
|
||||
on provider_auth_profiles(credential_id);
|
||||
create unique index if not exists idx_provider_auth_profiles_active
|
||||
on provider_auth_profiles(provider_id) where active = 1;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (38, 'provider_auth_profile_cascade', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,28 @@
|
||||
// The audit_events and social_audit_records immutability triggers raised ABORT
|
||||
// on EVERY delete, including the legitimate cascade when an organization is torn
|
||||
// down — so deleting an organization always rolled back. These rebuilt triggers
|
||||
// still block tampering deletes of individual records (the owning org exists),
|
||||
// but allow the cascade once the organization row itself is gone. Direct
|
||||
// deletes of audit rows whose org still exists remain forbidden.
|
||||
export const auditImmutabilityOrgTeardownSchema = `
|
||||
drop trigger if exists trg_audit_events_no_delete;
|
||||
create trigger trg_audit_events_no_delete
|
||||
before delete on audit_events
|
||||
for each row
|
||||
when exists (select 1 from organizations where id = old.org_id)
|
||||
begin
|
||||
select raise(abort, 'Audit events are immutable.');
|
||||
end;
|
||||
|
||||
drop trigger if exists trg_social_audit_records_no_delete;
|
||||
create trigger trg_social_audit_records_no_delete
|
||||
before delete on social_audit_records
|
||||
for each row
|
||||
when exists (select 1 from organizations where id = old.org_id)
|
||||
begin
|
||||
select raise(abort, 'Social audit records are immutable.');
|
||||
end;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (39, 'audit_immutability_org_teardown', datetime('now'));
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
export const documentsKnowledgeAutomationSchema = `
|
||||
create table if not exists documents (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
title text not null,
|
||||
doc_type text not null,
|
||||
content text not null,
|
||||
status text not null,
|
||||
author_agent_id text references agents(id) on delete set null,
|
||||
reviewer_agent_id text references agents(id) on delete set null,
|
||||
project_id text references projects(id) on delete set null,
|
||||
goal_id text references goals(id) on delete set null,
|
||||
task_id text references tasks(id) on delete set null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_documents_org on documents(org_id, doc_type);
|
||||
|
||||
create table if not exists knowledge_entries (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
topic text not null,
|
||||
category text not null,
|
||||
content text not null,
|
||||
importance text not null,
|
||||
source_type text not null,
|
||||
source_id text,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_knowledge_org on knowledge_entries(org_id, category);
|
||||
|
||||
create table if not exists automation_rules (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
trigger_event text not null,
|
||||
condition_json text not null,
|
||||
action_type text not null,
|
||||
action_config_json text not null,
|
||||
priority integer not null,
|
||||
enabled integer not null,
|
||||
last_fired_at text,
|
||||
fire_count integer not null default 0,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_automation_org on automation_rules(org_id, enabled);
|
||||
`;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const taskSubtasksDeleteSchema = `
|
||||
alter table tasks add column parent_task_id text references tasks(id) on delete set null;
|
||||
`;
|
||||
@@ -0,0 +1,37 @@
|
||||
export const approvalNotesMeetingsSprintsSchema = `
|
||||
alter table approvals add column decision_notes text;
|
||||
|
||||
create table if not exists meetings (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
title text not null,
|
||||
meeting_type text not null,
|
||||
status text not null,
|
||||
scheduled_at text,
|
||||
duration_minutes integer not null,
|
||||
agenda text not null,
|
||||
notes text not null,
|
||||
decisions text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_meetings_org on meetings(org_id, scheduled_at);
|
||||
|
||||
create table if not exists sprints (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
goal text not null,
|
||||
status text not null,
|
||||
start_date text not null,
|
||||
end_date text not null,
|
||||
velocity_points integer not null default 0,
|
||||
completed_points integer not null default 0,
|
||||
retrospective text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_sprints_org on sprints(org_id, status);
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
export const composioIntegrationSchema = `
|
||||
create table if not exists composio_settings (
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
key text not null,
|
||||
value text not null,
|
||||
updated_at text not null,
|
||||
primary key (org_id, key)
|
||||
);
|
||||
`;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const mentionWakeStateSchema = `
|
||||
alter table chat_mentions add column wake_state text not null default 'none';
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
export const connectorGrantsSchema = `
|
||||
create table if not exists connector_accounts (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
toolkit_slug text not null,
|
||||
toolkit_name text not null,
|
||||
auth_config_id text not null,
|
||||
status text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null,
|
||||
unique (org_id, toolkit_slug)
|
||||
);
|
||||
|
||||
create table if not exists agent_connector_grants (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
toolkit_slug text not null,
|
||||
granted_by_type text not null,
|
||||
granted_by_id text,
|
||||
created_at text not null,
|
||||
unique (org_id, agent_id, toolkit_slug)
|
||||
);
|
||||
|
||||
create index if not exists idx_connector_grants_agent on agent_connector_grants(org_id, agent_id);
|
||||
`;
|
||||
@@ -1,263 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
export const BASELINE_SCHEMA_VERSION = 56;
|
||||
export const BASELINE_SCHEMA_NAME = "baseline_v56";
|
||||
|
||||
export const BASELINE_TABLE_SQL = [
|
||||
"CREATE TABLE activity (id text primary key, company_id text not null, actor text not null, action text not null, entity_type text not null, entity_id text not null, detail text not null, created_at text not null)",
|
||||
"CREATE TABLE agent_config_revisions (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, agent_id TEXT NOT NULL, revision_number INTEGER NOT NULL, changed_keys TEXT NOT NULL DEFAULT '[]', config_before TEXT NOT NULL, config_after TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'patch', rolled_back_from_revision_id TEXT, created_by_agent_id TEXT, created_by_user_id TEXT, created_at TEXT NOT NULL, FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE agent_message_mentions (\n message_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n company_id TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (message_id, agent_id)\n )",
|
||||
"CREATE TABLE agent_message_projections (\n message_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n projected_at TEXT NOT NULL,\n PRIMARY KEY (message_id, session_id)\n )",
|
||||
"CREATE TABLE agent_message_reads (message_id text not null, agent_id text not null, read_at text not null, primary key (message_id, agent_id))",
|
||||
"CREATE TABLE agent_messages (id text primary key, company_id text not null, from_agent_id text not null, to_agent_id text, channel text not null default 'direct', channel_target_id text, subject text not null, body text not null, priority text not null default 'normal', parent_message_id text, attachments_json text not null default '[]', created_at text not null, sender_kind TEXT NOT NULL DEFAULT 'agent')",
|
||||
"CREATE TABLE agent_skill_assignments (agent_id TEXT NOT NULL, skill_id TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (agent_id, skill_id), FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE, FOREIGN KEY (skill_id) REFERENCES company_skills(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE agents (id text primary key, company_id text not null, name text not null, role text not null, title text not null, status text not null, reports_to text, connector_id text not null, workspace_id text, model text, capabilities text not null, budget_monthly_usd real not null default 0, spent_monthly_usd real not null default 0, metadata_json text not null default '{}', created_at text not null, updated_at text not null, department text, permissions_json TEXT NOT NULL DEFAULT '{}', heartbeat_policy_json TEXT, runtime_state_json TEXT, role_level TEXT NOT NULL DEFAULT 'ic', session_id TEXT, model_strategy TEXT NOT NULL DEFAULT 'explicit', heartbeat_enabled integer not null default 0, heartbeat_interval_sec integer not null default 120, last_heartbeat_at text)",
|
||||
"CREATE TABLE approval_comments (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, approval_id TEXT NOT NULL, author_agent_id TEXT, author_user_id TEXT, body TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (approval_id) REFERENCES approvals(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE approvals (id text primary key, company_id text not null, related_task_id text, requested_by_agent_id text, type text not null, payload_summary text not null, impact_summary text not null, payload_json text, decision_note text not null default '', state text not null, created_at text not null, updated_at text not null, related_agent_id text)",
|
||||
"CREATE TABLE automation_log (id text primary key, company_id text not null, rule_id text not null, rule_name text not null, trigger text not null, action text not null, detail text not null default '', executed_at text not null)",
|
||||
"CREATE TABLE automation_rules (id text primary key, company_id text not null, name text not null, description text not null default '', trigger text not null, conditions_json text not null default '{}', action text not null, action_config_json text not null default '{}', source_department text, target_department text, priority integer not null default 50, status text not null default 'active', execution_count integer not null default 0, last_executed_at text, created_at text not null, updated_at text not null)",
|
||||
"CREATE TABLE browser_actions (id text primary key, company_id text not null references companies(id) on delete cascade, social_account_id text not null references social_accounts(id) on delete cascade, agent_id text not null references agents(id) on delete cascade, task_id text, action_type text not null default 'custom_script', payload_json text not null default '{}', status text not null default 'queued', result_summary text not null default '', screenshot_path text, approval_id text, error_message text, started_at text, finished_at text, created_at text not null default (datetime('now')), updated_at text not null default (datetime('now')))",
|
||||
"CREATE TABLE budget_incidents (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, policy_id TEXT NOT NULL REFERENCES budget_policies(id), scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, metric TEXT NOT NULL, window_kind TEXT NOT NULL, window_start TEXT NOT NULL, window_end TEXT NOT NULL, threshold_type TEXT NOT NULL, amount_limit INTEGER NOT NULL, amount_observed INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'open', resolved_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
|
||||
"CREATE TABLE budget_policies (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, metric TEXT NOT NULL DEFAULT 'billed_cents', window_kind TEXT NOT NULL DEFAULT 'calendar_month_utc', amount INTEGER NOT NULL DEFAULT 0, warn_percent INTEGER NOT NULL DEFAULT 80, hard_stop_enabled INTEGER NOT NULL DEFAULT 1, notify_enabled INTEGER NOT NULL DEFAULT 1, is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
|
||||
"CREATE TABLE companies (id text primary key, name text not null, description text not null, status text not null, created_at text not null, updated_at text not null, auto_approve_hires integer not null default 0, review_deliverables integer not null default 0, execution_mode text not null default 'ai_ceo')",
|
||||
"CREATE TABLE \"company_skills\" (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, key TEXT NOT NULL, slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT, source_type TEXT NOT NULL DEFAULT 'local_path', source_locator TEXT, source_ref TEXT, trust_level TEXT NOT NULL DEFAULT 'markdown_only', compatibility TEXT NOT NULL DEFAULT 'compatible', file_inventory TEXT NOT NULL DEFAULT '[]', metadata TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
|
||||
"CREATE TABLE connector_model_pool_settings (\n company_id TEXT NOT NULL,\n connector_id TEXT NOT NULL,\n disabled_models_json TEXT NOT NULL DEFAULT '[]',\n updated_at TEXT NOT NULL,\n PRIMARY KEY (company_id, connector_id),\n FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE\n )",
|
||||
"CREATE TABLE connectors (id text primary key, label text not null, description text not null, status text not null, command text not null, version text, auth_state text not null, last_checked_at text, last_error text, model text, env_binding_text text not null, notes text not null, updated_at text, capability_json text)",
|
||||
"CREATE TABLE conversation_events (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL,\n actor_type TEXT NOT NULL,\n event_type TEXT NOT NULL,\n body TEXT NOT NULL,\n metadata_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL\n )",
|
||||
"CREATE TABLE conversation_participants (\n thread_id TEXT NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,\n participant_id TEXT NOT NULL,\n participant_type TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'member',\n joined_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, participant_id)\n )",
|
||||
"CREATE TABLE conversation_threads (\n id TEXT PRIMARY KEY,\n company_id TEXT NOT NULL,\n title TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'task',\n related_entity_type TEXT,\n related_entity_id TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )",
|
||||
"CREATE TABLE cost_entries (id text primary key, company_id text not null, run_id text not null, task_id text, agent_id text, connector_id text not null, amount_usd real, unattributed integer not null default 0, created_at text not null, project_id TEXT)",
|
||||
"CREATE TABLE credentials (\n connector_id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n backend TEXT NOT NULL,\n ciphertext BLOB NOT NULL,\n display_hint TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'active',\n expires_at TEXT,\n last_refreshed_at TEXT,\n last_error TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )",
|
||||
"CREATE TABLE document_lifecycle_events (\n id TEXT PRIMARY KEY,\n company_id TEXT NOT NULL,\n document_id TEXT NOT NULL,\n hook_type TEXT NOT NULL,\n status TEXT NOT NULL,\n attempts INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n payload_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n completed_at TEXT\n )",
|
||||
"CREATE TABLE document_revisions (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, document_id TEXT NOT NULL, revision_number INTEGER NOT NULL, body TEXT NOT NULL, change_summary TEXT, created_by_agent_id TEXT, created_by_user_id TEXT, created_at TEXT NOT NULL, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE documents (id text primary key, company_id text not null, type text not null, title text not null, content text not null default '', author_agent_id text, reviewer_agent_id text, project_id text, goal_id text, parent_doc_id text, version integer not null default 1, status text not null default 'draft', tags_json text not null default '[]', created_at text not null, updated_at text not null, latest_revision_id TEXT, source_run_id TEXT)",
|
||||
"CREATE TABLE goals (id text primary key, company_id text not null, title text not null, description text not null, parent_id text, owner_agent_id text, status text not null, created_at text not null, updated_at text not null)",
|
||||
"CREATE TABLE knowledge_base (id text primary key, company_id text not null, category text not null, topic text not null, content text not null, author_agent_id text, importance text not null default 'medium', tags_json text not null default '[]', referenced_entity_type text, referenced_entity_id text, created_at text not null, updated_at text not null, source_run_id TEXT)",
|
||||
"CREATE TABLE meetings (id text primary key, company_id text not null, type text not null, title text not null, organizer_agent_id text, participant_agent_ids text not null default '[]', scheduled_at text not null, duration_minutes integer not null default 30, agenda_json text not null default '[]', notes_json text not null default '[]', decisions_json text not null default '[]', action_items_json text not null default '[]', status text not null default 'scheduled', created_at text not null, updated_at text not null)",
|
||||
"CREATE TABLE model_capabilities (\n provider TEXT NOT NULL,\n model_id TEXT NOT NULL,\n reasoning INTEGER NOT NULL,\n coding INTEGER NOT NULL,\n writing INTEGER NOT NULL,\n tier TEXT NOT NULL,\n cost_per_mtok_out REAL NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (provider, model_id)\n )",
|
||||
"CREATE TABLE projects (id text primary key, company_id text not null, goal_id text, name text not null, description text not null, lead_agent_id text, status text not null, target_date text, created_at text not null, updated_at text not null)",
|
||||
"CREATE TABLE run_events (id INTEGER PRIMARY KEY AUTOINCREMENT, company_id TEXT NOT NULL, run_id TEXT NOT NULL, agent_id TEXT NOT NULL, seq INTEGER NOT NULL, event_type TEXT NOT NULL, stream TEXT, level TEXT, message TEXT, payload TEXT, created_at TEXT NOT NULL, FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE \"runs\" (\n id TEXT PRIMARY KEY,\n company_id TEXT NOT NULL,\n task_id TEXT,\n agent_id TEXT NOT NULL,\n workspace_id TEXT,\n connector_id TEXT NOT NULL,\n status TEXT NOT NULL,\n summary TEXT NOT NULL,\n error_message TEXT,\n exit_code INTEGER,\n signal TEXT,\n model TEXT,\n cost_usd REAL,\n unattributed_cost INTEGER NOT NULL DEFAULT 0,\n actor_type TEXT,\n actor_id TEXT,\n started_at TEXT,\n finished_at TEXT,\n log_path TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n , usage_status TEXT NOT NULL DEFAULT 'pre_first_message')",
|
||||
"CREATE TABLE schema_migrations (version integer primary key, name text not null, applied_at text not null)",
|
||||
"CREATE TABLE secrets (id text primary key, company_id text not null, name text not null, description text not null, backend text not null, ciphertext blob not null, updated_at text not null, created_at text not null, unique(company_id, name))",
|
||||
"CREATE TABLE session_entries (\n id TEXT PRIMARY KEY,\n task_session_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n kind TEXT NOT NULL,\n role TEXT,\n payload_json TEXT NOT NULL,\n is_superseded INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL\n )",
|
||||
"CREATE TABLE settings (key text primary key, value text not null)",
|
||||
"CREATE TABLE social_accounts (id text primary key, company_id text not null references companies(id) on delete cascade, platform text not null default 'other', account_name text not null, display_name text not null default '', profile_url text not null default '', credential_secret_id text, status text not null default 'login_required', require_approval integer not null default 1, metadata_json text not null default '{}', last_used_at text, created_at text not null default (datetime('now')), updated_at text not null default (datetime('now')))",
|
||||
"CREATE TABLE sprints (id text primary key, company_id text not null, name text not null, goal text not null default '', start_date text not null, end_date text not null, status text not null default 'planning', retrospective_notes text not null default '', velocity_points integer not null default 0, completed_points integer not null default 0, created_at text not null, updated_at text not null)",
|
||||
"CREATE TABLE task_approval_links (task_id TEXT NOT NULL, approval_id TEXT NOT NULL, company_id TEXT NOT NULL, linked_by_agent_id TEXT, created_at TEXT NOT NULL, PRIMARY KEY (task_id, approval_id), FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (approval_id) REFERENCES approvals(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE task_comments (id text primary key, company_id text not null, task_id text not null, author_agent_id text, author_name text not null, body text not null, created_at text not null)",
|
||||
"CREATE TABLE task_documents (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, task_id TEXT NOT NULL, document_id TEXT NOT NULL, key TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE)",
|
||||
"CREATE TABLE \"task_sessions\" (\n id TEXT PRIMARY KEY,\n company_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n adapter_type TEXT NOT NULL,\n last_run_id TEXT,\n last_error TEXT,\n run_count INTEGER NOT NULL DEFAULT 0,\n total_input_tokens INTEGER NOT NULL DEFAULT 0,\n total_output_tokens INTEGER NOT NULL DEFAULT 0,\n rotated_at TEXT,\n rotation_reason TEXT,\n handoff_markdown TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )",
|
||||
"CREATE TABLE \"tasks\" (\n id TEXT PRIMARY KEY,\n company_id TEXT NOT NULL,\n project_id TEXT,\n goal_id TEXT,\n parent_id TEXT,\n title TEXT NOT NULL,\n description TEXT NOT NULL,\n assignee_agent_id TEXT,\n workspace_id TEXT,\n status TEXT NOT NULL,\n priority TEXT NOT NULL,\n active_run_id TEXT,\n cost_usd REAL NOT NULL DEFAULT 0,\n task_type TEXT NOT NULL DEFAULT 'general',\n requires_user_review INTEGER NOT NULL DEFAULT 0,\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_failure_at TEXT,\n checkout_run_id TEXT,\n execution_locked_at TEXT,\n started_at TEXT,\n completed_at TEXT,\n cancelled_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n , requires_filesystem INTEGER NOT NULL DEFAULT 0, active_session_id text)",
|
||||
"CREATE TABLE thread_subscriptions (\n thread_id TEXT NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,\n subscriber_id TEXT NOT NULL,\n subscriber_type TEXT NOT NULL,\n notify_on TEXT NOT NULL DEFAULT 'all',\n subscribed_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, subscriber_id)\n )",
|
||||
"CREATE TABLE thread_wait_states (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,\n company_id TEXT NOT NULL,\n waiting_for TEXT NOT NULL,\n related_approval_id TEXT,\n reason TEXT NOT NULL,\n created_at TEXT NOT NULL,\n resolved_at TEXT\n )",
|
||||
"CREATE TABLE wakeup_requests (id TEXT PRIMARY KEY, company_id TEXT NOT NULL, agent_id TEXT NOT NULL, source TEXT NOT NULL, trigger_detail TEXT, reason TEXT, payload TEXT, status TEXT NOT NULL DEFAULT 'queued', coalesced_count INTEGER NOT NULL DEFAULT 0, requested_by_actor_type TEXT, requested_by_actor_id TEXT, idempotency_key TEXT, run_id TEXT, requested_at TEXT NOT NULL, claimed_at TEXT, finished_at TEXT, error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, context_snapshot TEXT)",
|
||||
"CREATE TABLE \"work_products\" (\n id TEXT PRIMARY KEY,\n company_id TEXT NOT NULL,\n project_id TEXT,\n task_id TEXT,\n type TEXT NOT NULL,\n provider TEXT NOT NULL,\n external_id TEXT,\n title TEXT NOT NULL,\n url TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n review_state TEXT NOT NULL DEFAULT 'none',\n is_primary INTEGER NOT NULL DEFAULT 0,\n summary TEXT,\n metadata_json TEXT,\n created_by_run_id TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL, workspace_id TEXT, last_touched_by_run_id TEXT,\n FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE\n )",
|
||||
"CREATE TABLE workflow_pipelines (id text primary key, company_id text not null, name text not null, description text not null default '', steps_json text not null default '[]', trigger_type text not null, trigger_config_json text not null default '{}', status text not null default 'draft', current_step_index integer not null default 0, run_count integer not null default 0, last_run_at text, created_at text not null, updated_at text not null)",
|
||||
"CREATE TABLE workspaces (id text primary key, company_id text not null, name text not null, local_path text not null, repo_url text not null, repo_ref text not null, project_id text, state text not null, is_primary integer not null default 0, created_at text not null, updated_at text not null, lock_holder_run_id text, lock_acquired_at text)",
|
||||
] as const;
|
||||
|
||||
export const BASELINE_INDEX_SQL = [
|
||||
"CREATE INDEX idx_activity_company on activity(company_id)",
|
||||
"CREATE INDEX idx_activity_company_actor_action ON activity(company_id, actor, action)",
|
||||
"CREATE INDEX idx_activity_company_created_at on activity(company_id, created_at desc)",
|
||||
"CREATE INDEX idx_agent_config_rev_agent ON agent_config_revisions(company_id, agent_id, created_at)",
|
||||
"CREATE UNIQUE INDEX idx_agent_config_rev_num ON agent_config_revisions(agent_id, revision_number)",
|
||||
"CREATE INDEX idx_agent_message_mentions_agent\n ON agent_message_mentions(company_id, agent_id, created_at)\n ",
|
||||
"CREATE INDEX idx_agent_message_mentions_message\n ON agent_message_mentions(message_id)\n ",
|
||||
"CREATE INDEX idx_agent_message_projections_session\n ON agent_message_projections(session_id, projected_at)\n ",
|
||||
"CREATE INDEX idx_agent_message_reads_agent on agent_message_reads(agent_id)",
|
||||
"CREATE INDEX idx_agent_messages_channel on agent_messages(channel)",
|
||||
"CREATE INDEX idx_agent_messages_company on agent_messages(company_id)",
|
||||
"CREATE INDEX idx_agent_messages_company_created_at on agent_messages(company_id, created_at desc)",
|
||||
"CREATE INDEX idx_agent_messages_from on agent_messages(from_agent_id)",
|
||||
"CREATE INDEX idx_agent_messages_to on agent_messages(to_agent_id)",
|
||||
"CREATE INDEX idx_agent_skill_assignments_skill ON agent_skill_assignments(skill_id)",
|
||||
"CREATE INDEX idx_agents_company on agents(company_id)",
|
||||
"CREATE INDEX idx_agents_company_status on agents(company_id, status)",
|
||||
"CREATE INDEX idx_agents_department on agents(company_id, department)",
|
||||
"CREATE INDEX idx_agents_reports_to on agents(reports_to)",
|
||||
"CREATE INDEX idx_agents_session_id ON agents(session_id)",
|
||||
"CREATE INDEX idx_approval_comments_approval ON approval_comments(approval_id, created_at)",
|
||||
"CREATE INDEX idx_approvals_company on approvals(company_id)",
|
||||
"CREATE INDEX idx_approvals_company_created_at ON approvals(company_id, created_at DESC)",
|
||||
"CREATE INDEX idx_approvals_company_state on approvals(company_id, state)",
|
||||
"CREATE INDEX idx_approvals_company_state_type ON approvals(company_id, state, type)",
|
||||
"CREATE INDEX idx_approvals_related_task ON approvals(related_task_id)",
|
||||
"CREATE INDEX idx_automation_log_company on automation_log(company_id)",
|
||||
"CREATE INDEX idx_automation_rules_company on automation_rules(company_id)",
|
||||
"CREATE INDEX idx_automation_rules_company_status on automation_rules(company_id, status)",
|
||||
"CREATE INDEX idx_automation_rules_trigger on automation_rules(trigger)",
|
||||
"CREATE INDEX idx_browser_actions_account on browser_actions(social_account_id)",
|
||||
"CREATE INDEX idx_browser_actions_company on browser_actions(company_id)",
|
||||
"CREATE INDEX idx_budget_incidents_company_scope ON budget_incidents(company_id, scope_type, scope_id, status)",
|
||||
"CREATE INDEX idx_budget_incidents_company_status ON budget_incidents(company_id, status)",
|
||||
"CREATE INDEX idx_budget_incidents_policy ON budget_incidents(policy_id, window_start, threshold_type)",
|
||||
"CREATE INDEX idx_budget_policies_company_scope ON budget_policies(company_id, scope_type, scope_id, is_active)",
|
||||
"CREATE UNIQUE INDEX idx_budget_policies_unique ON budget_policies(company_id, scope_type, scope_id, metric, window_kind)",
|
||||
"CREATE INDEX idx_comments_company on task_comments(company_id)",
|
||||
"CREATE INDEX idx_comments_task on task_comments(task_id)",
|
||||
"CREATE UNIQUE INDEX idx_company_skills_company_key ON company_skills(company_id, key)",
|
||||
"CREATE INDEX idx_company_skills_company_name ON company_skills(company_id, name)",
|
||||
"CREATE INDEX idx_connector_model_pool_company\n ON connector_model_pool_settings(company_id)",
|
||||
"CREATE INDEX idx_conv_events_thread ON conversation_events(thread_id, created_at)",
|
||||
"CREATE INDEX idx_conv_participants_participant ON conversation_participants(participant_id)",
|
||||
"CREATE INDEX idx_conv_threads_company ON conversation_threads(company_id)",
|
||||
"CREATE INDEX idx_conv_threads_entity ON conversation_threads(related_entity_type, related_entity_id)",
|
||||
"CREATE INDEX idx_cost_entries_agent_created_at ON cost_entries(agent_id, created_at DESC)",
|
||||
"CREATE INDEX idx_cost_entries_company on cost_entries(company_id)",
|
||||
"CREATE INDEX idx_cost_entries_company_created_at on cost_entries(company_id, created_at desc)",
|
||||
"CREATE INDEX idx_cost_entries_project ON cost_entries(project_id)",
|
||||
"CREATE INDEX idx_cost_entries_run on cost_entries(run_id)",
|
||||
"CREATE INDEX idx_credentials_status ON credentials(status)",
|
||||
"CREATE INDEX idx_doc_rev_company ON document_revisions(company_id, document_id, created_at)",
|
||||
"CREATE UNIQUE INDEX idx_doc_rev_doc_num ON document_revisions(document_id, revision_number)",
|
||||
"CREATE INDEX idx_document_lifecycle_events_company\n ON document_lifecycle_events(company_id, status, updated_at)\n ",
|
||||
"CREATE INDEX idx_document_lifecycle_events_document\n ON document_lifecycle_events(document_id, hook_type)\n ",
|
||||
"CREATE INDEX idx_documents_author on documents(author_agent_id)",
|
||||
"CREATE INDEX idx_documents_company on documents(company_id)",
|
||||
"CREATE INDEX idx_documents_company_updated_at ON documents(company_id, updated_at DESC)",
|
||||
"CREATE INDEX idx_documents_project on documents(project_id)",
|
||||
"CREATE INDEX idx_goals_company on goals(company_id)",
|
||||
"CREATE INDEX idx_knowledge_category on knowledge_base(category)",
|
||||
"CREATE INDEX idx_knowledge_company on knowledge_base(company_id)",
|
||||
"CREATE INDEX idx_meetings_company on meetings(company_id)",
|
||||
"CREATE INDEX idx_meetings_company_scheduled_at ON meetings(company_id, scheduled_at DESC)",
|
||||
"CREATE INDEX idx_meetings_organizer on meetings(organizer_agent_id)",
|
||||
"CREATE INDEX idx_messages_channel on agent_messages(channel, channel_target_id)",
|
||||
"CREATE INDEX idx_messages_company on agent_messages(company_id)",
|
||||
"CREATE INDEX idx_model_capabilities_tier ON model_capabilities(tier)",
|
||||
"CREATE INDEX idx_projects_company on projects(company_id)",
|
||||
"CREATE INDEX idx_run_events_company ON run_events(company_id, run_id)",
|
||||
"CREATE INDEX idx_run_events_run ON run_events(run_id, seq)",
|
||||
"CREATE UNIQUE INDEX idx_run_events_run_seq_unique ON run_events(run_id, seq)",
|
||||
"CREATE INDEX idx_runs_agent ON runs(agent_id)",
|
||||
"CREATE INDEX idx_runs_agent_created_at ON runs(agent_id, created_at DESC)",
|
||||
"CREATE INDEX idx_runs_company ON runs(company_id)",
|
||||
"CREATE INDEX idx_runs_company_created_at ON runs(company_id, created_at DESC)",
|
||||
"CREATE INDEX idx_runs_company_status ON runs(company_id, status)",
|
||||
"CREATE INDEX idx_runs_task ON runs(task_id)",
|
||||
"CREATE INDEX idx_secrets_company on secrets(company_id)",
|
||||
"CREATE INDEX idx_session_entries_active ON session_entries(task_session_id, is_superseded, sequence)",
|
||||
"CREATE INDEX idx_session_entries_session ON session_entries(task_session_id, sequence)",
|
||||
"CREATE INDEX idx_social_accounts_company on social_accounts(company_id)",
|
||||
"CREATE INDEX idx_sprints_company on sprints(company_id)",
|
||||
"CREATE INDEX idx_sprints_company_start_date ON sprints(company_id, start_date DESC)",
|
||||
"CREATE INDEX idx_task_approval_links_approval ON task_approval_links(approval_id)",
|
||||
"CREATE INDEX idx_task_approval_links_task ON task_approval_links(task_id)",
|
||||
"CREATE UNIQUE INDEX idx_task_documents_doc ON task_documents(document_id)",
|
||||
"CREATE UNIQUE INDEX idx_task_documents_task_key ON task_documents(company_id, task_id, key)",
|
||||
"CREATE INDEX idx_task_sessions_company_agent\n ON task_sessions(company_id, agent_id, updated_at)",
|
||||
"CREATE INDEX idx_task_sessions_lookup\n ON task_sessions(company_id, agent_id, adapter_type)",
|
||||
"CREATE INDEX idx_tasks_assignee ON tasks(assignee_agent_id)",
|
||||
"CREATE INDEX idx_tasks_company ON tasks(company_id)",
|
||||
"CREATE INDEX idx_tasks_company_status ON tasks(company_id, status)",
|
||||
"CREATE INDEX idx_tasks_project ON tasks(project_id)",
|
||||
"CREATE INDEX idx_thread_subs_subscriber ON thread_subscriptions(subscriber_id)",
|
||||
"CREATE INDEX idx_thread_wait_company ON thread_wait_states(company_id, resolved_at)",
|
||||
"CREATE INDEX idx_thread_wait_thread ON thread_wait_states(thread_id)",
|
||||
"CREATE INDEX idx_wakeup_requests_agent ON wakeup_requests(agent_id, company_id, status)",
|
||||
"CREATE INDEX idx_wakeup_requests_agent_requested ON wakeup_requests(agent_id, requested_at)",
|
||||
"CREATE INDEX idx_wakeup_requests_company_agent_status ON wakeup_requests(company_id, agent_id, status)",
|
||||
"CREATE INDEX idx_wakeup_requests_company_requested ON wakeup_requests(company_id, requested_at)",
|
||||
"CREATE UNIQUE INDEX idx_work_products_identity\n ON work_products(company_id, workspace_id, provider, external_id)\n WHERE workspace_id IS NOT NULL AND external_id IS NOT NULL\n ",
|
||||
"CREATE INDEX idx_work_products_last_touched\n ON work_products(company_id, last_touched_by_run_id)\n ",
|
||||
"CREATE UNIQUE INDEX idx_work_products_primary_active\n ON work_products(company_id, task_id, type)\n WHERE task_id IS NOT NULL\n AND is_primary = 1\n AND status <> 'archived'",
|
||||
"CREATE INDEX idx_work_products_task\n ON work_products(company_id, task_id, type)",
|
||||
"CREATE INDEX idx_work_products_updated\n ON work_products(company_id, updated_at)",
|
||||
"CREATE INDEX idx_workflows_company on workflow_pipelines(company_id)",
|
||||
"CREATE INDEX idx_workspaces_company on workspaces(company_id)",
|
||||
"CREATE UNIQUE INDEX secrets_company_name_idx on secrets(company_id, name)",
|
||||
] as const;
|
||||
|
||||
function quoteIdentifier(name: string): string {
|
||||
return `"${name.replaceAll("\"", "\"\"")}"`;
|
||||
}
|
||||
|
||||
function parseSchemaName(sql: string, prefix: RegExp, kind: string): string {
|
||||
const match = prefix.exec(sql);
|
||||
if (!match) throw new Error(`Cannot parse baseline ${kind} name from SQL: ${sql}`);
|
||||
return match[1] ?? match[2]!;
|
||||
}
|
||||
|
||||
export function parseBaselineTableName(sql: string): string {
|
||||
return parseSchemaName(
|
||||
sql,
|
||||
/^CREATE TABLE(?: IF NOT EXISTS)? (?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/,
|
||||
"table",
|
||||
);
|
||||
}
|
||||
|
||||
export function parseBaselineIndexName(sql: string): string {
|
||||
return parseSchemaName(
|
||||
sql,
|
||||
/^CREATE (?:UNIQUE )?INDEX(?: IF NOT EXISTS)? (?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/,
|
||||
"index",
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeSchemaSql(sql: string): string {
|
||||
return sql
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/\(\s+/g, "(")
|
||||
.replace(/\s+\)/g, ")")
|
||||
.replace(/^CREATE TABLE "([^"]+)"/, "CREATE TABLE $1")
|
||||
.replace(/^CREATE INDEX "([^"]+)"/, "CREATE INDEX $1")
|
||||
.replace(/^CREATE UNIQUE INDEX "([^"]+)"/, "CREATE UNIQUE INDEX $1")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function ifNotExists(sql: string): string {
|
||||
return sql
|
||||
.replace(/^CREATE TABLE /, "CREATE TABLE IF NOT EXISTS ")
|
||||
.replace(/^CREATE INDEX /, "CREATE INDEX IF NOT EXISTS ")
|
||||
.replace(/^CREATE UNIQUE INDEX /, "CREATE UNIQUE INDEX IF NOT EXISTS ");
|
||||
}
|
||||
|
||||
export function installBaselineTables(db: DatabaseSync): void {
|
||||
db.exec(BASELINE_TABLE_SQL.map(ifNotExists).join(";\n"));
|
||||
}
|
||||
|
||||
export function installBaselineIndexes(db: DatabaseSync): void {
|
||||
db.exec(BASELINE_INDEX_SQL.map(ifNotExists).join(";\n"));
|
||||
}
|
||||
|
||||
export function installCanonicalBaselineIndexes(db: DatabaseSync): void {
|
||||
for (const sql of BASELINE_INDEX_SQL) {
|
||||
db.exec(`DROP INDEX IF EXISTS ${quoteIdentifier(parseBaselineIndexName(sql))}`);
|
||||
}
|
||||
db.exec(BASELINE_INDEX_SQL.join(";\n"));
|
||||
}
|
||||
|
||||
export function installBaselineSchema(db: DatabaseSync): void {
|
||||
installBaselineTables(db);
|
||||
installBaselineIndexes(db);
|
||||
}
|
||||
|
||||
export function assertBaselineSchema(db: DatabaseSync): void {
|
||||
assertSchemaEntries(
|
||||
db,
|
||||
"table",
|
||||
BASELINE_TABLE_SQL.map((sql) => [parseBaselineTableName(sql), normalizeSchemaSql(sql)] as const),
|
||||
);
|
||||
assertSchemaEntries(
|
||||
db,
|
||||
"index",
|
||||
BASELINE_INDEX_SQL.map((sql) => [parseBaselineIndexName(sql), normalizeSchemaSql(sql)] as const),
|
||||
);
|
||||
}
|
||||
|
||||
function assertSchemaEntries(
|
||||
db: DatabaseSync,
|
||||
type: "table" | "index",
|
||||
expectedEntries: ReadonlyArray<readonly [string, string]>,
|
||||
): void {
|
||||
const selectSql = db.prepare("select sql from sqlite_master where type = ? and name = ?");
|
||||
for (const [name, expectedSql] of expectedEntries) {
|
||||
const row = selectSql.get(type, name) as { sql: string | null } | undefined;
|
||||
const actualSql = row?.sql ? normalizeSchemaSql(row.sql) : null;
|
||||
if (actualSql !== expectedSql) {
|
||||
throw new Error(`Baseline v56 ${type} drift for ${name}: expected ${expectedSql}; actual ${actualSql ?? "missing"}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import {
|
||||
BASELINE_SCHEMA_NAME,
|
||||
BASELINE_SCHEMA_VERSION,
|
||||
BASELINE_TABLE_SQL,
|
||||
installCanonicalBaselineIndexes,
|
||||
installBaselineTables,
|
||||
normalizeSchemaSql,
|
||||
parseBaselineTableName,
|
||||
} from "./baseline-v56";
|
||||
import {
|
||||
columnExists,
|
||||
nowIso,
|
||||
tableExists,
|
||||
type ReadAll,
|
||||
} from "./types";
|
||||
|
||||
type ColumnInfo = {
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: number;
|
||||
dflt_value: string | null;
|
||||
pk: number;
|
||||
};
|
||||
|
||||
type LegacyDeps = {
|
||||
db: DatabaseSync;
|
||||
readAll: ReadAll;
|
||||
};
|
||||
|
||||
const BASELINE_TABLE_NAMES = BASELINE_TABLE_SQL.map((sql) => parseTableName(sql));
|
||||
const EXPECTED_DROPPED_COLUMNS: Readonly<Record<string, ReadonlySet<string>>> = {
|
||||
agent_messages: new Set(["read_at"]),
|
||||
company_skills: new Set(["markdown"]),
|
||||
runs: new Set(["context_snapshot_json", "session_display_id"]),
|
||||
tasks: new Set(["is_heartbeat", "phase", "session_display_id", "session_json"]),
|
||||
task_sessions: new Set(["session_display_id", "session_params_json", "task_id"]),
|
||||
};
|
||||
|
||||
export function markBaselineSchema(db: DatabaseSync): void {
|
||||
db.prepare("delete from schema_migrations where version <> ?").run(BASELINE_SCHEMA_VERSION);
|
||||
db
|
||||
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
|
||||
.run(BASELINE_SCHEMA_VERSION, BASELINE_SCHEMA_NAME, nowIso());
|
||||
}
|
||||
|
||||
export function upgradeLegacyProfileToBaselineV56({ db, readAll }: LegacyDeps): void {
|
||||
installBaselineTables(db);
|
||||
migrateLegacySecrets(db);
|
||||
migratePendingWakes(db, readAll);
|
||||
migrateAgentBudgets(db, readAll);
|
||||
|
||||
for (const tableName of BASELINE_TABLE_NAMES) {
|
||||
if (tableName === "schema_migrations") continue;
|
||||
if (tableName === "runs") {
|
||||
backfillRunsUsageStatus(db);
|
||||
}
|
||||
if (tableName === "tasks") {
|
||||
migrateTaskSessions(db, readAll);
|
||||
backfillRequiresFilesystem(db);
|
||||
removeSyntheticHeartbeatTasks(db);
|
||||
}
|
||||
if (tableName === "work_products") {
|
||||
detachDanglingWorkProducts(db);
|
||||
backfillWorkProductProvenance(db);
|
||||
normalizeWorkProductPrimaries(db);
|
||||
}
|
||||
rebuildTableToBaseline(db, tableName);
|
||||
}
|
||||
|
||||
db.exec("drop table if exists pending_wakes");
|
||||
db.exec("drop table if exists task_leases");
|
||||
db.exec("drop trigger if exists trg_tasks_phase_from_status_insert");
|
||||
db.exec("drop trigger if exists trg_tasks_phase_from_status_update");
|
||||
installCanonicalBaselineIndexes(db);
|
||||
assertNoForeignKeyViolations(db);
|
||||
markBaselineSchema(db);
|
||||
}
|
||||
|
||||
function parseTableName(sql: string): string {
|
||||
return parseBaselineTableName(sql);
|
||||
}
|
||||
|
||||
function quoteIdentifier(name: string): string {
|
||||
return `"${name.replaceAll("\"", "\"\"")}"`;
|
||||
}
|
||||
|
||||
function tableSql(tableName: string): string {
|
||||
const sql = BASELINE_TABLE_SQL.find((entry) => parseTableName(entry) === tableName);
|
||||
if (!sql) throw new Error(`Missing baseline SQL for table ${tableName}`);
|
||||
return sql;
|
||||
}
|
||||
|
||||
function tableColumns(db: DatabaseSync, tableName: string): ColumnInfo[] {
|
||||
return db.prepare(`pragma table_info(${quoteIdentifier(tableName)})`).all() as ColumnInfo[];
|
||||
}
|
||||
|
||||
function createSqlForTable(sql: string, tableName: string): string {
|
||||
return sql.replace(/^CREATE TABLE (?:"[^"]+"|[A-Za-z_][A-Za-z0-9_]*)/, `CREATE TABLE ${quoteIdentifier(tableName)}`);
|
||||
}
|
||||
|
||||
function rebuildTableToBaseline(db: DatabaseSync, tableName: string): void {
|
||||
if (!tableExists(db, tableName)) return;
|
||||
if (isBaselineTableDefinition(db, tableName)) return;
|
||||
|
||||
const tempTableName = `__baseline_${tableName}_${process.pid}`;
|
||||
db.exec(`drop table if exists ${quoteIdentifier(tempTableName)}`);
|
||||
db.exec(createSqlForTable(tableSql(tableName), tempTableName));
|
||||
|
||||
const beforeCount = (db.prepare(`select count(*) as count from ${quoteIdentifier(tableName)}`).get() as { count: number }).count;
|
||||
const sourceColumns = new Set(tableColumns(db, tableName).map((column) => column.name));
|
||||
const targetColumns = tableColumns(db, tempTableName);
|
||||
assertDroppedColumnsAreExpected(tableName, sourceColumns, new Set(targetColumns.map((column) => column.name)));
|
||||
const insertColumns = targetColumns.map((column) => quoteIdentifier(column.name));
|
||||
const selectExpressions = targetColumns.map((column) => {
|
||||
if (sourceColumns.has(column.name)) return quoteIdentifier(column.name);
|
||||
if (column.dflt_value !== null) return column.dflt_value;
|
||||
if (column.notnull === 0) return "NULL";
|
||||
if (beforeCount === 0) return "NULL";
|
||||
throw new Error(`Cannot rebuild ${tableName}: missing required column ${column.name}`);
|
||||
});
|
||||
|
||||
db.exec(`insert into ${quoteIdentifier(tempTableName)} (${insertColumns.join(", ")}) select ${selectExpressions.join(", ")} from ${quoteIdentifier(tableName)}`);
|
||||
const afterCount = (db.prepare(`select count(*) as count from ${quoteIdentifier(tempTableName)}`).get() as { count: number }).count;
|
||||
if (afterCount !== beforeCount) {
|
||||
throw new Error(`Cannot rebuild ${tableName}: copied ${afterCount} of ${beforeCount} rows`);
|
||||
}
|
||||
|
||||
db.exec(`drop table ${quoteIdentifier(tableName)}`);
|
||||
db.exec(`alter table ${quoteIdentifier(tempTableName)} rename to ${quoteIdentifier(tableName)}`);
|
||||
}
|
||||
|
||||
function isBaselineTableDefinition(db: DatabaseSync, tableName: string): boolean {
|
||||
const row = db
|
||||
.prepare("select sql from sqlite_master where type = 'table' and name = ?")
|
||||
.get(tableName) as { sql: string | null } | undefined;
|
||||
if (!row?.sql) return false;
|
||||
return normalizeSchemaSql(row.sql) === normalizeSchemaSql(tableSql(tableName));
|
||||
}
|
||||
|
||||
function assertDroppedColumnsAreExpected(tableName: string, sourceColumns: Set<string>, targetColumns: Set<string>): void {
|
||||
const allowed = EXPECTED_DROPPED_COLUMNS[tableName] ?? new Set<string>();
|
||||
const unexpected = [...sourceColumns]
|
||||
.filter((column) => !targetColumns.has(column))
|
||||
.filter((column) => !allowed.has(column));
|
||||
if (unexpected.length > 0) {
|
||||
throw new Error(`Cannot rebuild ${tableName}: unexpected legacy columns would be dropped: ${unexpected.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLegacySecrets(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "secrets") || columnExists(db, "secrets", "company_id")) return;
|
||||
|
||||
const currentCompany = db.prepare("select value from settings where key = 'current_company_id'").get() as { value: string } | undefined;
|
||||
const firstCompany = db.prepare("select id from companies order by created_at asc limit 1").get() as { id: string } | undefined;
|
||||
const companyId = currentCompany?.value ?? firstCompany?.id;
|
||||
if (!companyId) {
|
||||
throw new Error("Cannot migrate legacy secrets table: no company row exists.");
|
||||
}
|
||||
|
||||
db.exec(`
|
||||
alter table secrets rename to secrets_legacy;
|
||||
create table secrets (id text primary key, company_id text not null, name text not null, description text not null, backend text not null, ciphertext blob not null, updated_at text not null, created_at text not null, unique(company_id, name));
|
||||
`);
|
||||
db
|
||||
.prepare("insert into secrets (id, company_id, name, description, backend, ciphertext, updated_at, created_at) select id, ?, name, description, backend, ciphertext, updated_at, created_at from secrets_legacy")
|
||||
.run(companyId);
|
||||
db.exec("drop table secrets_legacy");
|
||||
}
|
||||
|
||||
function migratePendingWakes(db: DatabaseSync, readAll: ReadAll): void {
|
||||
if (!tableExists(db, "pending_wakes")) return;
|
||||
const rows = readAll<{ id: string; companyId: string; agentId: string; triggerReason: string; createdAt: string }>(
|
||||
"select id, company_id as companyId, agent_id as agentId, trigger_reason as triggerReason, created_at as createdAt from pending_wakes",
|
||||
);
|
||||
const insert = db.prepare(`
|
||||
insert or ignore into wakeup_requests (id, company_id, agent_id, source, trigger_detail, status, coalesced_count, requested_at, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)
|
||||
`);
|
||||
const ts = nowIso();
|
||||
for (const row of rows) {
|
||||
const detail = JSON.stringify([{ trigger: row.triggerReason, at: row.createdAt, detail: row.triggerReason }]);
|
||||
insert.run(row.id, row.companyId, row.agentId, row.triggerReason, detail, row.createdAt, ts, ts);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateAgentBudgets(db: DatabaseSync, readAll: ReadAll): void {
|
||||
if (!tableExists(db, "agents") || !tableExists(db, "budget_policies")) return;
|
||||
if (!columnExists(db, "agents", "budget_monthly_usd")) return;
|
||||
const agents = readAll<{ id: string; companyId: string; budgetMonthlyUsd: number }>(
|
||||
"select id, company_id as companyId, budget_monthly_usd as budgetMonthlyUsd from agents where budget_monthly_usd > 0",
|
||||
);
|
||||
const existing = db.prepare("select id from budget_policies where company_id = ? and scope_type = 'agent' and scope_id = ? and metric = 'billed_cents' and window_kind = 'calendar_month_utc'");
|
||||
const insert = db.prepare(`
|
||||
insert into budget_policies (id, company_id, scope_type, scope_id, metric, window_kind, amount, warn_percent, hard_stop_enabled, notify_enabled, is_active, created_at, updated_at)
|
||||
values (?, ?, 'agent', ?, 'billed_cents', 'calendar_month_utc', ?, 80, 1, 1, 1, ?, ?)
|
||||
`);
|
||||
const ts = nowIso();
|
||||
for (const agent of agents) {
|
||||
if (existing.get(agent.companyId, agent.id)) continue;
|
||||
insert.run(randomUUID(), agent.companyId, agent.id, Math.round(agent.budgetMonthlyUsd * 100), ts, ts);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateTaskSessions(db: DatabaseSync, readAll: ReadAll): void {
|
||||
if (!tableExists(db, "tasks") || !columnExists(db, "tasks", "session_json")) return;
|
||||
if (!columnExists(db, "tasks", "active_session_id")) {
|
||||
db.exec("alter table tasks add column active_session_id text");
|
||||
}
|
||||
|
||||
const rows = readAll<{
|
||||
id: string;
|
||||
companyId: string;
|
||||
assigneeAgentId: string | null;
|
||||
sessionJson: string;
|
||||
sessionDisplayId: string | null;
|
||||
updatedAt: string;
|
||||
connectorId: string | null;
|
||||
}>(`
|
||||
select t.id,
|
||||
t.company_id as companyId,
|
||||
t.assignee_agent_id as assigneeAgentId,
|
||||
t.session_json as sessionJson,
|
||||
${columnExists(db, "tasks", "session_display_id") ? "t.session_display_id" : "NULL"} as sessionDisplayId,
|
||||
t.updated_at as updatedAt,
|
||||
a.connector_id as connectorId
|
||||
from tasks t
|
||||
left join agents a on a.id = t.assignee_agent_id
|
||||
where t.session_json is not null
|
||||
and (t.active_session_id is null or t.active_session_id = '')
|
||||
`);
|
||||
const sessionColumns = new Set(tableColumns(db, "task_sessions").map((column) => column.name));
|
||||
const updateTask = db.prepare("update tasks set active_session_id = ? where id = ?");
|
||||
const existingSessionByTask = sessionColumns.has("task_id")
|
||||
? db.prepare("select id from task_sessions where task_id = ? order by updated_at desc limit 1")
|
||||
: null;
|
||||
const insertColumns = [
|
||||
"id",
|
||||
"company_id",
|
||||
"agent_id",
|
||||
"adapter_type",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
...(sessionColumns.has("task_id") ? ["task_id"] : []),
|
||||
...(sessionColumns.has("session_params_json") ? ["session_params_json"] : []),
|
||||
...(sessionColumns.has("session_display_id") ? ["session_display_id"] : []),
|
||||
];
|
||||
const insert = db.prepare(`
|
||||
insert into task_sessions (${insertColumns.map(quoteIdentifier).join(", ")})
|
||||
values (${insertColumns.map(() => "?").join(", ")})
|
||||
`);
|
||||
for (const row of rows) {
|
||||
JSON.parse(row.sessionJson);
|
||||
const existingSession = existingSessionByTask?.get(row.id) as { id: string } | undefined;
|
||||
if (existingSession) {
|
||||
updateTask.run(existingSession.id, row.id);
|
||||
continue;
|
||||
}
|
||||
if (!row.assigneeAgentId) {
|
||||
throw new Error(`Cannot migrate task session for task ${row.id}: assignee_agent_id is missing.`);
|
||||
}
|
||||
if (!row.connectorId) {
|
||||
throw new Error(`Cannot migrate task session for task ${row.id}: assigned agent connector_id is missing.`);
|
||||
}
|
||||
const sessionId = randomUUID();
|
||||
const values = [
|
||||
sessionId,
|
||||
row.companyId,
|
||||
row.assigneeAgentId,
|
||||
row.connectorId,
|
||||
row.updatedAt,
|
||||
row.updatedAt,
|
||||
...(sessionColumns.has("task_id") ? [row.id] : []),
|
||||
...(sessionColumns.has("session_params_json") ? [row.sessionJson] : []),
|
||||
...(sessionColumns.has("session_display_id") ? [row.sessionDisplayId] : []),
|
||||
];
|
||||
insert.run(...values);
|
||||
updateTask.run(sessionId, row.id);
|
||||
}
|
||||
}
|
||||
|
||||
function backfillRequiresFilesystem(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "tasks")) return;
|
||||
if (!columnExists(db, "tasks", "requires_filesystem")) {
|
||||
db.exec("alter table tasks add column requires_filesystem integer not null default 0");
|
||||
}
|
||||
if (columnExists(db, "tasks", "active_run_id")) {
|
||||
db.exec(`
|
||||
update tasks
|
||||
set requires_filesystem = 1
|
||||
where status = 'in_progress'
|
||||
and active_run_id is not null
|
||||
`);
|
||||
}
|
||||
if (columnExists(db, "tasks", "assignee_agent_id")) {
|
||||
db.exec(`
|
||||
update tasks
|
||||
set requires_filesystem = 1
|
||||
where status in ('todo', 'backlog')
|
||||
and assignee_agent_id is not null
|
||||
and exists (
|
||||
select 1 from agents a
|
||||
where a.id = tasks.assignee_agent_id
|
||||
and (
|
||||
a.role like '%cto%'
|
||||
or a.role like '%engineer%'
|
||||
or a.role like '%developer%'
|
||||
or a.role like '%devops%'
|
||||
or a.role like '%sre%'
|
||||
or a.department = 'engineering'
|
||||
)
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function removeSyntheticHeartbeatTasks(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "tasks") || !columnExists(db, "tasks", "is_heartbeat")) return;
|
||||
if (tableExists(db, "runs")) {
|
||||
db.exec("update runs set task_id = null where task_id in (select id from tasks where is_heartbeat = 1)");
|
||||
}
|
||||
db.exec("delete from tasks where is_heartbeat = 1");
|
||||
}
|
||||
|
||||
function backfillRunsUsageStatus(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "runs")) return;
|
||||
if (!columnExists(db, "runs", "usage_status")) {
|
||||
db.exec("alter table runs add column usage_status text not null default 'pre_first_message'");
|
||||
}
|
||||
if (!columnExists(db, "runs", "unattributed_cost") || !columnExists(db, "runs", "status")) return;
|
||||
db.exec(`
|
||||
update runs
|
||||
set usage_status = case
|
||||
when unattributed_cost = 0 then 'ok'
|
||||
when status in ('cancelled', 'interrupted') then 'aborted_before_usage'
|
||||
else 'pre_first_message'
|
||||
end
|
||||
where usage_status = 'pre_first_message'
|
||||
`);
|
||||
}
|
||||
|
||||
function normalizeWorkProductPrimaries(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "work_products")) return;
|
||||
db.exec(`
|
||||
update work_products
|
||||
set is_primary = 0,
|
||||
updated_at = datetime('now')
|
||||
where id in (
|
||||
select id from (
|
||||
select id,
|
||||
row_number() over (
|
||||
partition by company_id, task_id, type
|
||||
order by updated_at desc, created_at desc, id desc
|
||||
) as rank_in_group
|
||||
from work_products
|
||||
where task_id is not null
|
||||
and is_primary = 1
|
||||
and status <> 'archived'
|
||||
)
|
||||
where rank_in_group > 1
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
function backfillWorkProductProvenance(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "work_products")) return;
|
||||
if (!columnExists(db, "work_products", "workspace_id")) {
|
||||
db.exec("alter table work_products add column workspace_id text");
|
||||
}
|
||||
if (!columnExists(db, "work_products", "last_touched_by_run_id")) {
|
||||
db.exec("alter table work_products add column last_touched_by_run_id text");
|
||||
}
|
||||
if (tableExists(db, "tasks") && columnExists(db, "work_products", "task_id") && columnExists(db, "tasks", "workspace_id")) {
|
||||
db.exec(`
|
||||
update work_products
|
||||
set workspace_id = (
|
||||
select tasks.workspace_id from tasks
|
||||
where tasks.id = work_products.task_id
|
||||
)
|
||||
where workspace_id is null
|
||||
and task_id is not null
|
||||
`);
|
||||
}
|
||||
if (tableExists(db, "runs") && columnExists(db, "work_products", "created_by_run_id") && columnExists(db, "runs", "workspace_id")) {
|
||||
db.exec(`
|
||||
update work_products
|
||||
set workspace_id = (
|
||||
select runs.workspace_id from runs
|
||||
where runs.id = work_products.created_by_run_id
|
||||
)
|
||||
where workspace_id is null
|
||||
and created_by_run_id is not null
|
||||
`);
|
||||
}
|
||||
if (columnExists(db, "work_products", "created_by_run_id")) {
|
||||
db.exec(`
|
||||
update work_products
|
||||
set last_touched_by_run_id = created_by_run_id
|
||||
where last_touched_by_run_id is null
|
||||
and created_by_run_id is not null
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function detachDanglingWorkProducts(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "work_products") || !tableExists(db, "tasks") || !columnExists(db, "work_products", "task_id")) return;
|
||||
db.exec(`
|
||||
update work_products
|
||||
set task_id = null
|
||||
where task_id is not null
|
||||
and not exists (
|
||||
select 1 from tasks
|
||||
where tasks.id = work_products.task_id
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
function assertNoForeignKeyViolations(db: DatabaseSync): void {
|
||||
const violations = db.prepare("pragma foreign_key_check").all() as Array<{
|
||||
table: string;
|
||||
rowid: number | null;
|
||||
parent: string;
|
||||
fkid: number;
|
||||
}>;
|
||||
if (violations.length === 0) return;
|
||||
|
||||
const sample = violations
|
||||
.slice(0, 5)
|
||||
.map((row) => `${row.table}:${row.rowid ?? "unknown"}->${row.parent}#${row.fkid}`)
|
||||
.join(", ");
|
||||
throw new Error(`Cannot migrate legacy profile: ${violations.length} foreign key violations after v56 rebuild (${sample}).`);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
|
||||
|
||||
export type ReadAll = <T>(sql: string, ...params: SQLInputValue[]) => T[];
|
||||
export type Transaction = <T>(operation: () => T) => T;
|
||||
|
||||
export type Migration = {
|
||||
version: number;
|
||||
name: string;
|
||||
sql?: string;
|
||||
fn?: () => void;
|
||||
};
|
||||
|
||||
export type MigrationDeps = {
|
||||
db: DatabaseSync;
|
||||
readAll: ReadAll;
|
||||
};
|
||||
|
||||
export function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
export function columnExists(
|
||||
db: DatabaseSync,
|
||||
table: string,
|
||||
column: string,
|
||||
): boolean {
|
||||
const rows = db.prepare(`pragma table_info(${table})`).all() as Array<{
|
||||
name: string;
|
||||
}>;
|
||||
return rows.some((row) => row.name === column);
|
||||
}
|
||||
|
||||
export function tableExists(db: DatabaseSync, table: string): boolean {
|
||||
const row = db
|
||||
.prepare("select name from sqlite_master where type = 'table' and name = ?")
|
||||
.get(table) as { name: string } | undefined;
|
||||
return !!row;
|
||||
}
|
||||
|
||||
export function addColumnIfMissing(
|
||||
db: DatabaseSync,
|
||||
table: string,
|
||||
column: string,
|
||||
sql: string,
|
||||
): void {
|
||||
if (!columnExists(db, table, column)) db.prepare(sql).run();
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { STALE_LOCK_TIMEOUT_MS } from "../../constants";
|
||||
import { columnExists, nowIso, tableExists } from "./types";
|
||||
|
||||
export const TASK_LEASE_SCHEMA_VERSION = 57;
|
||||
export const TASK_LEASE_SCHEMA_NAME = "task_leases_runtime_ownership";
|
||||
|
||||
const LEASE_SECONDS = Math.floor(STALE_LOCK_TIMEOUT_MS / 1000);
|
||||
|
||||
export function migrateTaskLeasesV57(db: DatabaseSync): void {
|
||||
installTaskLeasesTable(db);
|
||||
backfillTaskLeases(db);
|
||||
dropTaskOwnershipColumns(db);
|
||||
db
|
||||
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
|
||||
.run(TASK_LEASE_SCHEMA_VERSION, TASK_LEASE_SCHEMA_NAME, nowIso());
|
||||
assertTaskLeasesV57(db);
|
||||
}
|
||||
|
||||
export function assertTaskLeasesV57(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "task_leases")) {
|
||||
throw new Error("Schema v57 drift: task_leases table is missing.");
|
||||
}
|
||||
if (!tableExists(db, "tasks")) {
|
||||
throw new Error("Schema v57 drift: tasks table is missing.");
|
||||
}
|
||||
|
||||
const leaseColumns = db
|
||||
.prepare("pragma table_info(task_leases)")
|
||||
.all()
|
||||
.map((row) => row as { name: string; type: string; notnull: number; pk: number });
|
||||
const expectedLeaseColumns = [
|
||||
["task_id", "TEXT", 0, 1],
|
||||
["company_id", "TEXT", 1, 0],
|
||||
["run_id", "TEXT", 1, 0],
|
||||
["agent_id", "TEXT", 1, 0],
|
||||
["acquired_at", "TEXT", 1, 0],
|
||||
["renewed_at", "TEXT", 1, 0],
|
||||
["expires_at", "TEXT", 1, 0],
|
||||
["created_at", "TEXT", 1, 0],
|
||||
["updated_at", "TEXT", 1, 0],
|
||||
];
|
||||
const actualLeaseColumns = leaseColumns.map((column) => [
|
||||
column.name,
|
||||
column.type.toUpperCase(),
|
||||
column.notnull,
|
||||
column.pk,
|
||||
]);
|
||||
if (JSON.stringify(actualLeaseColumns) !== JSON.stringify(expectedLeaseColumns)) {
|
||||
throw new Error("Schema v57 drift: task_leases columns do not match the canonical schema.");
|
||||
}
|
||||
|
||||
const columns = new Set(
|
||||
db.prepare("pragma table_info(tasks)").all().map((row) => (row as { name: string }).name),
|
||||
);
|
||||
for (const column of ["active_run_id", "checkout_run_id", "execution_locked_at", "active_session_id"]) {
|
||||
if (columns.has(column)) {
|
||||
throw new Error(`Schema v57 drift: tasks.${column} should be stored in task_leases.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installTaskLeasesTable(db: DatabaseSync): void {
|
||||
db.exec(`
|
||||
create table if not exists task_leases (
|
||||
task_id text primary key references tasks(id) on delete cascade,
|
||||
company_id text not null,
|
||||
run_id text not null unique references runs(id) on delete cascade,
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
acquired_at text not null,
|
||||
renewed_at text not null,
|
||||
expires_at text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
create index if not exists idx_task_leases_company_agent on task_leases(company_id, agent_id);
|
||||
create index if not exists idx_task_leases_expires_at on task_leases(expires_at);
|
||||
`);
|
||||
}
|
||||
|
||||
function backfillTaskLeases(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "tasks") || !tableExists(db, "runs")) return;
|
||||
if (!columnExists(db, "tasks", "checkout_run_id") && !columnExists(db, "tasks", "active_run_id")) return;
|
||||
|
||||
const runExpr = [
|
||||
columnExists(db, "tasks", "checkout_run_id") ? "t.checkout_run_id" : "NULL",
|
||||
columnExists(db, "tasks", "active_run_id") ? "t.active_run_id" : "NULL",
|
||||
].join(", ");
|
||||
const lockExpr = columnExists(db, "tasks", "execution_locked_at")
|
||||
? "coalesce(t.execution_locked_at, t.updated_at)"
|
||||
: "t.updated_at";
|
||||
const ts = nowIso();
|
||||
|
||||
db.exec(`
|
||||
insert or ignore into task_leases (
|
||||
task_id, company_id, run_id, agent_id, acquired_at, renewed_at, expires_at, created_at, updated_at
|
||||
)
|
||||
select t.id,
|
||||
t.company_id,
|
||||
coalesce(${runExpr}) as run_id,
|
||||
r.agent_id,
|
||||
${lockExpr} as acquired_at,
|
||||
${lockExpr} as renewed_at,
|
||||
strftime('%Y-%m-%dT%H:%M:%fZ', ${lockExpr}, '+${LEASE_SECONDS} seconds') as expires_at,
|
||||
'${ts}' as created_at,
|
||||
'${ts}' as updated_at
|
||||
from tasks t
|
||||
join runs r on r.id = coalesce(${runExpr})
|
||||
where coalesce(${runExpr}) is not null
|
||||
and r.status in ('queued', 'running')
|
||||
`);
|
||||
}
|
||||
|
||||
function dropTaskOwnershipColumns(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "tasks")) return;
|
||||
for (const column of ["active_run_id", "checkout_run_id", "execution_locked_at", "active_session_id"]) {
|
||||
if (columnExists(db, "tasks", column)) {
|
||||
db.exec(`alter table tasks drop column ${column}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { APPROVAL_POLICY_SEED } from "../../approval-policy-definitions";
|
||||
import { nowIso, tableExists } from "./types";
|
||||
|
||||
export const APPROVAL_POLICY_SCHEMA_VERSION = 58;
|
||||
export const APPROVAL_POLICY_SCHEMA_NAME = "approval_policy_table";
|
||||
|
||||
export function migrateApprovalPoliciesV58(db: DatabaseSync): void {
|
||||
installApprovalPoliciesTable(db);
|
||||
seedApprovalPolicies(db);
|
||||
db
|
||||
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
|
||||
.run(APPROVAL_POLICY_SCHEMA_VERSION, APPROVAL_POLICY_SCHEMA_NAME, nowIso());
|
||||
assertApprovalPoliciesV58(db);
|
||||
}
|
||||
|
||||
export function assertApprovalPoliciesV58(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "approval_policies")) {
|
||||
throw new Error("Schema v58 drift: approval_policies table is missing.");
|
||||
}
|
||||
|
||||
const columns = db
|
||||
.prepare("pragma table_info(approval_policies)")
|
||||
.all()
|
||||
.map((row) => row as { name: string; type: string; notnull: number; pk: number });
|
||||
const actualColumns = columns.map((column) => [
|
||||
column.name,
|
||||
column.type.toUpperCase(),
|
||||
column.notnull,
|
||||
column.pk,
|
||||
]);
|
||||
const expectedColumns = [
|
||||
["approval_type", "TEXT", 0, 1],
|
||||
["scope_type", "TEXT", 1, 0],
|
||||
["blocks_execution", "INTEGER", 1, 0],
|
||||
["required_mode", "TEXT", 1, 0],
|
||||
["auto_approvable", "INTEGER", 1, 0],
|
||||
["created_at", "TEXT", 1, 0],
|
||||
["updated_at", "TEXT", 1, 0],
|
||||
];
|
||||
if (JSON.stringify(actualColumns) !== JSON.stringify(expectedColumns)) {
|
||||
throw new Error("Schema v58 drift: approval_policies columns do not match the canonical schema.");
|
||||
}
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
"select approval_type as approvalType, scope_type as scopeType, blocks_execution as blocksExecution, required_mode as requiredMode, auto_approvable as autoApprovable from approval_policies order by approval_type",
|
||||
)
|
||||
.all() as Array<{
|
||||
approvalType: string;
|
||||
scopeType: string;
|
||||
blocksExecution: number;
|
||||
requiredMode: string;
|
||||
autoApprovable: number;
|
||||
}>;
|
||||
const actual = rows.map((row) => ({
|
||||
approvalType: row.approvalType,
|
||||
scopeType: row.scopeType,
|
||||
blocksExecution: row.blocksExecution,
|
||||
requiredMode: row.requiredMode,
|
||||
autoApprovable: row.autoApprovable,
|
||||
}));
|
||||
const expected = [...APPROVAL_POLICY_SEED]
|
||||
.sort((a, b) => a.approvalType.localeCompare(b.approvalType))
|
||||
.map((policy) => ({
|
||||
approvalType: policy.approvalType,
|
||||
scopeType: policy.scopeType,
|
||||
blocksExecution: policy.blocksExecution ? 1 : 0,
|
||||
requiredMode: policy.requiredMode,
|
||||
autoApprovable: policy.autoApprovable ? 1 : 0,
|
||||
}));
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error("Schema v58 drift: approval_policies seed rows do not match the canonical policy.");
|
||||
}
|
||||
}
|
||||
|
||||
function installApprovalPoliciesTable(db: DatabaseSync): void {
|
||||
db.exec(`
|
||||
create table if not exists approval_policies (
|
||||
approval_type text primary key,
|
||||
scope_type text not null check (scope_type in ('requester_or_task', 'related_agent')),
|
||||
blocks_execution integer not null check (blocks_execution in (0, 1)),
|
||||
required_mode text not null check (required_mode in ('any', 'ai_ceo', 'ai_cofounder')),
|
||||
auto_approvable integer not null check (auto_approvable in (0, 1)),
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
function seedApprovalPolicies(db: DatabaseSync): void {
|
||||
const ts = nowIso();
|
||||
const stmt = db.prepare(
|
||||
`insert or replace into approval_policies (
|
||||
approval_type, scope_type, blocks_execution, required_mode, auto_approvable, created_at, updated_at
|
||||
) values (?, ?, ?, ?, ?, coalesce((select created_at from approval_policies where approval_type = ?), ?), ?)`,
|
||||
);
|
||||
for (const policy of APPROVAL_POLICY_SEED) {
|
||||
stmt.run(
|
||||
policy.approvalType,
|
||||
policy.scopeType,
|
||||
policy.blocksExecution ? 1 : 0,
|
||||
policy.requiredMode,
|
||||
policy.autoApprovable ? 1 : 0,
|
||||
policy.approvalType,
|
||||
ts,
|
||||
ts,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { nowIso, tableExists } from "./types";
|
||||
|
||||
export const SOCIAL_COMPANY_SCOPE_SCHEMA_VERSION = 59;
|
||||
export const SOCIAL_COMPANY_SCOPE_SCHEMA_NAME = "social_company_scope";
|
||||
|
||||
const TRIGGER_NAMES = [
|
||||
"trg_social_accounts_credential_secret_scope_insert",
|
||||
"trg_social_accounts_credential_secret_scope_update",
|
||||
"trg_browser_actions_social_account_scope_insert",
|
||||
"trg_browser_actions_social_account_scope_update",
|
||||
] as const;
|
||||
|
||||
export function migrateSocialCompanyScopeV59(db: DatabaseSync): void {
|
||||
cleanupInvalidSocialAccountSecretRefs(db);
|
||||
cleanupInvalidBrowserActionRefs(db);
|
||||
installSocialCompanyScopeTriggers(db);
|
||||
db
|
||||
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
|
||||
.run(SOCIAL_COMPANY_SCOPE_SCHEMA_VERSION, SOCIAL_COMPANY_SCOPE_SCHEMA_NAME, nowIso());
|
||||
assertSocialCompanyScopeV59(db);
|
||||
}
|
||||
|
||||
export function assertSocialCompanyScopeV59(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "social_accounts")) {
|
||||
throw new Error("Schema v59 drift: social_accounts table is missing.");
|
||||
}
|
||||
if (!tableExists(db, "secrets")) {
|
||||
throw new Error("Schema v59 drift: secrets table is missing.");
|
||||
}
|
||||
if (!tableExists(db, "browser_actions")) {
|
||||
throw new Error("Schema v59 drift: browser_actions table is missing.");
|
||||
}
|
||||
const invalid = db
|
||||
.prepare(
|
||||
`select count(*) as count
|
||||
from social_accounts sa
|
||||
where sa.credential_secret_id is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from secrets s
|
||||
where s.id = sa.credential_secret_id
|
||||
and s.company_id = sa.company_id
|
||||
)`,
|
||||
)
|
||||
.get() as { count: number } | undefined;
|
||||
if ((invalid?.count ?? 0) > 0) {
|
||||
throw new Error("Schema v59 drift: social account credential secrets must belong to the same company.");
|
||||
}
|
||||
const invalidActions = db
|
||||
.prepare(
|
||||
`select count(*) as count
|
||||
from browser_actions ba
|
||||
where not exists (
|
||||
select 1
|
||||
from social_accounts sa
|
||||
where sa.id = ba.social_account_id
|
||||
and sa.company_id = ba.company_id
|
||||
)`,
|
||||
)
|
||||
.get() as { count: number } | undefined;
|
||||
if ((invalidActions?.count ?? 0) > 0) {
|
||||
throw new Error("Schema v59 drift: browser actions must reference social accounts in the same company.");
|
||||
}
|
||||
for (const triggerName of TRIGGER_NAMES) {
|
||||
if (!triggerExists(db, triggerName)) {
|
||||
throw new Error(`Schema v59 drift: ${triggerName} trigger is missing.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupInvalidSocialAccountSecretRefs(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "social_accounts") || !tableExists(db, "secrets")) return;
|
||||
db
|
||||
.prepare(
|
||||
`update social_accounts
|
||||
set credential_secret_id = null,
|
||||
updated_at = ?
|
||||
where credential_secret_id is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from secrets s
|
||||
where s.id = social_accounts.credential_secret_id
|
||||
and s.company_id = social_accounts.company_id
|
||||
)`,
|
||||
)
|
||||
.run(nowIso());
|
||||
}
|
||||
|
||||
function cleanupInvalidBrowserActionRefs(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "browser_actions") || !tableExists(db, "social_accounts")) return;
|
||||
db
|
||||
.prepare(
|
||||
`delete from browser_actions
|
||||
where not exists (
|
||||
select 1
|
||||
from social_accounts sa
|
||||
where sa.id = browser_actions.social_account_id
|
||||
and sa.company_id = browser_actions.company_id
|
||||
)`,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
function installSocialCompanyScopeTriggers(db: DatabaseSync): void {
|
||||
for (const triggerName of TRIGGER_NAMES) {
|
||||
db.exec(`drop trigger if exists ${triggerName}`);
|
||||
}
|
||||
db.exec(`
|
||||
create trigger trg_social_accounts_credential_secret_scope_insert
|
||||
before insert on social_accounts
|
||||
when new.credential_secret_id is not null
|
||||
and not exists (
|
||||
select 1 from secrets
|
||||
where id = new.credential_secret_id
|
||||
and company_id = new.company_id
|
||||
)
|
||||
begin
|
||||
select raise(abort, 'social account credential secret must belong to the same company');
|
||||
end;
|
||||
|
||||
create trigger trg_social_accounts_credential_secret_scope_update
|
||||
before update of credential_secret_id, company_id on social_accounts
|
||||
when new.credential_secret_id is not null
|
||||
and not exists (
|
||||
select 1 from secrets
|
||||
where id = new.credential_secret_id
|
||||
and company_id = new.company_id
|
||||
)
|
||||
begin
|
||||
select raise(abort, 'social account credential secret must belong to the same company');
|
||||
end;
|
||||
|
||||
create trigger trg_browser_actions_social_account_scope_insert
|
||||
before insert on browser_actions
|
||||
when not exists (
|
||||
select 1 from social_accounts
|
||||
where id = new.social_account_id
|
||||
and company_id = new.company_id
|
||||
)
|
||||
begin
|
||||
select raise(abort, 'browser action social account must belong to the same company');
|
||||
end;
|
||||
|
||||
create trigger trg_browser_actions_social_account_scope_update
|
||||
before update of social_account_id, company_id on browser_actions
|
||||
when not exists (
|
||||
select 1 from social_accounts
|
||||
where id = new.social_account_id
|
||||
and company_id = new.company_id
|
||||
)
|
||||
begin
|
||||
select raise(abort, 'browser action social account must belong to the same company');
|
||||
end;
|
||||
`);
|
||||
}
|
||||
|
||||
function triggerExists(db: DatabaseSync, name: string): boolean {
|
||||
const row = db
|
||||
.prepare("select name from sqlite_master where type = 'trigger' and name = ? limit 1")
|
||||
.get(name) as { name: string } | undefined;
|
||||
return Boolean(row);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { nowIso, tableExists } from "./types";
|
||||
|
||||
export const FILESYSTEM_TASK_WORKSPACES_SCHEMA_VERSION = 60;
|
||||
export const FILESYSTEM_TASK_WORKSPACES_SCHEMA_NAME = "filesystem_task_workspaces";
|
||||
|
||||
const ACTIVE_TASK_STATUSES = "'backlog', 'todo', 'in_progress', 'blocked'";
|
||||
|
||||
export function migrateFilesystemTaskWorkspacesV60(db: DatabaseSync): void {
|
||||
backfillProjectWorkspaces(db);
|
||||
backfillAssigneeWorkspaces(db);
|
||||
backfillSingleCompanyWorkspaces(db);
|
||||
db
|
||||
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
|
||||
.run(FILESYSTEM_TASK_WORKSPACES_SCHEMA_VERSION, FILESYSTEM_TASK_WORKSPACES_SCHEMA_NAME, nowIso());
|
||||
assertFilesystemTaskWorkspacesV60(db);
|
||||
}
|
||||
|
||||
export function assertFilesystemTaskWorkspacesV60(db: DatabaseSync): void {
|
||||
if (!tableExists(db, "tasks") || !tableExists(db, "workspaces")) {
|
||||
throw new Error("Schema v60 drift: tasks/workspaces tables are missing.");
|
||||
}
|
||||
const unresolved = db
|
||||
.prepare(
|
||||
`select count(*) as count
|
||||
from tasks t
|
||||
where t.requires_filesystem = 1
|
||||
and t.workspace_id is null
|
||||
and t.status in (${ACTIVE_TASK_STATUSES})
|
||||
and (
|
||||
select count(*)
|
||||
from workspaces w
|
||||
where w.company_id = t.company_id
|
||||
) = 1`,
|
||||
)
|
||||
.get() as { count: number } | undefined;
|
||||
if ((unresolved?.count ?? 0) > 0) {
|
||||
throw new Error("Schema v60 drift: single-workspace companies still have unresolved filesystem tasks.");
|
||||
}
|
||||
}
|
||||
|
||||
function backfillProjectWorkspaces(db: DatabaseSync): void {
|
||||
if (!hasRequiredTables(db)) return;
|
||||
db
|
||||
.prepare(
|
||||
`update tasks
|
||||
set workspace_id = (
|
||||
select w.id
|
||||
from workspaces w
|
||||
where w.company_id = tasks.company_id
|
||||
and w.project_id = tasks.project_id
|
||||
order by w.is_primary desc, w.created_at asc
|
||||
limit 1
|
||||
),
|
||||
updated_at = ?
|
||||
where requires_filesystem = 1
|
||||
and workspace_id is null
|
||||
and status in (${ACTIVE_TASK_STATUSES})
|
||||
and project_id is not null
|
||||
and exists (
|
||||
select 1
|
||||
from workspaces w
|
||||
where w.company_id = tasks.company_id
|
||||
and w.project_id = tasks.project_id
|
||||
)`,
|
||||
)
|
||||
.run(nowIso());
|
||||
}
|
||||
|
||||
function backfillAssigneeWorkspaces(db: DatabaseSync): void {
|
||||
if (!hasRequiredTables(db) || !tableExists(db, "agents")) return;
|
||||
db
|
||||
.prepare(
|
||||
`update tasks
|
||||
set workspace_id = (
|
||||
select a.workspace_id
|
||||
from agents a
|
||||
join workspaces w
|
||||
on w.id = a.workspace_id
|
||||
and w.company_id = a.company_id
|
||||
where a.id = tasks.assignee_agent_id
|
||||
and a.company_id = tasks.company_id
|
||||
limit 1
|
||||
),
|
||||
updated_at = ?
|
||||
where requires_filesystem = 1
|
||||
and workspace_id is null
|
||||
and status in (${ACTIVE_TASK_STATUSES})
|
||||
and assignee_agent_id is not null
|
||||
and exists (
|
||||
select 1
|
||||
from agents a
|
||||
join workspaces w
|
||||
on w.id = a.workspace_id
|
||||
and w.company_id = a.company_id
|
||||
where a.id = tasks.assignee_agent_id
|
||||
and a.company_id = tasks.company_id
|
||||
)`,
|
||||
)
|
||||
.run(nowIso());
|
||||
}
|
||||
|
||||
function backfillSingleCompanyWorkspaces(db: DatabaseSync): void {
|
||||
if (!hasRequiredTables(db)) return;
|
||||
db
|
||||
.prepare(
|
||||
`update tasks
|
||||
set workspace_id = (
|
||||
select w.id
|
||||
from workspaces w
|
||||
where w.company_id = tasks.company_id
|
||||
order by w.is_primary desc, w.created_at asc
|
||||
limit 1
|
||||
),
|
||||
updated_at = ?
|
||||
where requires_filesystem = 1
|
||||
and workspace_id is null
|
||||
and status in (${ACTIVE_TASK_STATUSES})
|
||||
and (
|
||||
select count(*)
|
||||
from workspaces w
|
||||
where w.company_id = tasks.company_id
|
||||
) = 1`,
|
||||
)
|
||||
.run(nowIso());
|
||||
}
|
||||
|
||||
function hasRequiredTables(db: DatabaseSync): boolean {
|
||||
return tableExists(db, "tasks") && tableExists(db, "workspaces");
|
||||
}
|
||||
Reference in new issue
Block a user