HAL 0.4.1: 서버 등록 TLS 수정·"이미 등록된 서버" 안내, HAL 배포가 HAL 서버의 Node Agent·HawkEye 를 함께 설치해 부트스트랩을 닫음
- hal/tls.mjs: 공개 주소 TLS 신뢰 확인(시스템 CA → 실패면 체인·공개키 핀), 신뢰 앵커(체인 자체 서명 / <data>/tls/hal-ca.pem), 10분 캐시, 핀 설치 명령(curl -k --pinnedpubkey … --hal-pin). 공개 경로 GET /agent/v1/hal-ca.pem · /agent/v1/tls, CLI tls-probe
- deploy/agent-install.sh: --hal-pin(핀으로 CA 받기)·--ca·--insecure, curl --cacert / Node NODE_EXTRA_CA_CERTS / register --ca / 유닛 Environment, 연결 실패 원인·조치 한 줄, 이미 등록된 서버 안내
- agent/halca.mjs + agent/index.mjs·config.mjs: caFile 런타임 적용(tls.setDefaultCACertificates), fetch 실패를 읽을 수 있는 한 줄로(종료 1), 이미 등록(종료 2, --force), HAL 거부(종료 3), heartbeat observability → hawkeye
- core.registerAgent: 같은 서버의 두 번째 토큰 409 server_already_registered / 쓴 토큰 409 token_used("이미 등록된 서버"), 거부 토큰 미소비, 재등록 허용(replace: 기존 Agent 해지, 감사 agent.replace), 마이그레이션 hal_0011_token_replace, API/CLI --replace --json(재인증), ops.js 재등록 확인·핀 안내
- deploy/hal-host-agent.sh(server-create --profile hal → server-token --json → agent-install loopback → server-approve), bootstrap-hal.sh 6/7 단계(--no-agent), CLI server-create·server-approve, SERVER_PROFILES.hal
- core.deploymentStatus/selfHealth.deployment, 배너 "부트스트랩 완료 · 기능 단위 서버 배포 대기: …" / "진행 중", 배포 릴리스 화면 문구, heartbeat canonicalModuleName(0.3.17 잔여 결함)
- 개발 지시 v0.4.1(Oz)·VERSION_HISTORY 0.4.1·README·docs/HAL-AGENT §6·RUNBOOK §11·AI 스킬, 도입 모듈 변경 없음(Webmaker 1.9.0·AriMail 1.1.0)
- test/hal-041.test.mjs RED → GREEN 7/7(실제 자체 서명 TLS 픽스처·별도 프로세스 CLI), 기존 배너·프로필·버전 테스트 갱신, 전체 32파일 통과, 로컬 E2E 보고서 2026-11-06
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0f5e01a4cb
commit
2b31543a86
+23
-3
@@ -30,8 +30,9 @@ import * as mailsvc from './mailsvc.mjs';
|
||||
import { purgeSessions } from '../lib/session.mjs';
|
||||
import { audit } from '../lib/audit.mjs';
|
||||
import { describe } from '../lib/jobs.mjs';
|
||||
import { descriptor } from './modules/index.mjs';
|
||||
import { descriptor, profileOf } from './modules/index.mjs';
|
||||
import { randomToken } from '../lib/crypto.mjs';
|
||||
import { ensureTlsInfo, probePublicTls } from './tls.mjs'; // [0.4.1] 공개 주소 TLS 신뢰 확인(설치 명령 핀)
|
||||
import { writeFileSync, readFileSync, copyFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
|
||||
@@ -62,9 +63,26 @@ if (cmd === 'setup-token') {
|
||||
core.setBootstrap(d, { release_version: opt('version'), main_commit: opt('commit'), artifact_sha256: opt('sha256') }); console.log('부트스트랩 릴리스 정보를 기록했습니다');
|
||||
} else if (cmd === 'server-token') {
|
||||
// 운영자 콘솔(네트워크 관문·운영자 세션)이 닿지 않는 부트스트랩·연동 단계에서 HAL 데이터 소유자가 발급한다. 토큰은 1회 표시, 감사 기록(actor cli:<by>).
|
||||
// [0.4.1] --replace: 이미 Agent 가 있는 서버의 재등록 허용(기존 Agent 해지). --json: 스크립트(부트스트랩)가 읽는 한 줄 JSON. 설치 명령은 공개 주소 TLS 신뢰 상태를 먼저 확인해 핀을 붙인다.
|
||||
const sid = opt('server'); if (!sid || !core.getServer(d, sid)) { console.error('--server <srv_id> 가 필요합니다(없는 서버)'); process.exit(1); }
|
||||
const t = core.issueRegistrationToken(d, { kind: 'managed', serverId: sid, by: `cli:${opt('by', 'cli')}` });
|
||||
console.log(`등록 토큰(1회 표시, ${t.expiresAt} 까지, 공유 금지): ${t.token}\n설치: ${t.installCommand}`);
|
||||
await ensureTlsInfo(d);
|
||||
const t = core.issueRegistrationToken(d, { kind: 'managed', serverId: sid, by: `cli:${opt('by', 'cli')}`, replace: !!opt('replace') });
|
||||
if (opt('json')) console.log(JSON.stringify({ token: t.token, expiresAt: t.expiresAt, installCommand: t.installCommand, tls: t.tls, replace: t.replace, existingAgent: t.existingAgent }));
|
||||
else console.log(`등록 토큰(1회 표시, ${t.expiresAt} 까지, 공유 금지): ${t.token}\n설치: ${t.installCommand}${t.tls && t.tls.trusted === false ? `\n(HAL 공개 주소의 인증서가 시스템 CA 로 신뢰되지 않아(${t.tls.reason}) 핀을 붙였습니다 — 설치 스크립트가 핀으로 HAL 의 CA 를 받아 씁니다)` : ''}${t.existingAgent && !t.replace ? `\n주의: 이 서버에는 이미 Agent ${t.existingAgent.id} 가 있습니다 — 이 토큰으로는 등록이 거부됩니다(--replace 로 재등록 허용)` : ''}`);
|
||||
} else if (cmd === 'server-create') {
|
||||
// [0.4.1] 부트스트랩이 HAL 서버 자신을 관리 서버(control)로 만들 때 쓴다(운영자 콘솔이 아직 없는 단계). 출력은 한 줄 JSON.
|
||||
const name = opt('name'); const role = opt('role', 'control'); if (!name || name === true) { console.error('--name <이름> 이 필요합니다'); process.exit(1); }
|
||||
const by = `cli:${opt('by', 'cli')}`; let s;
|
||||
try { s = core.createServer(d, { name, role, location: opt('location', '') === true ? '' : opt('location', ''), by }); } catch (e) { console.error(`서버 생성 실패: ${e.code || ''} ${e.message}`); process.exit(1); }
|
||||
const profile = opt('profile'); if (profile && profile !== true) { const prof = profileOf(profile); if (!prof) { console.error(`알 수 없는 프로필: ${profile}`); process.exit(1); } core.updateServer(d, s.id, { profile: prof.id, role: prof.role, slots_json: JSON.stringify(prof.slots || {}) }, by); s = core.getServer(d, s.id); }
|
||||
console.log(JSON.stringify({ id: s.id, name: s.name, role: s.role, status: s.status, profile: s.profile || null }));
|
||||
} else if (cmd === 'server-approve') {
|
||||
// [0.4.1] 부트스트랩이 HAL 서버의 첫 Agent 를 승인해 부트스트랩을 닫는다(사람 결정은 bootstrap-hal.sh 실행 자체). 감사 actor cli:<by>.
|
||||
const sid = opt('server'); if (!sid || sid === true) { console.error('--server <srv_id> 가 필요합니다'); process.exit(1); }
|
||||
try { const s = core.approveServer(d, sid, `cli:${opt('by', 'cli')}`); const h = core.selfHealth(d); console.log(JSON.stringify({ id: s.id, name: s.name, status: s.status, agentId: s.agent_id, bootstrapComplete: h.bootstrap.complete, pendingModules: h.deployment ? h.deployment.pending : [] })); } catch (e) { console.error(`승인 실패: ${e.code || ''} ${e.message}`); process.exit(1); }
|
||||
} else if (cmd === 'tls-probe') {
|
||||
// [0.4.1] 공개 주소 TLS 신뢰 상태 readback(핀·신뢰 앵커 유무, 비밀 없음)
|
||||
const info = await probePublicTls(opt('url') && opt('url') !== true ? opt('url') : cfg.publicUrl, { dataDir: cfg.dataDir }); console.log(JSON.stringify({ url: info.url, trusted: info.trusted, reason: info.reason || null, pin: info.pin || null, caCount: (info.ca || []).length, caSource: info.caSource || null, subject: info.subject || null, issuer: info.issuer || null, validTo: info.validTo || null, error: info.error || null }, null, 1));
|
||||
} else if (cmd === 'mail-import') {
|
||||
const file = opt('discovery'); if (!file) { console.error('--discovery <json> 이 필요합니다'); process.exit(1); }
|
||||
const doc = JSON.parse(readFileSync(file, 'utf8')); const v = mailsvc.validateDiscovery(doc); if (!v.ok) { console.error(`discovery 검증 실패:\n - ${v.errors.join('\n - ')}`); process.exit(1); }
|
||||
@@ -109,6 +127,8 @@ if (cmd === 'setup-token') {
|
||||
every(3600000, () => { core.purgeMetrics(d); purgeSessions(d.store); });
|
||||
every(60000, () => { const c = dr.getConfig(d); if (!c.snapshot_interval_minutes) return; const last = d.store.get('SELECT created_at FROM dr_snapshots WHERE verify_state=\'ok\' ORDER BY created_at DESC LIMIT 1'); if (!last || Date.now() - Date.parse(last.created_at) >= c.snapshot_interval_minutes * 60000) dr.snapshot(d, { kind: 'scheduled' }); });
|
||||
await app.listen(cfg.port, cfg.host);
|
||||
// [0.4.1] 공개 주소 TLS 신뢰 확인(설치 명령 핀·CA 배포 근거) — 기동 직후 1회, 이후 10분마다(인증서 갱신 반영)
|
||||
ensureTlsInfo(d).then((t) => log(`[hal] 공개 주소 TLS: ${t.trusted === true ? '신뢰됨' : t.trusted === false ? `시스템 CA 로 신뢰되지 않음(${t.reason}) — 설치 명령에 핀을 붙입니다` : `확인 불가(${t.error || t.reason})`}`)); every(600000, () => ensureTlsInfo(d, { force: true }));
|
||||
log(`[hal] Ariweb HAL v${VERSION} — http://${cfg.host}:${cfg.port} (공개 ${cfg.publicUrl}, 데이터 ${cfg.dataDir}, 부트스트랩 ${core.bootstrapComplete(d) ? '완료' : '진행 중'})`);
|
||||
process.on('SIGTERM', () => { d.stopping = true; d.jobs.stop(); setTimeout(() => process.exit(0), 500); });
|
||||
} else { console.error(`알 수 없는 명령: ${cmd}`); process.exit(1); }
|
||||
|
||||
Reference in New Issue
Block a user