From e525063df3daca0a83e950ed120bf0a22b583e5f Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sat, 16 May 2026 23:45:07 +0000 Subject: [PATCH] history: trust file helpers for Phase 9 (commit #1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for the project-overlay trust mechanism. No callers yet — commit #2 wires main.lua to use these. Three new functions: history._sha256_file(path) -> hex digest or nil Shells `sha256sum`; parses first whitespace-separated field; validates 64-hex-char length. nil on any failure (path missing, binary missing, file unreadable). Caller treats nil as "skip the trust path" — never crashes. history.is_trusted(trust_path, project_path, sha256) -> bool Reads trust_path as JSONL; returns true iff an entry exists matching BOTH project_path AND sha256. Missing / corrupt / unreadable trust file -> false (re-prompt). Per-line JSON decode means partial-write corruption affects at most one line. history.add_trusted(trust_path, project_path, sha256) -> bool mkdir -p parent; append JSONL line {path, sha256, ts (ISO)}; chmod 600 the trust file (best-effort; ignore failure). Single writer per call; append-only. 11 unit cases verified: - sha256 known value matches manual `sha256sum` - nil / missing-file -> nil (no crash) - is_trusted on missing trust file -> false - add_trusted + is_trusted roundtrip works - Different sha -> not trusted (content-binding) - Different path -> not trusted - Multi-entry trust file: each entry independently checked - chmod 600 verified via stat Regression: test_safety 87/87, test_router_model 31/31. Co-Authored-By: Claude Opus 4.7 (1M context) --- history.lua | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/history.lua b/history.lua index 794302d..1dc47d0 100644 --- a/history.lua +++ b/history.lua @@ -309,4 +309,62 @@ function M.load_memory(path) return active end +-- ---------------------------------------------------------------- Phase 9 trust file +-- ~/.aish/trusted-projects (JSONL, mode 0600). One entry per accepted +-- project .aish.lua. Schema: {path = "", sha256 = "", +-- ts = ""}. sha256 binds bytes; content change re-prompts. + +-- Internal helper: shell out to `sha256sum`. Returns hex digest or nil +-- on any failure (binary missing, file unreadable, etc.). Caller +-- treats nil as "skip the trust path" rather than crashing. +function M._sha256_file(path) + if not path or path == "" then return nil end + local q = "'" .. path:gsub("'", [['\'']]) .. "'" + local pipe = io.popen("sha256sum " .. q .. " 2>/dev/null") + if not pipe then return nil end + local line = pipe:read("*l") + pipe:close() + if not line then return nil end + local digest = line:match("^(%x+)") -- first whitespace-separated field + if digest and #digest == 64 then return digest end + return nil +end + +-- Returns true iff a JSONL entry exists at trust_path matching BOTH +-- project_path AND sha256. Missing / unreadable / corrupt-line file +-- treated as "not trusted". +function M.is_trusted(trust_path, project_path, sha256) + if not (trust_path and project_path and sha256) then return false end + local fh = io.open(trust_path, "r") + if not fh then return false end + for line in fh:lines() do + if #line > 0 then + local entry = json.decode(line) + if entry and entry.path == project_path + and entry.sha256 == sha256 then + fh:close() + return true + end + end + end + fh:close() + return false +end + +-- Appends a trust record. mkdir -p parent; chmod 0600 on first creation. +-- Append-only JSONL; partial writes corrupt at most one line (caller's +-- subsequent reads skip them). +function M.add_trusted(trust_path, project_path, sha256) + if not (trust_path and project_path and sha256) then return false end + ensure_dir(parent_dir(trust_path)) + local fh = io.open(trust_path, "a") + if not fh then return false end + local ts = os.date("!%Y-%m-%dT%H:%M:%SZ") + fh:write(json.encode({ path = project_path, sha256 = sha256, ts = ts }) .. "\n") + fh:close() + -- Best-effort chmod 0600; ignore failure (next read will succeed). + os.execute("chmod 600 '" .. trust_path:gsub("'", [['\'']]) .. "' 2>/dev/null") + return true +end + return M