Files
AriWeb-Suite/hal/schema.mjs
T

146 lines
14 KiB
JavaScript
Raw Normal View History

/**
* hal/schema.mjs — HAL DB 마이그레이션(hal.sqlite).
* lib 공통 표(sessions·jobs·notifications·audit) 뒤에 HAL 고유 표를 둔다. docs/ARCHITECTURE.md §5·§10, Dev-Order §28·§32·§33·§34.
*/
import * as session from '../lib/session.mjs';
import * as jobs from '../lib/jobs.mjs';
import * as notify from '../lib/notify.mjs';
import * as audit from '../lib/audit.mjs';
import { MIGRATIONS as MAIL_MIGRATIONS } from './mailsvc.mjs';
export const MIGRATIONS = [...session.MIGRATIONS, ...jobs.MIGRATIONS, ...notify.MIGRATIONS, ...audit.MIGRATIONS, { id: 'hal_0001', sql: `
-- 관리 서버(§28). agent_id 는 그 서버에서 돌아가는 Node Agent. target/actual 모듈 버전은 module_targets 가 가진다.
CREATE TABLE IF NOT EXISTS servers (
id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL, location TEXT, status TEXT NOT NULL DEFAULT 'pending', accepts_new INTEGER NOT NULL DEFAULT 0,
approved_at TEXT, approved_by TEXT, agent_id TEXT, last_seen_at TEXT, clock_skew_ms INTEGER, agent_version TEXT, metrics_json TEXT, capacity_json TEXT, facts_json TEXT,
maintenance_note TEXT, isolated_reason TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
-- Node Agent(관리형) 또는 External Pull Agent(고객 외부 서버, 사이트 하나에 귀속). 비밀은 보관소('service:agent:<id>')에, 여기엔 해시만.
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY, server_id TEXT, kind TEXT NOT NULL DEFAULT 'managed', secret_hash TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', label TEXT,
group_id TEXT, site_id TEXT, version TEXT, registered_at TEXT NOT NULL, last_seen_at TEXT, revoked_at TEXT, facts_json TEXT
);
-- 일회용 등록 토큰(10분, 1회). 외부 Agent 토큰은 group_id/site_id 가 있다.
CREATE TABLE IF NOT EXISTS registration_tokens (
id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, kind TEXT NOT NULL DEFAULT 'managed', server_id TEXT, group_id TEXT, site_id TEXT, label TEXT,
created_by TEXT, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, used_at TEXT, used_by_agent TEXT
);
-- 릴리스 등록부(§32.1). artifacts_json = { 모듈: { file, sha256, size } }
CREATE TABLE IF NOT EXISTS releases (
version TEXT PRIMARY KEY, main_commit TEXT NOT NULL, built_at TEXT NOT NULL, artifacts_json TEXT NOT NULL, checks_json TEXT, status TEXT NOT NULL DEFAULT 'registered',
approved_by TEXT, approved_at TEXT, hal_deployed_at TEXT, notes TEXT, registered_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS modules (name TEXT PRIMARY KEY, target_version TEXT, updated_at TEXT NOT NULL);
-- 모듈 × 서버 배치 상태(목표 버전·실제 버전·건강·마지막 확인)
CREATE TABLE IF NOT EXISTS module_targets (
module TEXT NOT NULL, server_id TEXT NOT NULL, target_version TEXT, actual_version TEXT, health TEXT NOT NULL DEFAULT 'unknown', active INTEGER, last_check_at TEXT, last_error TEXT,
last_job_id TEXT, updated_at TEXT NOT NULL, PRIMARY KEY(module, server_id)
);
-- Agent 에게 배정된 작업(HAL 작업의 자식). 상태: queued → running → done|failed|needs_user|cancelled
CREATE TABLE IF NOT EXISTS agent_jobs (
id TEXT PRIMARY KEY, hal_job_id TEXT, agent_id TEXT NOT NULL, kind TEXT NOT NULL, payload_json TEXT, status TEXT NOT NULL DEFAULT 'queued', progress INTEGER NOT NULL DEFAULT 0,
steps_json TEXT, log_json TEXT, result_json TEXT, error TEXT, error_code TEXT, user_message TEXT, cancel_requested INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL, assigned_at TEXT, started_at TEXT, ended_at TEXT, heartbeat_at TEXT
);
CREATE INDEX IF NOT EXISTS agent_jobs_agent ON agent_jobs(agent_id, status, created_at);
CREATE INDEX IF NOT EXISTS agent_jobs_hal ON agent_jobs(hal_job_id);
-- 서버 지표(90일 이상 보존, §28.1)
CREATE TABLE IF NOT EXISTS metrics_samples (
server_id TEXT NOT NULL, at TEXT NOT NULL, cpu REAL, mem_used REAL, mem_total REAL, disk_used REAL, disk_total REAL, inode_used REAL, inode_total REAL, io_read REAL, io_write REAL, net_in REAL, net_out REAL, clock_skew_ms INTEGER, load1 REAL,
PRIMARY KEY(server_id, at)
);
-- 사건(§31): 같은 원인은 key 로 묶는다
CREATE TABLE IF NOT EXISTS incidents (
id TEXT PRIMARY KEY, key TEXT NOT NULL, severity TEXT NOT NULL, title TEXT NOT NULL, component TEXT, customer_impact TEXT, started_at TEXT NOT NULL, resolved_at TEXT, verified_at TEXT,
assignee TEXT, public INTEGER NOT NULL DEFAULT 0, public_note TEXT, notes_json TEXT, updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS incidents_key ON incidents(key, resolved_at);
-- 운영자 계정(§25). 고객 계정과 완전히 분리. mfa_json = { totp: { secret(보관소 참조 id), enabledAt } }
CREATE TABLE IF NOT EXISTS ops_accounts (
id TEXT PRIMARY KEY, login TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, pass_hash TEXT NOT NULL, pass_salt TEXT NOT NULL, mfa_json TEXT, status TEXT NOT NULL DEFAULT 'active',
failed_logins INTEGER NOT NULL DEFAULT 0, lock_until TEXT, last_login_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, must_change_password INTEGER NOT NULL DEFAULT 0
);
-- 짧은 수명의 사용자·기기 귀속 접근권(§25: 허용 네트워크 밖에서는 이것 없이 로그인 화면도 열리지 않음)
CREATE TABLE IF NOT EXISTS ops_access_grants (
id TEXT PRIMARY KEY, account_id TEXT NOT NULL, code_hash TEXT UNIQUE, cookie_hash TEXT UNIQUE, ua_hash TEXT, ip TEXT, created_by TEXT, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, bound_at TEXT, revoked_at TEXT, reason TEXT
);
-- 긴급 복구 세션(§33)
CREATE TABLE IF NOT EXISTS emergency_sessions (
id TEXT PRIMARY KEY, incident_key TEXT NOT NULL, reason TEXT NOT NULL, scope_json TEXT NOT NULL, status TEXT NOT NULL, declared_by TEXT NOT NULL, declared_at TEXT NOT NULL,
approvals_json TEXT NOT NULL DEFAULT '[]', credential_hash TEXT, credential_issued_at TEXT, credential_expires_at TEXT, credential_revoked_at TEXT,
actions_json TEXT NOT NULL DEFAULT '[]', verify_json TEXT, reconcile_json TEXT, closed_at TEXT, closed_by TEXT, hal_healthy_at_declare INTEGER, updated_at TEXT NOT NULL
);
-- 재해복구(§34)
CREATE TABLE IF NOT EXISTS dr_config (id INTEGER PRIMARY KEY CHECK(id=1), rto_minutes INTEGER, rpo_minutes INTEGER, snapshot_interval_minutes INTEGER, retention_count INTEGER, external_store TEXT, updated_at TEXT, updated_by TEXT);
CREATE TABLE IF NOT EXISTS dr_snapshots (id TEXT PRIMARY KEY, created_at TEXT NOT NULL, location TEXT NOT NULL, size_bytes INTEGER, sha256 TEXT, verify_state TEXT NOT NULL DEFAULT 'pending', verified_at TEXT, external_copied_at TEXT, external_location TEXT, kind TEXT NOT NULL DEFAULT 'scheduled', error TEXT, deleted_at TEXT);
CREATE TABLE IF NOT EXISTS dr_restores (id TEXT PRIMARY KEY, snapshot_id TEXT NOT NULL, requested_by TEXT NOT NULL, approved_by TEXT, approved_at TEXT, old_control_stopped_confirmed_at TEXT, started_at TEXT, finished_at TEXT, status TEXT NOT NULL, checks_json TEXT, diff_json TEXT, note TEXT);
CREATE TABLE IF NOT EXISTS dr_drills (id TEXT PRIMARY KEY, started_at TEXT NOT NULL, finished_at TEXT, snapshot_id TEXT, performed_by TEXT, steps_json TEXT NOT NULL, measured_rto_minutes REAL, measured_rpo_minutes REAL, result TEXT NOT NULL DEFAULT 'running', findings_json TEXT, next_due_at TEXT);
-- 부트스트랩·실행 상태(§32.2). 행 하나(id=1).
CREATE TABLE IF NOT EXISTS bootstrap_state (
id INTEGER PRIMARY KEY CHECK(id=1), first_install INTEGER NOT NULL DEFAULT 1, release_version TEXT, main_commit TEXT, artifact_sha256 TEXT, hal_running_version TEXT, hal_started_at TEXT,
first_agent_id TEXT, first_agent_registered_at TEXT, manual_install_locked INTEGER NOT NULL DEFAULT 0, live_instance_id TEXT, live_lease_at TEXT, completed_at TEXT, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS transfers (id TEXT PRIMARY KEY, created_by_agent TEXT, path TEXT NOT NULL, size_bytes INTEGER, sha256 TEXT, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, purpose TEXT, meta_json TEXT, deleted_at TEXT);
CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value_json TEXT, updated_at TEXT NOT NULL, updated_by TEXT);
CREATE TABLE IF NOT EXISTS portal_events_outbox (id TEXT PRIMARY KEY, payload_json TEXT NOT NULL, created_at TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, next_at TEXT NOT NULL, delivered_at TEXT, last_error TEXT);
` },
// [통합 보완] 같은 호스트 재시작 판정을 위한 임대 소유자 호스트·PID
{ id: 'hal_0002_live_host', sql: `ALTER TABLE bootstrap_state ADD COLUMN live_host TEXT; ALTER TABLE bootstrap_state ADD COLUMN live_pid TEXT;` },
// [legacy 메일 서비스 가져오기] HAL 관리형 메일 서비스 등록부·가져오기 단위(hal/mailsvc.mjs). 표 추가만(기존 표 변경 없음).
...MAIL_MIGRATIONS,
// Webmaker 전체 인벤토리는 heartbeat 2 MiB 한도를 넘을 수 있어 인증된 배치로 원자적으로 조립한다.
{ id: 'hal_0004_webmaker_inventory_stage', sql: `
CREATE TABLE IF NOT EXISTS agent_inventory_staging (
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE, kind TEXT NOT NULL, scan_id TEXT NOT NULL, total INTEGER NOT NULL, next_offset INTEGER NOT NULL DEFAULT 0,
meta_json TEXT NOT NULL, items_json TEXT NOT NULL DEFAULT '[]', updated_at TEXT NOT NULL,
PRIMARY KEY(agent_id, kind)
);
` },
// 사전 릴리스에서 FK 없이 같은 staging 표를 만든 경우에도 고아 row를 정리하고 이후 삭제를 보장한다.
{ id: 'hal_0005_webmaker_inventory_cleanup', sql: `
DELETE FROM agent_inventory_staging WHERE agent_id NOT IN (SELECT id FROM agents);
CREATE TRIGGER IF NOT EXISTS agent_inventory_staging_agent_cleanup
AFTER DELETE ON agents BEGIN
DELETE FROM agent_inventory_staging WHERE agent_id=OLD.id;
END;
` },
// 0.3.0 AI 채팅룸(hal/ai): 모델 연결(비밀값은 Vault, 행에는 참조·상태만) · 방(통합 1 + 연결별 개별) · 메시지(블록 JSON, 방 안 seq) · 제안(쓰기·고위험 도구 호출의 승인 대기) · OAuth PKCE state(해시, 1회용).
{ id: 'hal_0006_ai_chat', sql: `
CREATE TABLE IF NOT EXISTS ai_connections (
id TEXT PRIMARY KEY, name TEXT NOT NULL, tab_label TEXT, provider TEXT NOT NULL, auth_kind TEXT NOT NULL, base_url TEXT, model TEXT, models_json TEXT NOT NULL DEFAULT '[]',
identity_json TEXT, oauth_json TEXT, cli_json TEXT, status TEXT NOT NULL DEFAULT 'active', last_ok_at TEXT, last_error TEXT, position INTEGER NOT NULL DEFAULT 0,
created_by TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS ai_connections_name ON ai_connections(lower(name));
CREATE TABLE IF NOT EXISTS ai_rooms (id TEXT PRIMARY KEY, kind TEXT NOT NULL, connection_id TEXT, title TEXT, created_by TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS ai_messages (
id TEXT PRIMARY KEY, room_id TEXT NOT NULL, seq INTEGER NOT NULL, role TEXT NOT NULL, connection_id TEXT, author TEXT, content_json TEXT NOT NULL, addressed_json TEXT,
relay_depth INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'done', usage_json TEXT, error TEXT, created_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS ai_messages_room_seq ON ai_messages(room_id, seq);
CREATE TABLE IF NOT EXISTS ai_actions (
id TEXT PRIMARY KEY, room_id TEXT NOT NULL, message_id TEXT, connection_id TEXT NOT NULL, tool TEXT NOT NULL, args_json TEXT NOT NULL, risk TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'proposed', proposed_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, result_json TEXT, error TEXT, tool_use_id TEXT, relay_depth INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS ai_actions_room ON ai_actions(room_id, status);
CREATE TABLE IF NOT EXISTS ai_oauth_states (state_hash TEXT PRIMARY KEY, connection_id TEXT NOT NULL, operator_id TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL);
` },
// 0.3.2: 연결별 도구 정책(tools_json: {preset, enabled}) · 제안 출처(source: chat | mcp)
{ id: 'hal_0007_ai_tools_policy', sql: `ALTER TABLE ai_connections ADD COLUMN tools_json TEXT; ALTER TABLE ai_actions ADD COLUMN source TEXT NOT NULL DEFAULT 'chat';` },
// 0.3.17: observability → hawkeye(HawkEye) 이름 변경(배치 상태·목표 버전 행), 서버 프로필(역할별 통합 배포)·슬롯
{ id: 'hal_0008_hawkeye', sql: `
UPDATE module_targets SET module='hawkeye' WHERE module='observability' AND NOT EXISTS (SELECT 1 FROM module_targets t2 WHERE t2.module='hawkeye' AND t2.server_id=module_targets.server_id);
DELETE FROM module_targets WHERE module='observability';
INSERT OR IGNORE INTO modules(name, target_version, updated_at) SELECT 'hawkeye', target_version, updated_at FROM modules WHERE name='observability';
DELETE FROM modules WHERE name='observability';
` },
{ id: 'hal_0009_server_profile', sql: `ALTER TABLE servers ADD COLUMN profile TEXT; ALTER TABLE servers ADD COLUMN slots_json TEXT;` },
// 0.4.0 통합 설정 관제(hal/settings): 관리 설정(범위 global | server:<id>, 비밀값은 Vault 참조만) · 서버별 적용 상태(적용된 hash·시각·결과)
{ id: 'hal_0010_managed_settings', sql: `
CREATE TABLE IF NOT EXISTS managed_settings (scope TEXT NOT NULL, module TEXT NOT NULL, key TEXT NOT NULL, value_json TEXT, is_secret INTEGER NOT NULL DEFAULT 0, updated_by TEXT, updated_at TEXT NOT NULL, PRIMARY KEY(scope, module, key));
CREATE TABLE IF NOT EXISTS managed_settings_state (server_id TEXT NOT NULL, module TEXT NOT NULL, applied_hash TEXT, applied_at TEXT, status TEXT NOT NULL DEFAULT 'unknown', error TEXT, job_id TEXT, updated_at TEXT NOT NULL, PRIMARY KEY(server_id, module));
` },
// [0.4.1] 등록 토큰의 재등록 허용 표시: 이미 Agent 가 있는 서버에 새 Agent 를 등록할 때(기존 Agent 해지) 운영자가 명시해야 한다 — 없으면 server_already_registered 로 거부
{ id: 'hal_0011_token_replace', sql: `ALTER TABLE registration_tokens ADD COLUMN replace_existing INTEGER NOT NULL DEFAULT 0;` },
];