From b5ca91ab2b333266ecafaa65ea60401ccb08bae6 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sun, 26 Jul 2026 19:07:30 -0500 Subject: [PATCH 1/4] Update interactions.jsonl --- .beads/interactions.jsonl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index b4a5e45..b863e46 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -11,5 +11,23 @@ {"id":"int-094b242d80d5d56af716debade6c8a56","kind":"field_change","created_at":"2026-07-26T13:19:06.284453Z","actor":"Val Alexander","issue_id":"cmem-8qg","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Standalone local-first memory dashboard Phase 1 completed and verified across UI and Coven daemon worktrees; changes remain uncommitted for maintainer review."}} {"id":"int-bbf4154db905ff91cc0cb843006db298","kind":"field_change","created_at":"2026-07-26T15:18:30.083579Z","actor":"Val Alexander","issue_id":"cmem-4z6","extra":{"field":"priority","new_value":"2","old_value":"3"}} {"id":"int-83059a741e02cac2641710019910778f","kind":"field_change","created_at":"2026-07-26T15:35:16.33239Z","actor":"Val Alexander","issue_id":"cmem-4k9.7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Responded"}} +{"id":"int-75688f635fa60623a4cd42c51351eba7","kind":"field_change","created_at":"2026-07-26T17:41:26.970634Z","actor":"Val Alexander","issue_id":"cmem-4z6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Strict per-request nonce CSP is implemented and independently verified, including prefetched HTML coverage."}} +{"id":"int-3a376f23359a24f0868a2ac3df65198b","kind":"field_change","created_at":"2026-07-26T18:46:34.968359Z","actor":"Val Alexander","issue_id":"cmem-4k9.8","extra":{"field":"status","new_value":"blocked","old_value":"in_progress"}} +{"id":"int-418708bdc1ea75851ce5f82699e42f38","kind":"field_change","created_at":"2026-07-26T18:59:50.783672Z","actor":"Val Alexander","issue_id":"cmem-4k9.8","extra":{"field":"status","new_value":"in_progress","old_value":"blocked"}} +{"id":"int-95b9f72fefe9f3cf3c7aee47ca12ff0d","kind":"field_change","created_at":"2026-07-26T19:18:10.639167Z","actor":"Val Alexander","issue_id":"cmem-4k9.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and verified the supplied Memory.dc.html handoff from the ZIP, preserving secure read-only local data/session/privacy boundaries."}} +{"id":"int-2f0b043f4c9d69b7118dfa7c809c5d3f","kind":"field_change","created_at":"2026-07-26T20:48:49.592797Z","actor":"Val Alexander","issue_id":"cmem-4k9.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance criteria satisfied by independently reviewed implementation and root browser/full-gate evidence at c7cc161."}} +{"id":"int-a3b32155b68f18a7a01362824219d968","kind":"field_change","created_at":"2026-07-26T20:48:50.137868Z","actor":"Val Alexander","issue_id":"cmem-4k9.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance criteria satisfied by independently reviewed implementation and root browser/full-gate evidence at c7cc161."}} +{"id":"int-fca076f04b054927264264305c1c6291","kind":"field_change","created_at":"2026-07-26T20:48:50.675374Z","actor":"Val Alexander","issue_id":"cmem-4k9.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance criteria satisfied by independently reviewed implementation and root browser/full-gate evidence at c7cc161."}} +{"id":"int-bf3870ba5cfb19426691ecc88972fbdb","kind":"field_change","created_at":"2026-07-26T20:48:51.171455Z","actor":"Val Alexander","issue_id":"cmem-4k9.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance criteria satisfied by independently reviewed implementation and root browser/full-gate evidence at c7cc161."}} +{"id":"int-319b50549e9fa67057b11180debc20a0","kind":"field_change","created_at":"2026-07-26T20:48:51.515253Z","actor":"Val Alexander","issue_id":"cmem-4k9.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance criteria satisfied by independently reviewed implementation and root browser/full-gate evidence at c7cc161."}} +{"id":"int-786a052567c1fd800c5e42a1b9fe3e6f","kind":"field_change","created_at":"2026-07-26T22:06:34.65912Z","actor":"Val Alexander","issue_id":"cmem-4k9.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Approved design handoff is faithfully reconciled into the hardened dashboard with independent no-findings review and complete root verification."}} +{"id":"int-a8c9bf4bdf6c6ab79f387e98f7d1477a","kind":"field_change","created_at":"2026-07-26T22:39:58.799175Z","actor":"Val Alexander","issue_id":"cmem-4k9.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Safe Markdown/source truthfulness acceptance is complete; authoritative summary source is merged after the compatible dashboard deployment, with cross-platform CI and exact-tree verification."}} +{"id":"int-5241d5455c0c42abbe4bb3c8a386c561","kind":"field_change","created_at":"2026-07-26T22:40:13.763027Z","actor":"Val Alexander","issue_id":"cmem-4k9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Comprehensive dashboard security and design-system UX hardening is merged and verified across both repositories; every child acceptance criterion is complete."}} +{"id":"int-623f0ab60d817b939dd95f233078fe92","kind":"field_change","created_at":"2026-07-26T23:14:53.995497Z","actor":"Val Alexander","issue_id":"cmem-7i6.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and verified process-local transport proof, loopback injection, and strict loopback/Tailscale Host-Origin guard."}} +{"id":"int-3b8822da215f159972b432a3abf5a632","kind":"field_change","created_at":"2026-07-26T23:21:18.104865Z","actor":"Val Alexander","issue_id":"cmem-7i6.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-db84506086a75aa221d78e38fe6f5812","kind":"field_change","created_at":"2026-07-26T23:25:28.656909Z","actor":"Val Alexander","issue_id":"cmem-7i6.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-bb83dd1e254f551904d7264e84ddc6bb","kind":"field_change","created_at":"2026-07-26T23:27:18.389227Z","actor":"Val Alexander","issue_id":"cmem-6g4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Audit complete: no unpublished inactive branch has a relevant delta; the sole new branch remains actively owned by cmem-7i6.4."}} +{"id":"int-4fab98e772e8b3769ac8c786e0f5ad39","kind":"field_change","created_at":"2026-07-26T23:36:35.98067Z","actor":"Val Alexander","issue_id":"cmem-7i6.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-c7b0fc45800ab6414db6ad52aad8264f","kind":"field_change","created_at":"2026-07-26T23:36:38.911625Z","actor":"Val Alexander","issue_id":"cmem-7i6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} {"id":"int-89520d833e15d70086dffa9bde0537a3","kind":"field_change","created_at":"2026-07-27T00:11:35.746186Z","actor":"Val Alexander","issue_id":"cmem-byc.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Replaced the single blocked familiar-workspace runtime path in cmem-8qg.4.1 notes with FAMILIAR_ROOT; fresh Beads-export guard passes."}} {"id":"int-43f83102ea4b17a1be7c7ab5ca17b0b5","kind":"field_change","created_at":"2026-07-27T07:26:26.516008Z","actor":"Val Alexander","issue_id":"cmem-7gn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented on fix/next-dev-csp-styles: development style-src now follows Next 16 guidance with self plus unsafe-inline, while production remains self plus the request nonce. TDD RED observed the old nonce-only development directive; focused 3/3 and full 138/138 tests passed. pnpm check passed lint, typecheck, tests, production build, smoke, and guard scan. Live dev response on disposable port 3755 emitted the expected development policy. No commit or push performed."}} From 20269b3ebc81f4564a5719120027b692bccc97a2 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 27 Jul 2026 13:11:34 -0500 Subject: [PATCH 2/4] fix: align privacy guard enforcement --- .github/workflows/privacy-guard.yml | 8 +- .gitleaks-default.toml | 5 + AGENTS.md | 2 +- SECURITY.md | 25 ++-- package.json | 4 +- scripts/check-instruction-sync.sh | 18 +++ scripts/guard-policy.test.mjs | 170 ++++++++++++++++++++++++++++ scripts/guard-scan.sh | 11 +- vitest.config.ts | 3 +- 9 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 .gitleaks-default.toml create mode 100644 scripts/check-instruction-sync.sh create mode 100644 scripts/guard-policy.test.mjs diff --git a/.github/workflows/privacy-guard.yml b/.github/workflows/privacy-guard.yml index 4185db2..62401df 100644 --- a/.github/workflows/privacy-guard.yml +++ b/.github/workflows/privacy-guard.yml @@ -25,10 +25,7 @@ jobs: sudo mv /tmp/gitleaks /usr/local/bin/gitleaks gitleaks version - - name: Full-history secret & privacy scan (gitleaks + Coven rules) - run: gitleaks detect --config .gitleaks.toml --no-banner --redact - - - name: Tracked-tree privacy pattern scan + - name: Full-history secret scan and tracked-tree privacy scan run: bash scripts/guard-scan.sh - name: PR diff privacy scan (changed files only, loud on hits) @@ -39,12 +36,13 @@ jobs: git diff --name-only -z "origin/${{ github.base_ref }}"...HEAD -- > /tmp/changed.zlist PATTERNS='agent:[a-z0-9_-]+:(telegram|imessage|discord|whatsapp|signal|webchat):|telegram:direct:[0-9]|(/Users/|/home/)[A-Za-z0-9._-]+|~/\.(openclaw|coven)/(agents|workspaces|credentials|sessions)|\+1[0-9]{10}' PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)\b' + ALLOW_MARKERS='guard-scan-allow|gitleaks:allow' FAIL=0 # NUL-delimited iteration: filenames containing whitespace must not dodge the scan while IFS= read -r -d '' f; do case "$f" in *.png|*.jpg|*.jpeg|*.gif|*.pdf|*.ico|*.woff*|*.db) continue;; esac [ -f "$f" ] || continue - if grep -EnI "$PATTERNS" "$f" | grep -vE "$PLACEHOLDERS" | grep -v "guard-scan-allow"; then + if grep -EnI "$PATTERNS" "$f" | grep -vE "$PLACEHOLDERS" | grep -vE "$ALLOW_MARKERS"; then echo "::error file=$f::Privacy pattern found — see SECURITY.md. Nothing local/personal/session-identifying may enter this public repo." FAIL=1 fi diff --git a/.gitleaks-default.toml b/.gitleaks-default.toml new file mode 100644 index 0000000..98bb940 --- /dev/null +++ b/.gitleaks-default.toml @@ -0,0 +1,5 @@ +# Default gitleaks rules only. This pass ignores inline `gitleaks:allow` +# annotations so a custom Coven privacy-rule exception can never suppress a +# standard secret finding. +[extend] +useDefault = true diff --git a/AGENTS.md b/AGENTS.md index 01341f2..836d208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,7 +91,7 @@ This protocol applies when ending a Beads implementation workflow. It is subordi # Team-maintainer opt-in only, unless current instructions forbid it: git pull --rebase - bd dolt push + scripts/bd-dolt-push.sh git push git status ``` diff --git a/SECURITY.md b/SECURITY.md index a7f98c0..a7eda33 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -66,13 +66,15 @@ memory must never be copied into tests, screenshots, traces, or issue notes. ## Enforcement layers (defense in depth) 1. **Pre-commit hook** — `scripts/guard-scan.sh --staged`: gitleaks (with - `.gitleaks.toml` Coven rules) + plain-pattern scan on staged content. + `.gitleaks.toml` Coven rules and a separate default-rules-only pass) + + plain-pattern scan on staged content. Fail-closed: if gitleaks is missing, the commit is blocked. 2. **Pre-push hook** — full-tree scan **plus a fresh `bd export` scan of the beads database**, because bead notes sync via dolt refs to this remote too. Fail-closed: if `bd export` fails, the push is blocked. -3. **CI (`privacy-guard.yml`)** — runs on every push and PR: full-history - gitleaks scan, tracked-tree scan, and a changed-files scan on PRs. +3. **CI (`privacy-guard.yml`)** — runs on every push and PR: the same + baseline-aware full-history gitleaks and tracked-tree scan as local hooks, + plus a changed-files scan on PRs. Local hooks can be skipped; **CI cannot**. CI is the authority for everything that reaches a branch. **Known limit:** bead notes sync via `bd dolt push` (refs/dolt/data), which @@ -93,12 +95,17 @@ Sync bead notes with `scripts/bd-dolt-push.sh` (guarded), not bare `bd dolt push ## False positives -For the plain-pattern scan, add the inline marker `guard-scan-allow` on the -flagged line. For a hit from a **custom Coven gitleaks rule**, use gitleaks' -own inline marker `gitleaks:allow` on the flagged line. Either way, justify it -in the PR description and reviewers must confirm the marker is legitimate. -Neither marker excuses a hit from the gitleaks **default** rules — real -secrets are never allowed, anywhere, including the guard files themselves. +For a reviewed false positive from a **custom Coven privacy rule**, add +`gitleaks:allow` on the flagged line. The gitleaks and plain-pattern tiers both +honour that marker; `guard-scan-allow` remains accepted only for existing, +reviewed annotations. Justify every marker in the PR description and have a +reviewer confirm it is legitimate. A separate `.gitleaks-default.toml` pass +uses `--ignore-gitleaks-allow`, so neither marker suppresses gitleaks default +rules: real secrets are never allowed, including in the guard files. + +`.gitleaks-baseline.json` is optional and suppresses only recorded historical +findings. When present, every local and CI gitleaks pass uses it; new findings +still fail every pass. ## Bead-notes discipline (contributors and familiars) diff --git a/package.json b/package.json index 92384a8..bbc4b70 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,14 @@ "test": "vitest run", "test:watch": "vitest", "test:browser": "node scripts/verify-dashboard-ui.mjs", + "test:guard": "node --test scripts/guard-policy.test.mjs", + "check:instruction-sync": "bash scripts/check-instruction-sync.sh", "audit:prod": "pnpm audit --prod --audit-level high", "fake-daemon": "node scripts/fake-memory-daemon.mjs", "test:smoke": "node scripts/smoke-dashboard.mjs", "test:package": "pnpm build:package && node --test scripts/dashboard-bin.test.mjs scripts/sanitize-build-artifact.test.mjs && node scripts/package-contents-test.mjs", "prepack": "pnpm build:package", - "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm test:smoke && ./scripts/guard-scan.sh" + "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm test:guard && pnpm build && pnpm test:smoke && pnpm check:instruction-sync && ./scripts/guard-scan.sh" }, "dependencies": { "@opencoven/coven-design-system": "git+https://github.com/OpenCoven/coven-design-system.git#6032f9f407982379e39ed1a40eec7a2e8b24e5c6", diff --git a/scripts/check-instruction-sync.sh b/scripts/check-instruction-sync.sh new file mode 100644 index 0000000..5578b03 --- /dev/null +++ b/scripts/check-instruction-sync.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Guard Beads sync guidance: Dolt note pushes bypass git hooks and branch CI. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +FAIL=0 +while IFS= read -r -d '' file; do + if grep -nE '^[[:space:]]*bd[[:space:]]+dolt[[:space:]]+push([[:space:]]|$)' "$file"; then + echo "instruction-sync: unguarded Beads sync command in $file." >&2 + echo "Use scripts/bd-dolt-push.sh so bead notes are scanned before sync." >&2 + FAIL=1 + fi +# These are instruction surfaces read directly by the supported agent tools; +# keep the list explicit so prose documentation is not treated as an executable +# command surface. +done < <(git ls-files -z -- '*AGENTS.md' '*CLAUDE.md' '*GEMINI.md' '*COPILOT.md' '.github/copilot-instructions.md' '.cursorrules') + +exit "$FAIL" diff --git a/scripts/guard-policy.test.mjs b/scripts/guard-policy.test.mjs new file mode 100644 index 0000000..4308289 --- /dev/null +++ b/scripts/guard-policy.test.mjs @@ -0,0 +1,170 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { chmod, copyFile, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const syntheticPrivacyPattern = ["agent", "example", "telegram", "direct", "123456"].join(":"); + +async function createGuardFixture() { + const directory = await mkdtemp(join(tmpdir(), "coven-memory-guard-")); + const scriptsDirectory = join(directory, "scripts"); + const binDirectory = join(directory, "bin"); + const invocationLog = join(directory, "gitleaks.log"); + + await mkdir(scriptsDirectory, { recursive: true }); + await mkdir(binDirectory, { recursive: true }); + const guardScript = await readFile(join(repositoryRoot, "scripts/guard-scan.sh"), "utf8"); + await writeFile( + join(scriptsDirectory, "guard-scan.sh"), + guardScript.replace('export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"', 'export PATH="$PATH"') + ); + await copyFile(join(repositoryRoot, ".gitleaks.toml"), join(directory, ".gitleaks.toml")); + await copyFile(join(repositoryRoot, ".gitleaks-default.toml"), join(directory, ".gitleaks-default.toml")); + await writeFile(join(directory, ".gitleaks-baseline.json"), "[]\n"); + await writeFile( + join(binDirectory, "gitleaks"), + "#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$GITLEAKS_LOG\"\n" + ); + await chmod(join(binDirectory, "gitleaks"), 0o755); + + runOk("git", ["init", "-q"], directory); + runOk("git", ["config", "user.email", "guard@example.invalid"], directory); + runOk("git", ["config", "user.name", "Guard Test"], directory); + runOk("git", ["config", "commit.gpgsign", "false"], directory); + runOk("git", ["add", "scripts/guard-scan.sh", ".gitleaks.toml"], directory); + runOk("git", ["commit", "-qm", "guard fixture"], directory); + + return { binDirectory, directory, invocationLog }; +} + +function run(command, args, cwd, environment = {}) { + return spawnSync(command, args, { + cwd, + encoding: "utf8", + env: { ...process.env, ...environment } + }); +} + +function runOk(command, args, cwd, environment = {}) { + const result = run(command, args, cwd, environment); + assert.equal(result.status, 0, result.stderr); + return result; +} + +function runGuard(fixture) { + return run("bash", ["scripts/guard-scan.sh"], fixture.directory, { + GITLEAKS_LOG: fixture.invocationLog, + PATH: `${fixture.binDirectory}:${process.env.PATH}` + }); +} + +async function withGuardFixture(callback) { + const fixture = await createGuardFixture(); + try { + await callback(fixture); + } finally { + await rm(fixture.directory, { force: true, recursive: true }); + } +} + +test("runs both baseline-aware gitleaks configurations", async () => { + await withGuardFixture(async (fixture) => { + const result = runGuard(fixture); + assert.equal(result.status, 0, result.stderr); + + const invocations = (await readFile(fixture.invocationLog, "utf8")) + .trim() + .split("\n"); + assert.equal(invocations.length, 2); + assert.match(invocations[0], /--config .gitleaks\.toml/); + assert.match(invocations[0], /--baseline-path .gitleaks-baseline\.json/); + assert.match(invocations[1], /--config .gitleaks-default\.toml/); + assert.match(invocations[1], /--baseline-path .gitleaks-baseline\.json/); + assert.match(invocations[1], /--ignore-gitleaks-allow/); + }); +}); + +test("accepts the documented marker in the plain-pattern scan", async () => { + await withGuardFixture(async (fixture) => { + await writeFile( + join(fixture.directory, "custom-rule-example.md"), + `${syntheticPrivacyPattern} gitleaks:allow\n` + ); + runOk("git", ["add", "custom-rule-example.md"], fixture.directory); + runOk("git", ["commit", "-qm", "marker fixture"], fixture.directory); + + const result = runGuard(fixture); + assert.equal(result.status, 0, result.stderr); + }); +}); + +test("rejects an unmarked privacy pattern with the approved remediation", async () => { + await withGuardFixture(async (fixture) => { + await writeFile(join(fixture.directory, "unmarked-example.md"), `${syntheticPrivacyPattern}\n`); + runOk("git", ["add", "unmarked-example.md"], fixture.directory); + runOk("git", ["commit", "-qm", "unmarked fixture"], fixture.directory); + + const result = runGuard(fixture); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Add inline marker: gitleaks:allow/); + }); +}); + +test("CI delegates history scanning to the baseline-aware guard script", async () => { + const workflow = await readFile(join(repositoryRoot, ".github/workflows/privacy-guard.yml"), "utf8"); + assert.doesNotMatch(workflow, /run: gitleaks detect/); + assert.match(workflow, /run: bash scripts\/guard-scan\.sh/); +}); + +test("instruction policy rejects executable bare Beads sync commands", async () => { + const currentPolicy = run("bash", ["scripts/check-instruction-sync.sh"], repositoryRoot); + assert.equal(currentPolicy.status, 0, currentPolicy.stderr); + + const fixtureDirectory = await mkdtemp(join(tmpdir(), "coven-memory-instructions-")); + try { + await writeFile(join(fixtureDirectory, "AGENTS.md"), "```bash\nbd dolt push\n```\n"); + runOk("git", ["init", "-q"], fixtureDirectory); + runOk("git", ["config", "user.email", "guard@example.invalid"], fixtureDirectory); + runOk("git", ["config", "user.name", "Guard Test"], fixtureDirectory); + runOk("git", ["config", "commit.gpgsign", "false"], fixtureDirectory); + runOk("git", ["add", "AGENTS.md"], fixtureDirectory); + runOk("git", ["commit", "-qm", "instruction fixture"], fixtureDirectory); + + const policy = run( + "bash", + [join(repositoryRoot, "scripts/check-instruction-sync.sh")], + fixtureDirectory + ); + assert.notEqual(policy.status, 0); + assert.match(policy.stderr, /Use scripts\/bd-dolt-push\.sh/); + } finally { + await rm(fixtureDirectory, { force: true, recursive: true }); + } +}); + +test("instruction policy covers every supported agent-instruction filename", async () => { + const fixtureDirectory = await mkdtemp(join(tmpdir(), "coven-memory-claude-instructions-")); + try { + await writeFile(join(fixtureDirectory, "CLAUDE.md"), "```bash\nbd dolt push\n```\n"); + runOk("git", ["init", "-q"], fixtureDirectory); + runOk("git", ["config", "user.email", "guard@example.invalid"], fixtureDirectory); + runOk("git", ["config", "user.name", "Guard Test"], fixtureDirectory); + runOk("git", ["config", "commit.gpgsign", "false"], fixtureDirectory); + runOk("git", ["add", "CLAUDE.md"], fixtureDirectory); + runOk("git", ["commit", "-qm", "instruction fixture"], fixtureDirectory); + + const policy = run( + "bash", + [join(repositoryRoot, "scripts/check-instruction-sync.sh")], + fixtureDirectory + ); + assert.notEqual(policy.status, 0); + assert.match(policy.stderr, /Use scripts\/bd-dolt-push\.sh/); + } finally { + await rm(fixtureDirectory, { force: true, recursive: true }); + } +}); diff --git a/scripts/guard-scan.sh b/scripts/guard-scan.sh index 6291b29..23470aa 100755 --- a/scripts/guard-scan.sh +++ b/scripts/guard-scan.sh @@ -28,8 +28,10 @@ fi if [ "$MODE" = "--staged" ]; then gitleaks protect --staged --config .gitleaks.toml --no-banner --redact ${BASELINE_ARGS[@]+"${BASELINE_ARGS[@]}"} || FAIL=1 + gitleaks protect --staged --config .gitleaks-default.toml --ignore-gitleaks-allow --no-banner --redact ${BASELINE_ARGS[@]+"${BASELINE_ARGS[@]}"} || FAIL=1 else gitleaks detect --config .gitleaks.toml --no-banner --redact ${BASELINE_ARGS[@]+"${BASELINE_ARGS[@]}"} || FAIL=1 + gitleaks detect --config .gitleaks-default.toml --ignore-gitleaks-allow --no-banner --redact ${BASELINE_ARGS[@]+"${BASELINE_ARGS[@]}"} || FAIL=1 fi # Belt-and-suspenders plain-pattern pass over tracked text files @@ -38,6 +40,9 @@ PATTERNS='agent:[a-z0-9_-]+:(telegram|imessage|discord|whatsapp|signal|webchat): # Mirrors the [rules.allowlist] regexes in .gitleaks.toml so both passes agree # on what counts as an obvious placeholder rather than a real home directory. PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)\b' +# `gitleaks:allow` is the documented marker for reviewed custom Coven-rule +# false positives. Keep the older guard marker for already-reviewed history. +ALLOW_MARKERS='guard-scan-allow|gitleaks:allow' if [ "$MODE" = "--staged" ]; then LIST=(git diff --cached --name-only --diff-filter=ACM -z) else @@ -53,7 +58,7 @@ while IFS= read -r -d '' f; do [ -f "$f" ] || continue CONTENT=$(cat "$f") fi - HITS=$(printf '%s' "$CONTENT" | grep -EnI "$PATTERNS" | grep -vE "$PLACEHOLDERS" | grep -v "guard-scan-allow" || true) + HITS=$(printf '%s' "$CONTENT" | grep -EnI "$PATTERNS" | grep -vE "$PLACEHOLDERS" | grep -vE "$ALLOW_MARKERS" || true) if [ -n "$HITS" ]; then echo "guard-scan: PRIVACY PATTERN in $f:" >&2 echo "$HITS" | head -5 >&2 @@ -74,7 +79,7 @@ if [ "${2:-}" = "--beads" ] || [ "$MODE" = "--beads" ]; then echo "guard-scan: bd export FAILED — bead notes cannot be verified. Fail-closed." >&2 exit 1 fi - BEAD_HITS=$(grep -EnI "$PATTERNS" "$TMP" | grep -vE "$PLACEHOLDERS" | grep -v "guard-scan-allow" || true) + BEAD_HITS=$(grep -EnI "$PATTERNS" "$TMP" | grep -vE "$PLACEHOLDERS" | grep -vE "$ALLOW_MARKERS" || true) if [ -n "$BEAD_HITS" ]; then echo "guard-scan: PRIVACY PATTERN in beads database (bd export). Clean bead notes before any dolt push." >&2 echo "$BEAD_HITS" | head -5 >&2 @@ -87,7 +92,7 @@ if [ "$FAIL" -ne 0 ]; then echo "" >&2 echo "guard-scan: BLOCKED. This is a public memory-layer repo — nothing local," >&2 echo "personal, or session-identifying may be committed. See SECURITY.md." >&2 - echo "False positive? Add inline marker: guard-scan-allow (reviewed in PR)." >&2 + echo "False positive for a Coven privacy rule? Add inline marker: gitleaks:allow (reviewed in PR). This never suppresses default secret rules." >&2 exit 1 fi echo "guard-scan: clean ($MODE)" diff --git a/vitest.config.ts b/vitest.config.ts index 4fbe17b..c496cf6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,7 +16,8 @@ export default defineConfig({ ...configDefaults.exclude, "**/.worktrees/**", "scripts/dashboard-bin.test.mjs", - "scripts/sanitize-build-artifact.test.mjs" + "scripts/sanitize-build-artifact.test.mjs", + "scripts/guard-policy.test.mjs" ] } }); From 516e3275033640c35920a058dd3f180500d163ce Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 27 Jul 2026 13:22:20 -0500 Subject: [PATCH 3/4] test: enforce privacy guard version and pattern parity --- .github/workflows/privacy-guard.yml | 7 ++--- .gitleaks-version | 1 + SECURITY.md | 6 ++++ scripts/guard-policy.test.mjs | 46 ++++++++++++++++++++++++++--- scripts/guard-scan.sh | 21 ++++++++----- scripts/privacy-patterns.sh | 5 ++++ 6 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 .gitleaks-version create mode 100644 scripts/privacy-patterns.sh diff --git a/.github/workflows/privacy-guard.yml b/.github/workflows/privacy-guard.yml index 62401df..c65d23f 100644 --- a/.github/workflows/privacy-guard.yml +++ b/.github/workflows/privacy-guard.yml @@ -20,7 +20,8 @@ jobs: - name: Install gitleaks run: | - curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz -o /tmp/gl.tgz + GITLEAKS_VERSION="$(tr -d '[:space:]' < .gitleaks-version)" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o /tmp/gl.tgz tar -xzf /tmp/gl.tgz -C /tmp gitleaks sudo mv /tmp/gitleaks /usr/local/bin/gitleaks gitleaks version @@ -34,9 +35,7 @@ jobs: git fetch origin "${{ github.base_ref }}" --depth=1 # Standalone diff so a git failure fails the step instead of emptying the list git diff --name-only -z "origin/${{ github.base_ref }}"...HEAD -- > /tmp/changed.zlist - PATTERNS='agent:[a-z0-9_-]+:(telegram|imessage|discord|whatsapp|signal|webchat):|telegram:direct:[0-9]|(/Users/|/home/)[A-Za-z0-9._-]+|~/\.(openclaw|coven)/(agents|workspaces|credentials|sessions)|\+1[0-9]{10}' - PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)\b' - ALLOW_MARKERS='guard-scan-allow|gitleaks:allow' + source scripts/privacy-patterns.sh FAIL=0 # NUL-delimited iteration: filenames containing whitespace must not dodge the scan while IFS= read -r -d '' f; do diff --git a/.gitleaks-version b/.gitleaks-version new file mode 100644 index 0000000..57b9fc1 --- /dev/null +++ b/.gitleaks-version @@ -0,0 +1 @@ +8.30.1 diff --git a/SECURITY.md b/SECURITY.md index a7eda33..10f59ad 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -84,6 +84,12 @@ memory must never be copied into tests, screenshots, traces, or issue notes. 4. **Review discipline** — PR reviewers treat any privacy hit as a blocker, never a warn-and-proceed. Same fail-closed principle as the promotion gate. +All gitleaks invocations must use the exact version in `.gitleaks-version`. +The local guard fails on version drift, and CI downloads that same tracked +release. Shell-based local and PR-diff patterns live in +`scripts/privacy-patterns.sh`; its privacy categories are checked against +`.gitleaks.toml` by the guard-policy test. + ## Contributor setup (one time, after clone) ```bash diff --git a/scripts/guard-policy.test.mjs b/scripts/guard-policy.test.mjs index 4308289..58a5a58 100644 --- a/scripts/guard-policy.test.mjs +++ b/scripts/guard-policy.test.mjs @@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const syntheticPrivacyPattern = ["agent", "example", "telegram", "direct", "123456"].join(":"); +const expectedGitleaksVersion = "8.30.1"; async function createGuardFixture() { const directory = await mkdtemp(join(tmpdir(), "coven-memory-guard-")); @@ -24,10 +25,12 @@ async function createGuardFixture() { ); await copyFile(join(repositoryRoot, ".gitleaks.toml"), join(directory, ".gitleaks.toml")); await copyFile(join(repositoryRoot, ".gitleaks-default.toml"), join(directory, ".gitleaks-default.toml")); + await copyFile(join(repositoryRoot, ".gitleaks-version"), join(directory, ".gitleaks-version")); + await copyFile(join(repositoryRoot, "scripts/privacy-patterns.sh"), join(scriptsDirectory, "privacy-patterns.sh")); await writeFile(join(directory, ".gitleaks-baseline.json"), "[]\n"); await writeFile( join(binDirectory, "gitleaks"), - "#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$GITLEAKS_LOG\"\n" + "#!/usr/bin/env bash\nif [ \"$1\" = version ]; then\n printf '%s\\n' \"${GITLEAKS_VERSION}\"\nelse\n printf '%s\\n' \"$*\" >> \"$GITLEAKS_LOG\"\nfi\n" ); await chmod(join(binDirectory, "gitleaks"), 0o755); @@ -35,7 +38,7 @@ async function createGuardFixture() { runOk("git", ["config", "user.email", "guard@example.invalid"], directory); runOk("git", ["config", "user.name", "Guard Test"], directory); runOk("git", ["config", "commit.gpgsign", "false"], directory); - runOk("git", ["add", "scripts/guard-scan.sh", ".gitleaks.toml"], directory); + runOk("git", ["add", "scripts/guard-scan.sh", "scripts/privacy-patterns.sh", ".gitleaks.toml"], directory); runOk("git", ["commit", "-qm", "guard fixture"], directory); return { binDirectory, directory, invocationLog }; @@ -55,10 +58,12 @@ function runOk(command, args, cwd, environment = {}) { return result; } -function runGuard(fixture) { +function runGuard(fixture, environment = {}) { return run("bash", ["scripts/guard-scan.sh"], fixture.directory, { GITLEAKS_LOG: fixture.invocationLog, - PATH: `${fixture.binDirectory}:${process.env.PATH}` + GITLEAKS_VERSION: expectedGitleaksVersion, + PATH: `${fixture.binDirectory}:${process.env.PATH}`, + ...environment }); } @@ -88,6 +93,39 @@ test("runs both baseline-aware gitleaks configurations", async () => { }); }); +test("pins the gitleaks version across local guard and CI", async () => { + const version = (await readFile(join(repositoryRoot, ".gitleaks-version"), "utf8")).trim(); + const workflow = await readFile(join(repositoryRoot, ".github/workflows/privacy-guard.yml"), "utf8"); + assert.equal(version, expectedGitleaksVersion); + assert.match(workflow, /GITLEAKS_VERSION="\$\(tr -d '\[:space:\]' < \.gitleaks-version\)"/); + assert.match(workflow, /gitleaks_\$\{GITLEAKS_VERSION\}_linux_x64\.tar\.gz/); + + await withGuardFixture(async (fixture) => { + const result = runGuard(fixture, { GITLEAKS_VERSION: "8.21.2" }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /expected gitleaks 8\.30\.1, found 8\.21\.2/); + }); +}); + +test("shares shell patterns and enforces gitleaks privacy categories", async () => { + const patterns = await readFile(join(repositoryRoot, "scripts/privacy-patterns.sh"), "utf8"); + const guard = await readFile(join(repositoryRoot, "scripts/guard-scan.sh"), "utf8"); + const workflow = await readFile(join(repositoryRoot, ".github/workflows/privacy-guard.yml"), "utf8"); + const gitleaks = await readFile(join(repositoryRoot, ".gitleaks.toml"), "utf8"); + + assert.match(patterns, /^PATTERNS='/m); + assert.match(patterns, /^PLACEHOLDERS='/m); + assert.match(patterns, /^ALLOW_MARKERS='/m); + const expectedIds = patterns.match(/^GITLEAKS_RULE_IDS='([^']+)'$/m)?.[1].split(" "); + assert.ok(expectedIds); + assert.match(guard, /source scripts\/privacy-patterns\.sh/); + assert.doesNotMatch(guard, /^PATTERNS=/m); + assert.match(workflow, /source scripts\/privacy-patterns\.sh/); + assert.doesNotMatch(workflow, /^\s+PATTERNS=/m); + const actualIds = [...gitleaks.matchAll(/^id = "([^"]+)"$/gm)].map((match) => match[1]); + assert.deepEqual(actualIds, expectedIds); +}); + test("accepts the documented marker in the plain-pattern scan", async () => { await withGuardFixture(async (fixture) => { await writeFile( diff --git a/scripts/guard-scan.sh b/scripts/guard-scan.sh index 23470aa..810f0b1 100755 --- a/scripts/guard-scan.sh +++ b/scripts/guard-scan.sh @@ -18,6 +18,17 @@ if ! command -v gitleaks >/dev/null 2>&1; then exit 1 fi +if [ ! -f .gitleaks-version ]; then + echo "guard-scan: .gitleaks-version is missing. Fail-closed." >&2 + exit 1 +fi +EXPECTED_GITLEAKS_VERSION="$(tr -d '\r\n' < .gitleaks-version)" +ACTUAL_GITLEAKS_VERSION="$(gitleaks version)" +if [ "$ACTUAL_GITLEAKS_VERSION" != "$EXPECTED_GITLEAKS_VERSION" ]; then + echo "guard-scan: expected gitleaks $EXPECTED_GITLEAKS_VERSION, found $ACTUAL_GITLEAKS_VERSION. Fail-closed." >&2 + exit 1 +fi + # Optional baseline of pre-existing HISTORICAL findings (see the repo's # security notes). Only suppresses findings already recorded in the baseline — # any NEW finding still fails. Absent in repos with clean history. @@ -35,14 +46,8 @@ else fi # Belt-and-suspenders plain-pattern pass over tracked text files -# (catches what regex-tuned tools miss; patterns mirror .gitleaks.toml) -PATTERNS='agent:[a-z0-9_-]+:(telegram|imessage|discord|whatsapp|signal|webchat):|telegram:direct:[0-9]|(/Users/|/home/)[A-Za-z0-9._-]+|~/\.(openclaw|coven)/(agents|workspaces|credentials|sessions)|\+1[0-9]{10}' -# Mirrors the [rules.allowlist] regexes in .gitleaks.toml so both passes agree -# on what counts as an obvious placeholder rather than a real home directory. -PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)\b' -# `gitleaks:allow` is the documented marker for reviewed custom Coven-rule -# false positives. Keep the older guard marker for already-reviewed history. -ALLOW_MARKERS='guard-scan-allow|gitleaks:allow' +# (catches what regex-tuned tools miss; categories are verified against .gitleaks.toml) +source scripts/privacy-patterns.sh if [ "$MODE" = "--staged" ]; then LIST=(git diff --cached --name-only --diff-filter=ACM -z) else diff --git a/scripts/privacy-patterns.sh b/scripts/privacy-patterns.sh new file mode 100644 index 0000000..a7ed670 --- /dev/null +++ b/scripts/privacy-patterns.sh @@ -0,0 +1,5 @@ +# Shared privacy-pattern contract for local guards and CI PR-diff scanning. +PATTERNS='agent:[a-z0-9_-]+:(telegram|imessage|discord|whatsapp|signal|webchat):|telegram:direct:[0-9]|(/Users/|/home/)[A-Za-z0-9._-]+|~/\.(openclaw|coven)/(agents|workspaces|credentials|sessions)|\+1[0-9]{10}' +PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)\b' +ALLOW_MARKERS='guard-scan-allow|gitleaks:allow' +GITLEAKS_RULE_IDS='coven-session-key coven-chat-id absolute-home-path openclaw-internal-path phone-number invite-or-handoff-url' From 7ac2e46ed45a8159402705c2f5105d79fe5ee3c0 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 29 Jul 2026 06:53:16 -0500 Subject: [PATCH 4/4] fix: harden guard portability --- scripts/guard-policy.test.mjs | 23 ++++++++++++++++++++++- scripts/guard-scan.sh | 2 +- scripts/privacy-patterns.sh | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/guard-policy.test.mjs b/scripts/guard-policy.test.mjs index 58a5a58..3a1cb06 100644 --- a/scripts/guard-policy.test.mjs +++ b/scripts/guard-policy.test.mjs @@ -30,7 +30,7 @@ async function createGuardFixture() { await writeFile(join(directory, ".gitleaks-baseline.json"), "[]\n"); await writeFile( join(binDirectory, "gitleaks"), - "#!/usr/bin/env bash\nif [ \"$1\" = version ]; then\n printf '%s\\n' \"${GITLEAKS_VERSION}\"\nelse\n printf '%s\\n' \"$*\" >> \"$GITLEAKS_LOG\"\nfi\n" + "#!/usr/bin/env bash\nif [ \"$1\" = version ]; then\n printf 'gitleaks version %s\\n' \"${GITLEAKS_VERSION}\"\nelse\n printf '%s\\n' \"$*\" >> \"$GITLEAKS_LOG\"\nfi\n" ); await chmod(join(binDirectory, "gitleaks"), 0o755); @@ -140,6 +140,27 @@ test("accepts the documented marker in the plain-pattern scan", async () => { }); }); +test("accepts documented placeholder home paths portably", async () => { + await withGuardFixture(async (fixture) => { + const placeholderHomePath = [ + "/Users", + "example", + ".coven", + "workspaces", + "demo" + ].join("/"); + await writeFile( + join(fixture.directory, "placeholder-example.md"), + `${placeholderHomePath}\n` + ); + runOk("git", ["add", "placeholder-example.md"], fixture.directory); + runOk("git", ["commit", "-qm", "placeholder fixture"], fixture.directory); + + const result = runGuard(fixture); + assert.equal(result.status, 0, result.stderr); + }); +}); + test("rejects an unmarked privacy pattern with the approved remediation", async () => { await withGuardFixture(async (fixture) => { await writeFile(join(fixture.directory, "unmarked-example.md"), `${syntheticPrivacyPattern}\n`); diff --git a/scripts/guard-scan.sh b/scripts/guard-scan.sh index 810f0b1..226d786 100755 --- a/scripts/guard-scan.sh +++ b/scripts/guard-scan.sh @@ -23,7 +23,7 @@ if [ ! -f .gitleaks-version ]; then exit 1 fi EXPECTED_GITLEAKS_VERSION="$(tr -d '\r\n' < .gitleaks-version)" -ACTUAL_GITLEAKS_VERSION="$(gitleaks version)" +ACTUAL_GITLEAKS_VERSION="$(gitleaks version | tr -d '\r' | awk '{print $NF}')" if [ "$ACTUAL_GITLEAKS_VERSION" != "$EXPECTED_GITLEAKS_VERSION" ]; then echo "guard-scan: expected gitleaks $EXPECTED_GITLEAKS_VERSION, found $ACTUAL_GITLEAKS_VERSION. Fail-closed." >&2 exit 1 diff --git a/scripts/privacy-patterns.sh b/scripts/privacy-patterns.sh index a7ed670..321a9cf 100644 --- a/scripts/privacy-patterns.sh +++ b/scripts/privacy-patterns.sh @@ -1,5 +1,5 @@ # Shared privacy-pattern contract for local guards and CI PR-diff scanning. PATTERNS='agent:[a-z0-9_-]+:(telegram|imessage|discord|whatsapp|signal|webchat):|telegram:direct:[0-9]|(/Users/|/home/)[A-Za-z0-9._-]+|~/\.(openclaw|coven)/(agents|workspaces|credentials|sessions)|\+1[0-9]{10}' -PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)\b' +PLACEHOLDERS='(/Users/|/home/)(<[a-z-]+>|\$USER|USERNAME|example|placeholder|you)([^[:alnum:]_]|$)' ALLOW_MARKERS='guard-scan-allow|gitleaks:allow' GITLEAKS_RULE_IDS='coven-session-key coven-chat-id absolute-home-path openclaw-internal-path phone-number invite-or-handoff-url'