139 lines
13 KiB
JavaScript
139 lines
13 KiB
JavaScript
/**
|
|
* 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;` },
|
|
];
|