mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
fix(lib): jsonl-store's docstring stops lying; mode option added; lib bypasses adopted
The header claimed 'single source of truth... the ONLY copy' with write-time injection REJECTION — while appendJsonl never screened anything, only 1 of ~10 JSONL stores imported it, and a bypass appender lived in the same directory. Now: the contract is explicit (screening is the CALLER's job via hasInjection/firstInjectionMatch; the enforcing callers are named), a option applies 0600 at create for sensitive stores, and the lib bypasses are adopted (gstack-memory-helpers ×2, redact-audit-log — which keeps its chmod backstop for files created looser by pre-mode versions). browse/src keeps its own appenders by design (compiled-binary surface, own secure-append helper) and the header now says so. gstack-decision's batched archive append stays deliberate (single-write crash-window semantics appendJsonl's one-record contract can't express). New pins: 0600-at-create, and a test that documents appendJsonl does NOT self-screen — so nobody can re-document it as self-screening without making it true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3023216b87
commit
ef0fa9e9ff
@@ -17,7 +17,8 @@
|
||||
* helper warns once and returns an empty findings list — fail-safe defaults.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, appendFileSync } from "fs";
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
||||
import { appendJsonl } from "./jsonl-store";
|
||||
import { dirname, join } from "path";
|
||||
import { execFileSync } from "child_process";
|
||||
import { homedir } from "os";
|
||||
@@ -268,11 +269,7 @@ function logGbrainError(kind: string, detail: string): void {
|
||||
try {
|
||||
const path = errorLogPath();
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
appendFileSync(
|
||||
path,
|
||||
JSON.stringify({ ts: new Date().toISOString(), kind, detail: detail.slice(0, 500) }) + "\n",
|
||||
"utf-8"
|
||||
);
|
||||
appendJsonl(path, { ts: new Date().toISOString(), kind, detail: detail.slice(0, 500) });
|
||||
} catch { /* logging is best-effort */ }
|
||||
}
|
||||
|
||||
@@ -505,7 +502,7 @@ function logErrorContext(entry: ErrorContextEntry): void {
|
||||
try {
|
||||
const path = errorLogPath();
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
|
||||
appendJsonl(path, entry);
|
||||
} catch {
|
||||
// Logging failure is non-fatal — never block the op.
|
||||
}
|
||||
|
||||
+24
-12
@@ -1,17 +1,22 @@
|
||||
/**
|
||||
* jsonl-store — shared, audited plumbing for gstack's append-only JSONL stores.
|
||||
* jsonl-store — shared plumbing for gstack's append-only JSONL stores in
|
||||
* lib/ and bin/. (browse/src keeps its own appenders by design — the
|
||||
* compiled-binary surface has different logging semantics and its own
|
||||
* secure-append helper.)
|
||||
*
|
||||
* Single source of truth for the three things every JSONL store must get right:
|
||||
* 1. Injection sanitization (the prompt-injection patterns that must NOT survive
|
||||
* into agent context when a record is later resurfaced).
|
||||
* The three things a JSONL store must get right:
|
||||
* 1. Injection screening — SEE THE CONTRACT BELOW: appendJsonl does NOT
|
||||
* screen; callers that store free text MUST pre-check with
|
||||
* hasInjection()/firstInjectionMatch() and reject. Enforcing callers
|
||||
* today: bin/gstack-learnings-log, bin/gstack-decision-log (via
|
||||
* lib/gstack-decision.ts), bin/gstack-question-log.
|
||||
* 2. Atomic single-line append (concurrent agents must not corrupt the file).
|
||||
* 3. Tolerant read (a partially-written tail or one corrupt line must not take
|
||||
* down the whole read).
|
||||
* 3. Tolerant read (a partially-written tail or one corrupt line must not
|
||||
* take down the whole read).
|
||||
*
|
||||
* Extracted from `bin/gstack-learnings-log` (D2A) so `gstack-learnings-*` and the
|
||||
* new `gstack-decision-*` bins share ONE audited path — a new injection pattern or
|
||||
* a write-atomicity fix lands in both at once, never drifts. Per the
|
||||
* `squash-with-regen` / DRY discipline + the eng-review D2A decision.
|
||||
* Extracted from `bin/gstack-learnings-log` (D2A) so the learnings/decision/
|
||||
* question stores share ONE audited path — a new injection pattern or a
|
||||
* write-atomicity fix lands in all at once.
|
||||
*/
|
||||
|
||||
import { appendFileSync, readFileSync, existsSync } from "fs";
|
||||
@@ -60,12 +65,19 @@ export function firstInjectionMatch(text: string): RegExp | null {
|
||||
* Caveat: a record larger than PIPE_BUF loses the cross-process atomicity guarantee.
|
||||
* Keep records line-bounded; very large free-text should be truncated by the caller.
|
||||
*/
|
||||
export function appendJsonl(path: string, obj: unknown): void {
|
||||
export function appendJsonl(path: string, obj: unknown, opts: { mode?: number } = {}): void {
|
||||
const line = JSON.stringify(obj);
|
||||
if (line.includes("\n")) {
|
||||
throw new Error("jsonl-store: record serialized to multiple lines (embedded newline)");
|
||||
}
|
||||
appendFileSync(path, line + "\n", { encoding: "utf-8" });
|
||||
// `mode` applies only when the append CREATES the file (POSIX open(2)
|
||||
// semantics) — pass 0o600 for stores holding sensitive content so the
|
||||
// file never exists world-readable.
|
||||
if (opts.mode !== undefined) {
|
||||
appendFileSync(path, line + "\n", { encoding: "utf-8", mode: opts.mode });
|
||||
} else {
|
||||
appendFileSync(path, line + "\n", { encoding: "utf-8" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { createHash } from "crypto";
|
||||
import { appendJsonl } from "./jsonl-store";
|
||||
|
||||
export interface SemanticReviewEntry {
|
||||
ts: string;
|
||||
@@ -43,7 +44,9 @@ export function appendSemanticReview(entry: SemanticReviewEntry): void {
|
||||
const dir = securityDir();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, "semantic-reviews.jsonl");
|
||||
fs.appendFileSync(file, JSON.stringify(entry) + "\n");
|
||||
// 0600 at create via appendJsonl's mode opt; the chmod backstop covers
|
||||
// files created looser by pre-mode versions.
|
||||
appendJsonl(file, entry, { mode: 0o600 });
|
||||
try {
|
||||
fs.chmodSync(file, 0o600);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user