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
+7 -4
View File
@@ -23,9 +23,9 @@ import { homedir } from 'node:os';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2); const force = args.includes('--force'); const only = args.includes('--only') ? args[args.indexOf('--only') + 1] : null; const allowDirty = args.includes('--allow-dirty');
const SOURCES = [
{ name: 'webmaker', repo: 'https://source.ibit.kr/OZ-Group/WebHosting_AI_agent', src: process.env.WEBMAKER_SRC || join(homedir(), 'work', 'WebHoting_AI_Agent'), dest: 'modules/webmaker', pin: '4b8c4d0de0f69025001a9e0a28a58a94e1a1f088',
{ name: 'webmaker', repo: 'https://source.ibit.kr/OZ-Group/WebHosting_AI_agent', src: process.env.WEBMAKER_SRC || join(homedir(), 'work', 'WebHoting_AI_Agent'), dest: 'modules/webmaker', pin: '8f8716cc2d59fedd94c9916f7f65c3cfdcd7a6af',
exclude: [/^reports\//, /^sample\//, /\.pdf$/, /^Dev_Order/, /^WebHosting_AI_agent_/, /^CHANGELOG\.md$/, /^ERRORS\.md$/, /^\.gitignore$/], license: 'OZ-Group 내부 전용(private: true, 별도 LICENSE 없음) — 같은 조직의 Ariweb Suite 에서 재사용 허용. 외부 배포 불가.' },
{ name: 'arimail', repo: 'https://source.ibit.kr/OZ-Group/AriMail', src: process.env.ARIMAIL_SRC || join(homedir(), 'work', 'mail-gateway'), dest: 'modules/arimail', pin: 'a07f69f1ffb6667de6e66b3d1ec43c1c66fab924',
{ name: 'arimail', repo: 'https://source.ibit.kr/OZ-Group/AriMail', src: process.env.ARIMAIL_SRC || join(homedir(), 'work', 'mail-gateway'), dest: 'modules/arimail', pin: 'f959b656f410289e024001f1f4712f2737c3b42b',
exclude: [/^CHANGELOG\.md$/, /^\.gitignore$/, /^deploy\/sites\//], license: 'OZ-Group 내부 전용(private: true, 별도 LICENSE 없음) — 같은 조직의 Ariweb Suite 에서 재사용 허용. 외부 배포 불가. haraka/ 는 npm Haraka(MIT) 를 설치 시점에 내려받는 래퍼만 포함.' },
];
const sha = (f) => createHash('sha256').update(readFileSync(f)).digest('hex').slice(0, 16);
@@ -45,6 +45,7 @@ for (const s of SOURCES) {
const entry = { name: s.name, repo: s.repo, commit: head, dest: s.dest, license: s.license,
upstream: { version, branch, headCommit: head, workingTreeDirty: dirty, note: dirty ? '고정 커밋(commit) 위에 커밋되지 않은 작업 트리 변경을 함께 가져왔다. 원본 저장소에 커밋되면 pin 을 그 커밋으로 올려 다시 실행한다.' : '고정 커밋의 깨끗한 체크아웃' },
files: [] };
const prevFiles = new Map(((prevManifest.sources.find((x) => x.name === s.name) || {}).files || []).map((x) => [x.from, x.origHash]));
for (const f of files) {
const from = join(s.src, f); const to = join(ROOT, s.dest, f);
if (!statSync(from).isFile()) continue;
@@ -53,6 +54,8 @@ for (const s of SOURCES) {
if (!existsSync(to)) { mkdirSync(dirname(to), { recursive: true }); copyFileSync(from, to); status = 'imported'; }
else if (sha(to) === origHash) status = 'unchanged';
else if (force) { copyFileSync(from, to); status = 'restored'; }
// [0.4.0] pin 을 올릴 때: Suite 사본이 "이전에 가져온 원본"과 같으면(Suite 가 고치지 않은 파일) 새 원본으로 갱신한다. Suite 가 고친 파일만 'modified' 로 남겨 직접 옮긴다.
else if (prevFiles.get(f) && sha(to) === prevFiles.get(f)) { copyFileSync(from, to); status = 'updated'; }
else status = 'modified';
entry.files.push({ from: f, to: `${s.dest}/${f}`, status, origHash });
}
@@ -65,8 +68,8 @@ for (const e of manifest.sources) {
const mod = e.files.filter((f) => f.status === 'modified').length;
const up = e.upstream ? `- 원본 버전(package.json): ${e.upstream.version || '-'} · 브랜치: \`${e.upstream.branch || '-'}\` · 작업 트리: ${e.upstream.workingTreeDirty ? '**커밋되지 않은 변경 포함(--allow-dirty)** — ' + e.upstream.note : '깨끗함'}\n` : '';
md += `## ${e.name}\n\n- 원본 저장소: ${e.repo}\n- 고정 커밋(불변 기준): \`${e.commit}\`\n${up}- 대상 경로: \`${e.dest}/\`\n- 파일 수: ${e.files.length} (수정 ${mod})\n- 라이선스 검토: ${e.license}\n\n| 원본 경로 | 대상 경로 | 수정 | 원본 해시 |\n|---|---|---|---|\n`;
for (const f of e.files) md += `| ${f.from} | ${f.to} | ${f.status === 'modified' ? '**수정**' : f.status === 'restored' ? '원본 복원' : '그대로'} | ${f.origHash} |\n`;
for (const f of e.files) md += `| ${f.from} | ${f.to} | ${f.status === 'modified' ? '**수정**' : f.status === 'restored' ? '원본 복원' : f.status === 'updated' ? '새 원본으로 갱신' : '그대로'} | ${f.origHash} |\n`;
md += '\n';
}
writeFileSync(join(ROOT, 'IMPORTED_SOURCES.md'), md);
for (const e of manifest.sources) console.log(`${e.name}: ${e.files.length} 파일 (${e.files.filter((f) => f.status === 'imported').length} 새로 가져옴, ${e.files.filter((f) => f.status === 'modified').length} 수정됨)`);
for (const e of manifest.sources) console.log(`${e.name}: ${e.files.length} 파일 (${e.files.filter((f) => f.status === 'imported').length} 새로 가져옴, ${e.files.filter((f) => f.status === 'updated').length} 갱신, ${e.files.filter((f) => f.status === 'modified').length} 수정됨)`);