HAL 0.4.0: 통합 설정 관제 — Webmaker·AriMail·Agent 서버 단위 설정을 HAL 이 관리(설정 목록·범위 global/server·비밀값 Vault·유효값 hash, /ops/api/settings·재인증, settings.apply → settings.apply.target → Agent module.configure(HMAC 설정 받기·hash 확인·hal-managed.json 0600·재시작·모듈 적용 확인), /ops/settings 화면·운영 현황 "설정 적용 필요", AI 도구 hal_settings_get/set/apply(비밀값 거부)), 도입 모듈 Webmaker 1.9.0·AriMail 1.1.0(HAL 0.4.0 대응 halCompat, hal-managed 적용·관리 항목 409 managed_by_hal 잠금·"HAL 관리" 표시) 재도입(pin 8f8716c·f959b65, import-sources 미수정 파일 갱신), Agent heartbeat 관리, module-as-user write --mode 600, 개발 지시 v0.4.0(Oz)·docs/HAL-SETTINGS.md·VERSION_HISTORY 0.4.0 — test/hal-040.test.mjs RED → GREEN 5/5, agent 11/11, 전체 31파일 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-29 06:46:45 +00:00
co-authored by Claude Opus 5.5
parent 54db663531
commit 9f1e490b51
44 changed files with 1075 additions and 82 deletions
+6 -1
View File
@@ -148,6 +148,9 @@ function dnsCard() {
function field(key, { type = 'text', options = null, value = '', source = 'default', secret = false, placeholder = '', multiline = false } = {}) {
const [label, help] = FIELD[key] || [key, ''];
// 1.1.0 (HAL 0.4.0 대응): HAL 이 관리하는 항목은 잠그고 표시합니다(저장해도 서버가 409 로 거부). 잠긴 칸은 collect 가 건너뜁니다.
const halKeys = (typeof D !== 'undefined' && D && D.halManaged && D.halManaged.keys) || [];
if (halKeys.includes(key)) return `<label class="field"><span>${esc(label)} <span class="tag off">HAL 관리</span></span>${secret ? `<input type="password" disabled placeholder="${value ? '저장됨: ' + esc(value) : '(없음)'} — HAL 통합 설정에서 관리">` : `<input type="text" disabled value="${esc(value)}">`}<span class="help">HAL 운영자 콘솔 › 통합 설정에서 바꿉니다</span></label>`;
const src = `<span class="src">${esc(SRC[source] || source)}</span>`;
if (secret && multiline) return `<label class="field"><span>${esc(label)}</span><span class="secret-row"><textarea name="${key}" rows="4" autocomplete="off" placeholder="${value ? '저장됨: ' + esc(value) + ' (바꾸려면 붙여 넣기)' : '(없음) 붙여 넣으세요'}" data-secret="1"></textarea><button type="button" class="small ghost" data-clear="${key}" title="저장된 값을 지웁니다">비우기</button></span>${help ? `<span class="help">${esc(help)}</span>` : ''}${src}</label>`;
if (secret) return `<label class="field"><span>${esc(label)}</span><span class="secret-row"><input type="password" name="${key}" autocomplete="new-password" placeholder="${value ? '저장됨: ' + esc(value) + ' (바꾸려면 입력)' : '(없음) 입력하세요'}" data-secret="1"><button type="button" class="small ghost" data-clear="${key}" title="저장된 값을 지웁니다">비우기</button></span>${help ? `<span class="help">${esc(help)}</span>` : ''}${src}</label>`;
@@ -166,8 +169,9 @@ function render() {
const harakaTag = !m.usesHaraka ? tag(false, '', '사용 안 함', 'off') : !h.installed ? tag(false, '', '미설치') : !h.status || h.stale ? tag(false, '', '서비스 꺼짐') : hs.running ? tag(true, '실행 중', '') : tag(false, '', '시작 대기');
const choices = (name, current) => `<div class="choices">${Object.entries(MODES[name]).map(([k, [t, d]]) => `<label class="choice ${k === current ? 'selected' : ''}"><input type="radio" name="${name}" value="${k}" ${k === current ? 'checked' : ''}><span><span class="t">${esc(t)}</span><div class="d">${esc(d)}</div></span></label>`).join('')}</div>`;
const missing = checklist(D);
const hal = D.halManaged && D.halManaged.managed ? `<div class="card"><b>HAL 관리</b> — 이 서버의 다음 설정은 Ariweb HAL 통합 설정(HAL ${esc(D.halManaged.halCompat || '')})이 관리합니다: <code>${D.halManaged.keys.map(esc).join(', ')}</code>. 여기서는 바꿀 수 없고 HAL 운영자 콘솔 › 통합 설정에서 바꿉니다. 도메인별 설정은 그대로 여기서 관리합니다.${D.halManaged.appliedAt ? `<div class="meta muted">마지막 적용 ${esc(D.halManaged.appliedAt)}${(D.halManaged.errors || []).length ? ` · 적용 오류: ${D.halManaged.errors.map((x) => esc(x.key)).join(', ')}` : ''}</div>` : ''}</div>` : ''; // 1.1.0
$('#main').innerHTML = `
${domainsCard()}
${hal}${domainsCard()}
<section class="card"><h2>현재 상태${MODE === 'token' ? ` — <code>${esc(m.domain)}</code>${m.primary ? ' <span class="tag">기본 도메인</span>' : ''}` : ''}</h2><div class="summary">
<div class="item"><div class="k">수신</div><div class="v">${esc(MODES.inbound[m.inbound][0])}</div></div>
<div class="item"><div class="k">발신</div><div class="v">${esc(MODES.outbound[m.outbound][0])}</div></div>
@@ -222,6 +226,7 @@ function checklist(d) {
function collect(form) {
const patch = {};
for (const el of form.elements) {
if (el.disabled) continue; // 1.1.0: HAL 관리 칸
if (!el.name) continue;
if (el.type === 'radio') { if (el.checked) patch[el.name] = el.value; continue; }
if (el.dataset.secret) { if (el.dataset.clear === '1') patch[el.name] = ''; else if (el.value) patch[el.name] = el.value; continue; }