mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
fix(code-intelligence): consent that means what it says — polarity, receipts, read-only veto
Four review findings on the wave's own Phase 1 port, all red-first: 'consent <repo> no' recorded consent GRANTED (the CLI ignored the argument and always wrote true) — yes|no is now required and garbage records nothing; Sourcebot egress receipts claimed consented=true on paths that never checked consent — the actual consent state is threaded into every receipt, search is fail-closed on non-loopback, and the liveness probe's receipt says truthfully that it sends no repo content; repoPolicyVeto only honored the deny tier while gbrain refresh writes pages — write-class ops now veto on read-only too, matching the sync chokepoint, via one shared lib/gbrain-repo-policy-client.ts (win32 bash invocation, spawn-vs-unreadable error distinction) used by both call sites. Also: source ids get a host+path hash (same-name repos no longer collide), refresh timeout raised to 120s, availability probes run concurrently at 3s, graphify status stops JSON.parsing 100MB graphs for a count, and every ported file carries the fork MIT notice. +15 tests across the two suites.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
* per-repo indexing consent. Stored at `$GSTACK_HOME/code-intelligence.json`
|
||||
* (default `~/.gstack/`), the same home the rest of gstack uses.
|
||||
*
|
||||
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
|
||||
*
|
||||
* Consent is per-repo (keyed by absolute repo path), because indexing consent
|
||||
* is "may THIS repo's content be indexed by the selected provider" — a decision
|
||||
* a user makes per project, not once for the machine. No selection at all is the
|
||||
@@ -12,8 +14,9 @@
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { execFileSync, spawnSync } from "child_process";
|
||||
import type { CodeProviderId } from "./contract";
|
||||
import { execFileSync } from "child_process";
|
||||
import { hasRepoPolicyStore, repoPolicyTier } from "../gbrain-repo-policy-client";
|
||||
import type { CodeProviderId, OpClass } from "./contract";
|
||||
|
||||
export interface Selection {
|
||||
provider: CodeProviderId | null;
|
||||
@@ -76,14 +79,20 @@ export function setConsent(repoPath: string, consented: boolean, env: NodeJS.Pro
|
||||
* The per-remote trust store (gstack-gbrain-repo-policy) is the SINGLE
|
||||
* authority for consent-to-send: a `deny` tier vetoes any recorded
|
||||
* code-intelligence consent, so two stores can never disagree about whether
|
||||
* code may leave this repo (R1, fork port wave 2 review). Mirrors the
|
||||
* gbrain-sync chokepoint's polarity: no policy store → no veto (nothing was
|
||||
* ever set); unreadable store → veto (fail-closed — a policy the user set
|
||||
* must not be bypassed by a broken store).
|
||||
* code may leave this repo (R1, fork port wave 2 review). The veto is
|
||||
* op-class-aware (R2): `read-only` means "search allowed, page writes never"
|
||||
* (the exact semantics runCodeImport in bin/gstack-gbrain-sync.ts enforces —
|
||||
* code ingest writes pages), so it vetoes write-class ops (register / index /
|
||||
* refresh / add / delete) while read-class ops (search / export / status)
|
||||
* pass; `deny` vetoes both classes. Mirrors the gbrain-sync chokepoint's
|
||||
* polarity: no policy store → no veto (nothing was ever set); unreadable
|
||||
* store OR unspawnable policy helper → veto for every op class (fail-closed —
|
||||
* a policy the user set must not be bypassed by a broken store or a helper
|
||||
* that can't run). Reads through the shared lib/gbrain-repo-policy-client.ts
|
||||
* so this site and the gbrain-sync gate can never drift.
|
||||
*/
|
||||
function repoPolicyVeto(repoPath: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const home = env.GSTACK_HOME || join(homedir(), ".gstack");
|
||||
if (!existsSync(join(home, "gbrain-repo-policy.json"))) return false;
|
||||
function repoPolicyVeto(repoPath: string, opClass: OpClass, env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (!hasRepoPolicyStore(env)) return false; // fast path: nothing was ever set — skip the git spawn too
|
||||
let url = "";
|
||||
try {
|
||||
url = execFileSync("git", ["-C", resolve(repoPath), "remote", "get-url", "origin"], {
|
||||
@@ -93,17 +102,21 @@ function repoPolicyVeto(repoPath: string, env: NodeJS.ProcessEnv = process.env):
|
||||
return false; // no remote → policy (keyed by remote) has nothing set for this repo
|
||||
}
|
||||
if (!url) return false;
|
||||
const res = spawnSync(join(import.meta.dir, "..", "..", "bin", "gstack-gbrain-repo-policy"), ["get", url], {
|
||||
encoding: "utf-8", timeout: 10_000, env: { ...env } as NodeJS.ProcessEnv,
|
||||
});
|
||||
if (res.error || res.status !== 0) return true; // fail-closed
|
||||
const tier = (res.stdout || "").trim();
|
||||
return tier === "deny";
|
||||
const res = repoPolicyTier(url, env);
|
||||
if (res.error) return true; // fail-closed (unreadable store or spawn failure alike)
|
||||
if (res.tier === "deny") return true; // deny beats consent for every op class
|
||||
return res.tier === "read-only" && opClass === "write"; // read-only: writes never, reads pass
|
||||
}
|
||||
|
||||
export function hasConsent(repoPath: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
/**
|
||||
* Recorded per-repo consent, filtered through the repo-policy veto. `opClass`
|
||||
* defaults to "write" so a caller that doesn't classify its op gets the
|
||||
* fail-closed answer; pass "read" only for ops that write no pages (search /
|
||||
* export / status).
|
||||
*/
|
||||
export function hasConsent(repoPath: string, env: NodeJS.ProcessEnv = process.env, opClass: OpClass = "write"): boolean {
|
||||
if (readSelection(env).consents[resolve(repoPath)] !== true) return false;
|
||||
return !repoPolicyVeto(repoPath, env);
|
||||
return !repoPolicyVeto(repoPath, opClass, env);
|
||||
}
|
||||
|
||||
/** Record the repo path a provider last indexed, so search reads the same graph. */
|
||||
|
||||
Reference in New Issue
Block a user