history: trust file helpers for Phase 9 (commit #1)
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) <noreply@anthropic.com>
This commit is contained in:
+58
@@ -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 = "<abs>", sha256 = "<hex>",
|
||||
-- ts = "<iso>"}. 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
|
||||
|
||||
Reference in New Issue
Block a user