Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions lib/audit.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -54,4 +72,7 @@ function log(action, ctx) {
});
}

module.exports = { log: log };
module.exports = {
log: log,
auditLogPath: function () { return AUDIT_LOG_PATH; },
};
48 changes: 46 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
}

Expand All @@ -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
Expand Down Expand Up @@ -382,6 +425,7 @@ module.exports = {
socketPath: socketPath,
oldSocketPath: oldSocketPath,
logPath: logPath,
oldLogPath: oldLogPath,
ensureConfigDir: ensureConfigDir,
loadConfig: loadConfig,
saveConfig: saveConfig,
Expand Down
2 changes: 1 addition & 1 deletion lib/server-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
23 changes: 23 additions & 0 deletions test/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});
4 changes: 3 additions & 1 deletion test/security.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading