From ce6a98d534d0ee426732e7ee87372d28886930af Mon Sep 17 00:00:00 2001 From: zzabisagent-lab Date: Mon, 26 Oct 2026 12:02:07 +0000 Subject: [PATCH] =?UTF-8?q?HAL:=20=EA=B0=99=EC=9D=80=20=ED=98=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=9D=98=20=EC=A3=BD=EC=9D=80=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=20=EC=9E=84=EB=8C=80=20=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EB=B0=9B=EA=B8=B0(systemd=20=EC=9E=AC=EC=8B=9C=EC=9E=91),=20?= =?UTF-8?q?=EB=B6=80=ED=8A=B8=EC=8A=A4=ED=8A=B8=EB=9E=A9=EC=9D=B4=20HAL=20?= =?UTF-8?q?CLI=20=EB=A1=9C=20=EB=A6=B4=EB=A6=AC=EC=8A=A4=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- deploy/bootstrap-hal.sh | 2 +- hal/core.mjs | 11 +++++++++-- hal/schema.mjs | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/deploy/bootstrap-hal.sh b/deploy/bootstrap-hal.sh index 95192fb..995cbbf 100755 --- a/deploy/bootstrap-hal.sh +++ b/deploy/bootstrap-hal.sh @@ -31,7 +31,7 @@ tar -xzf "$ART" -C "$APP_DIR/releases/$VERSION"; ln -sfn "$APP_DIR/releases/$VER cd "$APP_DIR/current" export ARIWEB_HAL_DATA="$DATA"; $NODE_BIN -e "import('./lib/vault.mjs').then(m=>{m.openVault('$DATA');console.log(' 보관소 준비(vault.key 0600)')})" $NODE_BIN hal/index.mjs bootstrap-release --version "$VERSION" --commit "$COMMIT" --sha256 "$SUM" -$NODE_BIN scripts/release.mjs register --hal-data "$DATA" --dist "$DIST" --approve --by bootstrap +$NODE_BIN hal/index.mjs register-release --manifest "$DIST/manifest.json" --approve --by bootstrap # hal artifact 에는 scripts/ 가 없으므로 HAL CLI 로 등록·artifact 복사 $NODE_BIN -e "const fs=require('fs');const f='$DATA/config.json';const v=fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')).values:{};v.publicUrl='https://$HOST';v.portalUrl=v.portalUrl||'http://127.0.0.1:8910';fs.writeFileSync(f,JSON.stringify({version:1,updatedAt:new Date().toISOString(),updatedBy:'bootstrap',values:v},null,1),{mode:0o600})" echo "== 3/6 systemd 유닛 ariweb-hal" sed -e "s#{{APP_DIR}}#$APP_DIR#g" -e "s#{{USER}}#$USER_NAME#g" -e "s#{{NODE}}#$NODE_BIN#g" -e "s#{{VERSION}}#$VERSION#g" deploy/units/ariweb-hal.service.tmpl | sudo -n tee /etc/systemd/system/ariweb-hal.service >/dev/null diff --git a/hal/core.mjs b/hal/core.mjs index a23a575..0326388 100644 --- a/hal/core.mjs +++ b/hal/core.mjs @@ -2,6 +2,7 @@ * hal/core.mjs — HAL 핵심 서비스: 서버·Agent 등록부, Agent 작업 배정·대기, 릴리스 등록부, 부트스트랩 상태, 지표, 포털 이벤트 outbox. * app.mjs(라우트)와 jobs.mjs(작업 종류)가 함께 쓴다. 모든 함수는 deps(store·vault·config·log·jobs) 를 받는다. */ +import { hostname } from 'node:os'; import { newId, randomToken, sha256, hashPassword } from '../lib/crypto.mjs'; import { nowIso, toJson, parseJson } from '../lib/db.mjs'; import { audit } from '../lib/audit.mjs'; @@ -23,11 +24,17 @@ export function bootstrapComplete(d) { const b = bootstrapState(d); return !!(b. /** HAL 기동 때: 실행 버전·인스턴스 임대 기록. 다른 인스턴스의 최근 임대가 있으면 거부(§34 "하나의 HAL 만 운영"). */ export function claimLive(d, { instanceId, version, takeover = false, now = Date.now() }) { const b = bootstrapState(d); - if (b.live_instance_id && b.live_instance_id !== instanceId && b.live_lease_at && now - Date.parse(b.live_lease_at) < 60000 && !takeover) { + // [통합 보완] 같은 호스트에서 이전 프로세스가 죽은 뒤(systemd 재시작·비정상 종료) 60초 임대가 남아 있으면 기동이 막힌다. + // 임대에 호스트·PID 를 함께 기록해 두고, 같은 호스트이며 그 PID 가 더 이상 살아 있지 않으면 같은 제어 위치의 재시작으로 보아 이어받는다. + // 다른 호스트(재해복구로 띄운 두 번째 HAL)는 여전히 거부된다(§34 "하나의 HAL 만 운영"). + const sameHostDead = b.live_host && b.live_host === hostname() && b.live_pid && !pidAlive(Number(b.live_pid)); + if (b.live_instance_id && b.live_instance_id !== instanceId && b.live_lease_at && now - Date.parse(b.live_lease_at) < 60000 && !takeover && !sameHostDead) { throw Object.assign(new Error(`다른 HAL 인스턴스(${b.live_instance_id})가 ${b.live_lease_at} 에 운영 중입니다. 기존 제어 위치를 먼저 중지하세요`), { code: 'another_hal_live' }); } - return setBootstrap(d, { live_instance_id: instanceId, live_lease_at: new Date(now).toISOString(), hal_running_version: version, hal_started_at: nowIso() }); + return setBootstrap(d, { live_instance_id: instanceId, live_lease_at: new Date(now).toISOString(), live_host: hostname(), live_pid: String(process.pid), hal_running_version: version, hal_started_at: nowIso() }); } +/** PID 가 살아 있는지(신호 0). 권한 오류(EPERM)는 살아 있는 것으로 본다. */ +function pidAlive(pid) { if (!pid) return false; try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } } export function renewLease(d, instanceId) { d.store.run('UPDATE bootstrap_state SET live_lease_at=? WHERE id=1 AND live_instance_id=?', nowIso(), instanceId); } /** 첫 Agent 가 승인되면 부트스트랩을 닫고 수동 설치를 잠근다 */ export function maybeCompleteBootstrap(d, agentId) { diff --git a/hal/schema.mjs b/hal/schema.mjs index feb7a6a..c8350b8 100644 --- a/hal/schema.mjs +++ b/hal/schema.mjs @@ -82,4 +82,6 @@ CREATE TABLE IF NOT EXISTS bootstrap_state ( 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;` }, +];