diff --git a/lib/audit.js b/lib/audit.js index e791297..741a7b5 100644 --- a/lib/audit.js +++ b/lib/audit.js @@ -1,12 +1,30 @@ // audit.js — Append-only audit log for privileged actions. -// Writes JSON lines to ~/.clagentic/audit.log (mode 0o600). +// Writes JSON lines to ~/.clagentic/console/audit.log (mode 0o600). // Never throws into callers: all errors are logged to stderr only. var fs = require("fs"); var path = require("path"); var config = require("./config"); -var AUDIT_LOG_PATH = path.join(config.CONFIG_DIR, "audit.log"); +var AUDIT_LOG_PATH = path.join(config.CONFIG_DIR, "console", "audit.log"); +var OLD_AUDIT_LOG_PATH = path.join(config.CONFIG_DIR, "audit.log"); + +// One-time migration: copy old audit.log to new location if new is absent. +// Copy-not-rename so rollback still finds the old file. +// Only runs if CLAGENTIC_HOME is not explicitly set (default layout). +// Note: this runs at module load time, potentially before ensureConfigDir() creates +// console/ — so we ensure the parent dir exists ourselves before copying. +if (!process.env.CLAGENTIC_HOME && !fs.existsSync(AUDIT_LOG_PATH) && fs.existsSync(OLD_AUDIT_LOG_PATH)) { + try { + fs.mkdirSync(path.dirname(AUDIT_LOG_PATH), { recursive: true }); + fs.copyFileSync(OLD_AUDIT_LOG_PATH, AUDIT_LOG_PATH); + try { fs.chmodSync(AUDIT_LOG_PATH, 0o600); } catch (_) {} + console.warn("[audit] Migrated audit.log to ~/.clagentic/console/audit.log"); + } catch (e) { + console.error("[audit] Migration of audit.log failed (non-fatal):", e.message); + } +} + var _logFd = null; function _getLogFd() { @@ -54,4 +72,7 @@ function log(action, ctx) { }); } -module.exports = { log: log }; +module.exports = { + log: log, + auditLogPath: function () { return AUDIT_LOG_PATH; }, +}; diff --git a/lib/config.js b/lib/config.js index 7b8ed10..2dabfcc 100644 --- a/lib/config.js +++ b/lib/config.js @@ -52,14 +52,16 @@ if (!fs.existsSync(CLAGENTIC_HOME) && fs.existsSync(OLD_CLAY_HOME)) { } var CONFIG_DIR = CLAGENTIC_HOME; -var EXTERNAL_TRIGGERS_DIR = path.join(CONFIG_DIR, "external-triggers"); +var EXTERNAL_TRIGGERS_DIR = path.join(CONFIG_DIR, "console", "external-triggers"); +var OLD_EXTERNAL_TRIGGERS_DIR = path.join(CONFIG_DIR, "external-triggers"); var OLD_CLAYRC = path.join(REAL_HOME, ".clayrc"); var CLAGENTIC_RC_PATH = path.join(REAL_HOME, ".clagentic-rc"); // One-time migration: copy .clayrc → .clagentic-rc if new file absent if (!fs.existsSync(CLAGENTIC_RC_PATH) && fs.existsSync(OLD_CLAYRC)) { try { fs.copyFileSync(OLD_CLAYRC, CLAGENTIC_RC_PATH); } catch (e) {} } -var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json"); +var CRASH_INFO_PATH = path.join(CONFIG_DIR, "console", "crash.json"); +var OLD_CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json"); // Dev mode uses separate daemon files so dev and prod can run simultaneously var _devMode = !!(process.env.CLAGENTIC_DEV || process.env.CLAY_DEV); @@ -84,6 +86,11 @@ function oldSocketPath() { } function logPath() { + return path.join(CONFIG_DIR, "console", _devMode ? "daemon-dev.log" : "daemon.log"); +} + +// Path used by pre-1.x daemons (before lr-5dca moved logs into console/). +function oldLogPath() { return path.join(CONFIG_DIR, _devMode ? "daemon-dev.log" : "daemon.log"); } @@ -97,6 +104,42 @@ function ensureConfigDir() { chmodSafe(CONFIG_DIR, 0o700); fs.mkdirSync(path.join(CONFIG_DIR, "console"), { recursive: true }); chmodSafe(path.join(CONFIG_DIR, "console"), 0o700); + // One-time migrations: copy Console-owned runtime files to console/ subdir. + // Rules: copy-not-rename (rollback safety), never overwrite newer dest, + // non-fatal on permission errors, skip when CLAGENTIC_HOME is custom. + if (!process.env.CLAGENTIC_HOME) { + // daemon.log / daemon-dev.log + var oldLog = oldLogPath(); + var newLog = logPath(); + if (fs.existsSync(oldLog) && !fs.existsSync(newLog)) { + try { + fs.copyFileSync(oldLog, newLog); + console.warn("[config] Migrated " + oldLog + " → " + newLog); + } catch (e) { + console.error("[config] Migration of daemon log failed (non-fatal):", e.message); + } + } + // crash.json + if (fs.existsSync(OLD_CRASH_INFO_PATH) && !fs.existsSync(CRASH_INFO_PATH)) { + try { + fs.copyFileSync(OLD_CRASH_INFO_PATH, CRASH_INFO_PATH); + try { fs.chmodSync(CRASH_INFO_PATH, 0o600); } catch (_) {} + console.warn("[config] Migrated crash.json → " + CRASH_INFO_PATH); + } catch (e) { + console.error("[config] Migration of crash.json failed (non-fatal):", e.message); + } + } + // external-triggers/ + if (fs.existsSync(OLD_EXTERNAL_TRIGGERS_DIR) && !fs.existsSync(EXTERNAL_TRIGGERS_DIR)) { + try { + fs.cpSync(OLD_EXTERNAL_TRIGGERS_DIR, EXTERNAL_TRIGGERS_DIR, { recursive: true }); + console.warn("[config] Migrated external-triggers/ → " + EXTERNAL_TRIGGERS_DIR); + } catch (e) { + console.error("[config] Migration of external-triggers/ failed (non-fatal):", e.message); + } + } + } + // Guard stale old socket cleanup: only unlink when the old-path daemon is // dead. If a live pre-1.5 daemon is still on that path we must not yank its // socket out from under it — warn instead so the operator can shut it down @@ -382,6 +425,7 @@ module.exports = { socketPath: socketPath, oldSocketPath: oldSocketPath, logPath: logPath, + oldLogPath: oldLogPath, ensureConfigDir: ensureConfigDir, loadConfig: loadConfig, saveConfig: saveConfig, diff --git a/lib/server-admin.js b/lib/server-admin.js index 9caf8bd..d484adb 100644 --- a/lib/server-admin.js +++ b/lib/server-admin.js @@ -745,7 +745,7 @@ function attachAdmin(ctx) { res.end('{"error":"Admin access required"}'); return true; } - var auditPath = require("path").join(require("./config").CONFIG_DIR, "audit.log"); + var auditPath = require("./audit").auditLogPath(); var fs = require("fs"); try { var raw = fs.readFileSync(auditPath, "utf8"); diff --git a/test/config.test.js b/test/config.test.js index 4716db7..e77492b 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -29,3 +29,26 @@ test("socketPath dev mode returns path inside ~/.clagentic/console/", function ( "expected socketPath() to contain 'console' subdir in all modes, got: " + sock ); }); + +test("logPath returns path inside ~/.clagentic/console/", function () { + var log = config.logPath(); + assert.ok( + log.includes(path.join("console", "daemon.log")), + "expected logPath() to contain console/daemon.log, got: " + log + ); +}); + +test("crashInfoPath returns path inside ~/.clagentic/console/", function () { + var crash = config.crashInfoPath(); + assert.ok( + crash.includes(path.join("console", "crash.json")), + "expected crashInfoPath() to contain console/crash.json, got: " + crash + ); +}); + +test("EXTERNAL_TRIGGERS_DIR is inside ~/.clagentic/console/", function () { + assert.ok( + config.EXTERNAL_TRIGGERS_DIR.includes(path.join("console", "external-triggers")), + "expected EXTERNAL_TRIGGERS_DIR to contain console/external-triggers, got: " + config.EXTERNAL_TRIGGERS_DIR + ); +}); diff --git a/test/security.test.js b/test/security.test.js index ac5eccf..6047a60 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -578,7 +578,9 @@ test("context_sources_save filters out term: IDs the caller does not own", funct test("audit.log: writes a valid JSON line to the audit log file", function (t, done) { var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "audit-")); - var auditPath = path.join(tmpDir, "audit.log"); + // audit.js now writes to CONFIG_DIR/console/audit.log — create the subdir so openSync succeeds. + fs.mkdirSync(path.join(tmpDir, "console"), { recursive: true }); + var auditPath = path.join(tmpDir, "console", "audit.log"); // Temporarily override CONFIG_DIR by patching the loaded module's path resolution. // We re-require a fresh audit module with a patched config so it writes to tmpDir.