-- ============================================================================= -- AwoooP Phase 5: MCP Gateway 四表 -- ADR-116(五閘門 enforcement)+ ADR-118(credential isolation) -- 2026-05-04 ogt + Claude Sonnet 4.6 -- ============================================================================= -- 執行順序: -- 1. awooop_mcp_tool_registry — Tool 白名單 -- 2. awooop_mcp_grants — Agent × Tool 授權記錄 -- 3. awooop_mcp_credential_refs — k8s Secret 參照(不儲存明文) -- 4. awooop_mcp_gateway_audit — 每次 gateway call 稽核 -- ============================================================================= BEGIN; -- --------------------------------------------------------------------------- -- 1. awooop_mcp_tool_registry — Tool 白名單(Gate 3: Tool) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS awooop_mcp_tool_registry ( tool_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id) ON DELETE CASCADE, tool_name VARCHAR(128) NOT NULL, tool_type VARCHAR(32) NOT NULL, -- 'builtin' | 'mcp_server' | 'custom' description TEXT, allowed_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- ["read","write","admin"] environment_tags JSONB NOT NULL DEFAULT '{}'::jsonb, -- {"env": "prod"} gate 4 用 is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_tool_type CHECK (tool_type IN ('builtin','mcp_server','custom')), CONSTRAINT chk_allowed_scopes_array CHECK (jsonb_typeof(allowed_scopes) = 'array'), CONSTRAINT uix_tool_registry_project_name UNIQUE (project_id, tool_name) ); CREATE INDEX IF NOT EXISTS idx_mcp_tool_registry_project ON awooop_mcp_tool_registry (project_id, is_active); -- --------------------------------------------------------------------------- -- 2. awooop_mcp_grants — Agent × Tool 授權(Gate 2: Agent + Gate 3: Tool) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS awooop_mcp_grants ( grant_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id) ON DELETE CASCADE, agent_id VARCHAR(128) NOT NULL, -- awooop_agents.agent_id tool_id UUID NOT NULL REFERENCES awooop_mcp_tool_registry(tool_id) ON DELETE CASCADE, granted_by VARCHAR(128) NOT NULL, -- principal(human user / system) granted_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- subset of tool.allowed_scopes expires_at TIMESTAMPTZ, -- NULL = 永不過期 is_revoked BOOLEAN NOT NULL DEFAULT FALSE, revoked_at TIMESTAMPTZ, revoked_by VARCHAR(128), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_grant_scopes_array CHECK (jsonb_typeof(granted_scopes) = 'array'), CONSTRAINT chk_revoke_consistency CHECK ( (is_revoked = FALSE AND revoked_at IS NULL AND revoked_by IS NULL) OR (is_revoked = TRUE AND revoked_at IS NOT NULL) ), CONSTRAINT uix_mcp_grant_agent_tool UNIQUE (project_id, agent_id, tool_id) ); CREATE INDEX IF NOT EXISTS idx_mcp_grants_lookup ON awooop_mcp_grants (project_id, agent_id, tool_id) WHERE is_revoked = FALSE; CREATE INDEX IF NOT EXISTS idx_mcp_grants_expiry ON awooop_mcp_grants (expires_at) WHERE is_revoked = FALSE AND expires_at IS NOT NULL; -- --------------------------------------------------------------------------- -- 3. awooop_mcp_credential_refs — k8s Secret 參照(ADR-118 credential isolation) -- 只儲存 ref 路徑 + sha256 指紋;明文絕不入庫 -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS awooop_mcp_credential_refs ( ref_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tool_id UUID NOT NULL REFERENCES awooop_mcp_tool_registry(tool_id) ON DELETE CASCADE, project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id) ON DELETE CASCADE, -- k8s secret ref:格式 "namespace/secret-name#key" k8s_secret_ref VARCHAR(256) NOT NULL, -- sha256(actual_secret_value) — 用於 audit;不可還原原值 value_sha256 VARCHAR(64), description TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), rotated_at TIMESTAMPTZ, CONSTRAINT chk_k8s_ref_format CHECK (k8s_secret_ref ~ '^[a-z0-9-]+/[a-z0-9-]+#[a-zA-Z0-9_-]+$'), CONSTRAINT chk_value_sha256_hex CHECK (value_sha256 IS NULL OR value_sha256 ~ '^[0-9a-f]{64}$'), CONSTRAINT uix_credential_ref_tool UNIQUE (tool_id, k8s_secret_ref) ); CREATE INDEX IF NOT EXISTS idx_mcp_cred_refs_tool ON awooop_mcp_credential_refs (tool_id) WHERE is_active = TRUE; -- --------------------------------------------------------------------------- -- 4. awooop_mcp_gateway_audit — Gateway call 稽核日誌(ADR-116 P1-09) -- 不儲存 raw input/output;只儲存 hash + 結果狀態 -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS awooop_mcp_gateway_audit ( call_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id VARCHAR(64) NOT NULL, run_id UUID, -- FK soft(run 可能不存在) trace_id VARCHAR(128), agent_id VARCHAR(128), tool_id UUID NOT NULL REFERENCES awooop_mcp_tool_registry(tool_id), tool_name VARCHAR(128) NOT NULL, credential_ref VARCHAR(256), -- k8s_secret_ref 路徑(不含 key value) input_hash VARCHAR(64), -- sha256(canonical input JSON) output_hash VARCHAR(64), -- sha256(canonical output JSON) gate_result JSONB NOT NULL DEFAULT '{}'::jsonb, -- {"gate1_project": true, "gate2_agent": true, "gate3_tool": true, -- "gate4_env": true, "gate5_approval": true} result_status VARCHAR(16) NOT NULL, -- 'success' | 'blocked' | 'failed' | 'timeout' block_gate SMALLINT, -- 哪個 gate 攔截(1-5,NULL=未攔截) block_reason VARCHAR(256), latency_ms INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_gateway_result_status CHECK (result_status IN ('success','blocked','failed','timeout')), CONSTRAINT chk_block_gate_range CHECK (block_gate IS NULL OR (block_gate >= 1 AND block_gate <= 5)), CONSTRAINT chk_input_hash_hex CHECK (input_hash IS NULL OR input_hash ~ '^[0-9a-f]{64}$'), CONSTRAINT chk_output_hash_hex CHECK (output_hash IS NULL OR output_hash ~ '^[0-9a-f]{64}$') ); -- 查詢熱路徑:by project + run CREATE INDEX IF NOT EXISTS idx_mcp_audit_run ON awooop_mcp_gateway_audit (project_id, run_id, created_at DESC); -- 查詢熱路徑:blocked calls 分析 CREATE INDEX IF NOT EXISTS idx_mcp_audit_blocked ON awooop_mcp_gateway_audit (project_id, block_gate, created_at DESC) WHERE result_status = 'blocked'; -- 時序熱路徑(recent calls) CREATE INDEX IF NOT EXISTS idx_mcp_audit_recent ON awooop_mcp_gateway_audit (project_id, created_at DESC); -- ============================================================================= -- Row Level Security -- ============================================================================= ALTER TABLE awooop_mcp_tool_registry ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_grants ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_credential_refs ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_gateway_audit ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_tool_registry FORCE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_grants FORCE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_credential_refs FORCE ROW LEVEL SECURITY; ALTER TABLE awooop_mcp_gateway_audit FORCE ROW LEVEL SECURITY; -- awooop_app role:只能看自己 project 的資料 CREATE POLICY mcp_tool_registry_tenant_isolation ON awooop_mcp_tool_registry USING ( project_id = current_setting('app.project_id', TRUE) OR current_setting('app.project_id', TRUE) IS NULL ); CREATE POLICY mcp_grants_tenant_isolation ON awooop_mcp_grants USING ( project_id = current_setting('app.project_id', TRUE) OR current_setting('app.project_id', TRUE) IS NULL ); CREATE POLICY mcp_credential_refs_tenant_isolation ON awooop_mcp_credential_refs USING ( project_id = current_setting('app.project_id', TRUE) OR current_setting('app.project_id', TRUE) IS NULL ); CREATE POLICY mcp_gateway_audit_tenant_isolation ON awooop_mcp_gateway_audit USING ( project_id = current_setting('app.project_id', TRUE) OR current_setting('app.project_id', TRUE) IS NULL ); COMMIT;