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:
co-authored by
Claude Opus 5.5
parent
54db663531
commit
9f1e490b51
@@ -48,7 +48,8 @@ const modules = require('./lib/modules');
|
||||
const mailsys = require('./lib/mailsys'); // v1.3.0 사이트 메일 시스템
|
||||
const mailServers = require('./lib/mailservers'); // v1.6.38 메일 서버(Arimail) 등록부 — 여러 대, 닉네임·IP 로 구분
|
||||
const mailusage = require('./lib/mailusage'); // v1.7.3 메일 사용량(Arimail 1.0.9 사용량 API, 캐시)
|
||||
const vault = require('./lib/vault'); // v1.3.1 외부 연동 접속 정보 암호화 보관소
|
||||
const vault = require('./lib/vault');
|
||||
const halManaged = require('./lib/hal-managed'); // v1.9.0 (Ariweb Suite HAL 0.4.0 대응) HAL 통합 설정 관제 — 관리 항목 적용·잠금 // v1.3.1 외부 연동 접속 정보 암호화 보관소
|
||||
const logview = require('./lib/logview'); // v1.3.2 관리자 로그 보기
|
||||
const seo = require('./lib/seo'); // v1.3.5 검색·AI 최적화(SEO·AEO·GEO), 검색사이트 등록
|
||||
const templates = require('./lib/templates'); // v1.3.10 사이트 템플릿
|
||||
@@ -2275,11 +2276,12 @@ async function handle(req, res) {
|
||||
if (req.method === 'GET' && arg === 'preview-page' && ['index', 'login', 'site-admin'].includes(sub2)) return send(res, 200, { html: fs.readFileSync(path.join(PUBLIC_DIR, `${sub2}.html`), 'utf8') });
|
||||
if (req.method === 'POST' && arg === 'theme-preview') { const { fields: b } = await readBody(req); return send(res, 200, { css: settings.themeCss(b.theme || null) }); }
|
||||
if (req.method === 'GET' && arg === 'theme') return send(res, 200, { theme: settings.getTheme(), saved: !!settings.load().theme, defaults: settings.THEME_DEFAULTS, presets: settings.THEME_PRESETS });
|
||||
if (req.method === 'GET') return send(res, 200, { ...settings.publicView(), availableModels: providers.availableModels(null), unavailableProviders: providers.unavailableProviders(), serverLogin: settings.serverLoginInfo(), claude: claudeInfo(), version: VERSION, welcomeCandidates: welcomeCandidates(), domainProviders: domainsLib.publicCatalog() /* v1.5.36 */ });
|
||||
if (req.method === 'GET') return send(res, 200, { ...settings.publicView(), halManaged: halManaged.publicInfo() /* v1.9.0 */, availableModels: providers.availableModels(null), unavailableProviders: providers.unavailableProviders(), serverLogin: settings.serverLoginInfo(), claude: claudeInfo(), version: VERSION, welcomeCandidates: welcomeCandidates(), domainProviders: domainsLib.publicCatalog() /* v1.5.36 */ });
|
||||
if (req.method === 'PUT') {
|
||||
const { fields: b } = await readBody(req);
|
||||
// v1.4.5 첫 화면 페이지는 관리자가 만든 사이트만 (다른 회원 사이트를 모든 사용자에게 보이지 않게)
|
||||
if (b.welcomeSiteId) { const c = welcomeCandidates().find((x) => x.id === String(b.welcomeSiteId)); if (!c) return send(res, 400, { error: '첫 화면 페이지는 관리자 계정이 만든 사이트 중에서 고르세요' }); if (!c.published && !c.hasIndex) return send(res, 400, { error: '그 사이트에는 아직 첫 페이지(index.html)가 없습니다. 생성을 마친 뒤 고르세요' }); }
|
||||
halManaged.assertNotManaged(halManaged.lockedKeys(b)); // v1.9.0: HAL 이 관리하는 항목은 409 managed_by_hal
|
||||
settings.save(b); audit.log({ by: user.id, action: 'settings.update', targetType: 'settings', target: 'settings', detail: { keys: Object.keys(b || {}) } }); return send(res, 200, { ...settings.publicView(), availableModels: providers.availableModels(null), unavailableProviders: providers.unavailableProviders(), serverLogin: settings.serverLoginInfo(), claude: claudeInfo(), version: VERSION, welcomeCandidates: welcomeCandidates(), domainProviders: domainsLib.publicCatalog() /* v1.5.36 */ });
|
||||
}
|
||||
if (req.method === 'POST' && arg === 'test-connection') {
|
||||
@@ -2376,12 +2378,14 @@ async function handle(req, res) {
|
||||
if (what === 'credentials') {
|
||||
const kinds = Object.fromEntries(Object.entries(vault.KINDS).map(([k, v]) => [k, { label: v.label, fixedId: v.fixedId, defaultUrl: v.defaultUrl, secretLabel: v.secretLabel }]));
|
||||
if (req.method === 'GET' && !arg) return send(res, 200, { items: vault.list(), kinds });
|
||||
if (req.method === 'POST' && !arg) { const { fields: b } = await readBody(req); const r = vault.set(b, 'input'); audit.log({ by: user.id, action: 'vault.set', targetType: 'vault', target: (r && r.id) || b.id || b.kind, detail: { kind: b.kind, label: b.label } }); return send(res, 200, r); }
|
||||
// v1.9.0: HAL 이 관리하는 비밀값의 보관소 종류는 여기서 바꾸거나 지울 수 없다(HAL 통합 설정에서만)
|
||||
const vaultLocked = (kind) => { if (kind && halManaged.lockedVaultKinds().includes(String(kind))) throw halManaged.managedError(Object.entries(halManaged.VAULT_KIND).filter(([, v]) => v === String(kind)).map(([k]) => k)); };
|
||||
if (req.method === 'POST' && !arg) { const { fields: b } = await readBody(req); vaultLocked(b.kind); const r = vault.set(b, 'input'); audit.log({ by: user.id, action: 'vault.set', targetType: 'vault', target: (r && r.id) || b.id || b.kind, detail: { kind: b.kind, label: b.label } }); return send(res, 200, r); }
|
||||
// v1.3.2: .key 파일 자체를 올려 가져오기 (multipart: kind, id?, label?, file) — 파일은 저장하지 않고 메모리에서 읽는다
|
||||
if (req.method === 'POST' && arg === 'upload') { const { fields: b, files } = await readBody(req); const f = files[0]; if (!f) return send(res, 400, { error: '키 파일을 고르세요' }); if (f.size > 64 * 1024) return send(res, 400, { error: '키 파일이 너무 큽니다 (64KB 이하)' }); const text = Buffer.from(await f.arrayBuffer()).toString('utf8'); const r = vault.importText(b.kind, text, { id: b.id, label: b.label }, `upload:${String(f.name || 'key').slice(0, 60)}`); audit.log({ by: user.id, action: 'vault.upload', targetType: 'vault', target: (r && r.id) || b.id || b.kind, detail: { kind: b.kind, label: b.label, file: String(f.name || '').slice(0, 60) } }); return send(res, 200, r); }
|
||||
if (req.method === 'POST' && arg === 'import') { const { fields: b } = await readBody(req); const r = vault.importFile(b.kind, b.file, { id: b.id, label: b.label }); audit.log({ by: user.id, action: 'vault.import', targetType: 'vault', target: (r && r.id) || b.id || b.kind, detail: { kind: b.kind, label: b.label, file: b.file } }); return send(res, 200, r); }
|
||||
if (req.method === 'POST' && arg === 'upload') { const { fields: b, files } = await readBody(req); vaultLocked(b.kind); const f = files[0]; if (!f) return send(res, 400, { error: '키 파일을 고르세요' }); if (f.size > 64 * 1024) return send(res, 400, { error: '키 파일이 너무 큽니다 (64KB 이하)' }); const text = Buffer.from(await f.arrayBuffer()).toString('utf8'); const r = vault.importText(b.kind, text, { id: b.id, label: b.label }, `upload:${String(f.name || 'key').slice(0, 60)}`); audit.log({ by: user.id, action: 'vault.upload', targetType: 'vault', target: (r && r.id) || b.id || b.kind, detail: { kind: b.kind, label: b.label, file: String(f.name || '').slice(0, 60) } }); return send(res, 200, r); }
|
||||
if (req.method === 'POST' && arg === 'import') { const { fields: b } = await readBody(req); vaultLocked(b.kind); const r = vault.importFile(b.kind, b.file, { id: b.id, label: b.label }); audit.log({ by: user.id, action: 'vault.import', targetType: 'vault', target: (r && r.id) || b.id || b.kind, detail: { kind: b.kind, label: b.label, file: b.file } }); return send(res, 200, r); }
|
||||
if (req.method === 'POST' && arg && sub2 === 'test') return send(res, 200, await vault.test(arg));
|
||||
if (req.method === 'DELETE' && arg) { vault.remove(arg); audit.log({ by: user.id, action: 'vault.remove', targetType: 'vault', target: arg }); return send(res, 200, { ok: true }); }
|
||||
if (req.method === 'DELETE' && arg) { const it0 = vault.list().find((x) => x.id === arg); vaultLocked(it0 && it0.kind); vault.remove(arg); audit.log({ by: user.id, action: 'vault.remove', targetType: 'vault', target: arg }); return send(res, 200, { ok: true }); }
|
||||
}
|
||||
// ---- 메일 시스템 (v1.3.0): 전체 켜기/끄기·메일 서버 주소, 웹메일을 켠 사이트 목록 ----
|
||||
// ---- 메일 시스템 (v1.3.2: 사이트(호스팅 도메인) 단위로 켜기) ----
|
||||
@@ -3219,10 +3223,12 @@ function staticFile(res, name, status = 200) {
|
||||
|
||||
http.createServer((req, res) => {
|
||||
handle(req, res).catch((err) => {
|
||||
if (!res.headersSent) send(res, err.status || 500, { error: err.message });
|
||||
if (!res.headersSent) send(res, err.status || 500, { error: err.message, ...(err.code === 'managed_by_hal' ? { code: err.code, keys: err.keys } : {}) }); // v1.9.0 HAL 관리 항목 잠금은 코드·키를 함께 알린다
|
||||
else res.end();
|
||||
});
|
||||
}).listen(PORT, HOST, () => {
|
||||
// v1.9.0 (Ariweb Suite HAL 0.4.0 대응): HAL 이 Agent 로 내려준 통합 설정(DATA_ROOT/hal-managed.json)을 적용하고, 파일이 바뀌면 다시 적용한다
|
||||
try { halManaged.start(); } catch (e) { console.error(`[hal-managed] 시작 실패: ${e.message}`); }
|
||||
console.log(`webhosting-agent listening on http://${HOST}:${PORT} sites=${SITES_ROOT} public=${PUBLIC_BASE_URL} model=${MODEL}`);
|
||||
// v1.6.31 (요구 12): 이전 프로세스가 남긴 AI 자식 프로세스 정리 · (요구 10) 손상 파일 안내
|
||||
try { const n = require('./lib/jobpids').sweep(); if (n) console.log(`[jobs] 이전 실행이 남긴 AI 작업 프로세스 ${n}개를 종료했습니다`); } catch (_) {}
|
||||
|
||||
Reference in New Issue
Block a user