mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 07:40:09 +02:00
feat(spec,cso): wire shared redaction — semantic pass + scan-at-sink + taxonomy
/spec Phase 4.5 rewrite: - Phase 4.5a: in-conversation semantic content review (named-criticism, customer complaints, unannounced strategy, NDA, codename bleed). Injection- hardened (a body containing the SEMANTIC_REVIEW marker forces flagged). Content-free audit trail to ~/.gstack/security/semantic-reviews.jsonl. - Phase 4.5b: replaces the inline 7-regex prose with the shared gstack-redact scan-at-sink (exact-byte temp file). Three enforcement points: pre-codex, pre-issue (files via --body-file from the scanned file), pre-archive (D2: sanitized body to the archive). --no-gate skips codex score only; redaction always runs, no flag disables it. /cso: renders the full generated taxonomy table as its canonical pattern catalog (shared source), keeps its git-history archaeology (different use case). lib/redact-audit-log.ts: 0600 append-only semantic-review trail (no body text). Resolver gains compact-table + brief-block variants so /spec references the catalog instead of inlining it (stays under the v1.47 size budget). Tests: extended spec invariants (semantic pass, scan-at-sink, no-promotion), audit-log, cso/spec alignment. All green; spec 1.050× / cso 1.046× baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* redact-audit-log — append-only forensic trail for the Phase 4.5a semantic
|
||||
* review (D5). Records WHETHER the semantic pass marked a body clean/flagged and
|
||||
* WHICH categories fired — never the body content. A body_sha256 lets a later
|
||||
* investigation confirm "the pass saw this exact draft and called it clean."
|
||||
*
|
||||
* The file (`~/.gstack/security/semantic-reviews.jsonl`) is sensitive metadata,
|
||||
* not "safe": it leaks repo names, timing, and a membership oracle via the hash.
|
||||
* Written 0600. Local-only — no third-party egress.
|
||||
*
|
||||
* Usable two ways:
|
||||
* - CLI: bun lib/redact-audit-log.ts '<json-line-without-ts/hash>' [body-file]
|
||||
* (the skill passes the outcome JSON + a path to the scanned body; we
|
||||
* stamp ts + body_sha256 and append.)
|
||||
* - import { appendSemanticReview } from "./redact-audit-log";
|
||||
*/
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export interface SemanticReviewEntry {
|
||||
ts: string;
|
||||
spec_archive_path?: string;
|
||||
repo_visibility: string;
|
||||
outcome: "clean" | "flagged";
|
||||
categories_flagged: string[];
|
||||
body_sha256: string;
|
||||
}
|
||||
|
||||
function securityDir(): string {
|
||||
const home = process.env.GSTACK_HOME || path.join(os.homedir(), ".gstack");
|
||||
return path.join(home, "security");
|
||||
}
|
||||
|
||||
export function sha256(s: string): string {
|
||||
return createHash("sha256").update(s, "utf8").digest("hex");
|
||||
}
|
||||
|
||||
/** Append one entry. Best-effort: never throws into the caller's flow. */
|
||||
export function appendSemanticReview(entry: SemanticReviewEntry): void {
|
||||
try {
|
||||
const dir = securityDir();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, "semantic-reviews.jsonl");
|
||||
fs.appendFileSync(file, JSON.stringify(entry) + "\n");
|
||||
try {
|
||||
fs.chmodSync(file, 0o600);
|
||||
} catch {
|
||||
// chmod can fail on some filesystems; the append still happened.
|
||||
}
|
||||
} catch {
|
||||
// audit log is best-effort, not the security boundary
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function now(): string {
|
||||
// Date is allowed here (CLI process, not a resumable workflow).
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const json = process.argv[2];
|
||||
const bodyFile = process.argv[3];
|
||||
if (!json) {
|
||||
process.stderr.write(
|
||||
'usage: redact-audit-log \'{"repo_visibility":"public","outcome":"flagged","categories_flagged":["legal"],"spec_archive_path":"..."}\' [body-file]\n',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
let partial: Partial<SemanticReviewEntry>;
|
||||
try {
|
||||
partial = JSON.parse(json);
|
||||
} catch {
|
||||
process.stderr.write("redact-audit-log: invalid JSON\n");
|
||||
process.exit(1);
|
||||
}
|
||||
const body = bodyFile && fs.existsSync(bodyFile) ? fs.readFileSync(bodyFile, "utf8") : "";
|
||||
appendSemanticReview({
|
||||
ts: now(),
|
||||
repo_visibility: partial.repo_visibility ?? "unknown",
|
||||
outcome: partial.outcome === "flagged" ? "flagged" : "clean",
|
||||
categories_flagged: partial.categories_flagged ?? [],
|
||||
body_sha256: sha256(body),
|
||||
...(partial.spec_archive_path ? { spec_archive_path: partial.spec_archive_path } : {}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user