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