CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS schema_migrations ( id text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS agents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_user_id text NOT NULL, owner_email text, display_name text NOT NULL, avatar_url text, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'revoked')), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS agents_owner_user_id_idx ON agents(owner_user_id); CREATE INDEX IF NOT EXISTS agents_owner_email_idx ON agents(owner_email); CREATE TABLE IF NOT EXISTS agent_grants ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), agent_id uuid NOT NULL REFERENCES agents(id) ON DELETE CASCADE, workspace_slug text NOT NULL, project_id text NOT NULL DEFAULT '', scopes text[] NOT NULL DEFAULT '{}', mode text NOT NULL DEFAULT 'voluntary' CHECK (mode IN ('voluntary', 'reporting')), created_by_user_id text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(agent_id, workspace_slug, project_id) ); CREATE INDEX IF NOT EXISTS agent_grants_agent_id_idx ON agent_grants(agent_id); CREATE INDEX IF NOT EXISTS agent_grants_workspace_slug_idx ON agent_grants(workspace_slug); CREATE TABLE IF NOT EXISTS agent_tokens ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), agent_id uuid NOT NULL REFERENCES agents(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, name text NOT NULL, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')), expires_at timestamptz, last_used_at timestamptz, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS agent_tokens_agent_id_idx ON agent_tokens(agent_id); CREATE INDEX IF NOT EXISTS agent_tokens_status_idx ON agent_tokens(status); CREATE TABLE IF NOT EXISTS pairing_codes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), agent_id uuid NOT NULL REFERENCES agents(id) ON DELETE CASCADE, code_hash text NOT NULL UNIQUE, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'used', 'expired', 'revoked')), expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), used_at timestamptz ); CREATE INDEX IF NOT EXISTS pairing_codes_agent_id_idx ON pairing_codes(agent_id); CREATE INDEX IF NOT EXISTS pairing_codes_status_idx ON pairing_codes(status); CREATE TABLE IF NOT EXISTS agent_audit_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), agent_id uuid REFERENCES agents(id) ON DELETE SET NULL, event_type text NOT NULL, actor_user_id text, metadata jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS agent_audit_events_agent_id_idx ON agent_audit_events(agent_id); CREATE INDEX IF NOT EXISTS agent_audit_events_event_type_idx ON agent_audit_events(event_type); CREATE TABLE IF NOT EXISTS idempotency_keys ( key text PRIMARY KEY, agent_id uuid REFERENCES agents(id) ON DELETE CASCADE, request_hash text NOT NULL, response_body jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL ); CREATE INDEX IF NOT EXISTS idempotency_keys_expires_at_idx ON idempotency_keys(expires_at);