From 9c7a261a5b97985df78e94789eab175eff3703b4 Mon Sep 17 00:00:00 2001 From: vansin Date: Sun, 28 Jun 2026 09:21:48 +0800 Subject: [PATCH] chore(ci): add memory-slug leak guard + clean 6 P0 references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why === Several agents that contribute to this repo use a private file-based memory store indexed by `[[short-kebab-name]]` slugs. Those slugs are agent-internal — they only resolve inside that private store, so when they leak into committed source / docs the public OSS ends up with dangling reference markers and exposes internal process slang. A baseline scan found 63 such references across 17 files. Most live in the documentation trees (docs/sop/, docs/rfcs/, docs/research/, docs/troubleshooting/, docs/tests/) where the references are intentional design context that needs an owner-level audit before rewriting. The 6 references in production source + the user-facing feishu quickstart are unambiguous leaks and are cleaned here as part of the same change. What ==== .github/scripts/check-no-memory-slugs.py Pure-Python grep guard. Scans .ts/.tsx/.js/.jsx/.mjs/.cjs/.md/.yml/.yaml for the pattern `\[\[(feedback|project|reference|user)_\]\]`. Skips node_modules / dist / build / .git / memory stores. Allowlists docs/sop/, docs/rfcs/, docs/research/, docs/troubleshooting/, docs/tests/ for the initial rollout — those trees are tracked under a separate backlog audit. Self-allowlists this file + its workflow. .github/workflows/no-memory-slugs.yml CI hook that runs the Python guard on every PR + main push. 2 min timeout, concurrency cancel on non-main branches. Python (not in-yml bash) per the existing CI-guard pattern. Source cleanup (6 references, 4 files) - agent-node/src/cli.ts:1663 - agent-network/bin/cli.ts:1324 + :8199 - agent-network/docs/feishu-quickstart.md:98 (×2) - server/src/db-adapter.ts:237 Each rewrite drops the `[[slug]]` reference but keeps the surrounding rationale intact (the underlying rule is restated inline). Verification ============ - Guard returns exit 0 on the cleaned tree - agent-node bun test src/ — 274 / 0 - agent-network bun test — feishu suite green - server bun test (COMMHUB_DB=/tmp/...) — 131 / 0 Follow-up (separate) ==================== 57 references remain in the documentation trees noted above. Will open a backlog issue so the doc owners can audit + rewrite at their own pace. As each tree is audited, drop the corresponding prefix from the ALLOWLIST_PATH_PREFIXES list in check-no-memory-slugs.py. --- .github/scripts/check-no-memory-slugs.py | 137 +++++++++++++++++++++++ .github/workflows/no-memory-slugs.yml | 59 ++++++++++ agent-network/bin/cli.ts | 10 +- agent-network/docs/feishu-quickstart.md | 2 +- agent-node/src/cli.ts | 6 +- server/src/db-adapter.ts | 4 +- 6 files changed, 207 insertions(+), 11 deletions(-) create mode 100755 .github/scripts/check-no-memory-slugs.py create mode 100644 .github/workflows/no-memory-slugs.yml diff --git a/.github/scripts/check-no-memory-slugs.py b/.github/scripts/check-no-memory-slugs.py new file mode 100755 index 00000000..7e5d5cc8 --- /dev/null +++ b/.github/scripts/check-no-memory-slugs.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Block internal memory-slug references from leaking into public OSS files. + +Why this guard exists +===================== +Some agents that work on this repo use a private file-based memory system +indexed by `[[short-kebab-name]]` slugs (e.g. `[[feedback_no_test_on_prod]]`). +Those slugs are agent-internal: they only resolve inside the agent's +private memory store and have no meaning to outside contributors. Each +leak makes the public OSS look like it has dangling links and reveals +internal process slang. release.yml has burned us once on this class +already (Vincent caught a "Co-Authored-By: Claude" leak twice; the slug +shape is the same failure mode — internal artefact escaping to OSS via +auto-generated content). + +What this checks +================ +Scans the source tree for the pattern `[[_]]` where `` +is one of the four agent-memory categories: `feedback`, `project`, +`reference`, `user`. The matcher requires the leading double-bracket so +ordinary markdown reflinks `[label][ref]` don't false-positive. + +Skipped paths +============= +- `node_modules/`, `dist/`, `build/`, `coverage/` — generated/vendored +- `.git/` — VCS metadata +- `.claude/`, `~/.claude/`, `memory/` — these ARE the memory store +- This script itself + its workflow (talk about the pattern by design) + +Exit code: 0 if clean, 1 if any leak found (prints findings). +""" + +from __future__ import annotations + +import os +import re +import sys +from pathlib import Path + +# `\[\[(feedback|project|reference|user)_[a-z0-9_-]+\]\]` +# - leading `\[\[` to require the memory-link shape (not arbitrary `[x]`) +# - one of the four category prefixes (the agent memory types) +# - underscore separator + slug body (kebab/snake/digit chars) +# - closing `\]\]` +SLUG_RE = re.compile(r"\[\[(feedback|project|reference|user)_[a-z0-9_-]+(?:\.md)?\]\]") + +EXTENSIONS = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".md", ".yml", ".yaml"} + +# Walk-relative dir names to prune entirely. +SKIP_DIRS = { + "node_modules", + "dist", + "build", + "out", + "coverage", + ".git", + ".next", + ".turbo", + ".cache", + # Memory stores by convention. + "memory", + ".claude", +} + +# Files where the pattern is allowed (this guard talks about it by design). +SELF_ALLOWLIST = { + ".github/scripts/check-no-memory-slugs.py", + ".github/workflows/no-memory-slugs.yml", +} + +# Path-prefix allowlist for the initial rollout. The pre-existing 57 +# legacy references in these trees are historical design context — many +# RFCs intentionally cite the slug as the source of a decision and the +# SOP doc lists agent-memory categories by name. Cleaning them up is +# tracked as a backlog item separate from this guard, so we narrow the +# initial enforcement to production code + user-facing docs and let the +# documentation trees be audited offline at the owners' pace. +# +# REMOVE entries from this list as their backing tree is audited. +ALLOWLIST_PATH_PREFIXES = ( + "docs/sop/", + "docs/rfcs/", + "docs/research/", + "docs/troubleshooting/", + "docs/tests/", +) + + +def scan(root: Path) -> list[tuple[str, int, str]]: + findings: list[tuple[str, int, str]] = [] + for dirpath, dirnames, filenames in os.walk(root): + # Mutate dirnames in-place so os.walk doesn't descend into pruned dirs. + dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS] + for fname in filenames: + path = Path(dirpath) / fname + if path.suffix.lower() not in EXTENSIONS: + continue + rel = path.relative_to(root).as_posix() + if rel in SELF_ALLOWLIST: + continue + if any(rel.startswith(prefix) for prefix in ALLOWLIST_PATH_PREFIXES): + continue + try: + # `errors='replace'` so a stray binary masquerading as a text + # extension does not abort the whole scan. + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + for lineno, line in enumerate(text.splitlines(), start=1): + for match in SLUG_RE.finditer(line): + findings.append((rel, lineno, match.group(0))) + return findings + + +def main() -> int: + root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() + if not root.exists(): + print(f"error: scan root does not exist: {root}", file=sys.stderr) + return 2 + findings = scan(root) + if not findings: + print("OK: no internal memory-slug references found") + return 0 + print( + f"FAIL: found {len(findings)} internal memory-slug reference(s). " + "These are private agent-memory pointers and must not leak into " + "public OSS files — rewrite the comment to convey the intent " + "directly without the [[slug]] form.", + file=sys.stderr, + ) + for rel, lineno, snippet in findings: + print(f" {rel}:{lineno}: {snippet}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/no-memory-slugs.yml b/.github/workflows/no-memory-slugs.yml new file mode 100644 index 00000000..b717287a --- /dev/null +++ b/.github/workflows/no-memory-slugs.yml @@ -0,0 +1,59 @@ +# Block internal memory-slug references from leaking into public OSS files. +# +# Runs the Python grep guard in .github/scripts/check-no-memory-slugs.py +# on every PR + main push. Fails the build on any unexpected +# `[[feedback_*]]` / `[[project_*]]` / `[[reference_*]]` / `[[user_*]]` +# reference in source / docs (with a tree-prefix allowlist for the +# legacy documentation areas tracked under a separate backlog issue — +# see the script for the current allowlist). +# +# Python (not in-yml bash sed loop) per the team's CI-guard pattern: +# multi-pattern scans on large repos can run pathologically slow under +# bash on Windows runners; a small Python script is portable and easy +# to extend. + +name: lint (no internal memory-slug leak) + +on: + pull_request: + paths: + - '**/*.ts' + - '**/*.tsx' + - '**/*.js' + - '**/*.jsx' + - '**/*.mjs' + - '**/*.cjs' + - '**/*.md' + - '**/*.yml' + - '**/*.yaml' + - '.github/scripts/check-no-memory-slugs.py' + - '.github/workflows/no-memory-slugs.yml' + push: + branches: [main] + paths: + - '**/*.ts' + - '**/*.tsx' + - '**/*.js' + - '**/*.jsx' + - '**/*.mjs' + - '**/*.cjs' + - '**/*.md' + - '**/*.yml' + - '**/*.yaml' + - '.github/scripts/check-no-memory-slugs.py' + - '.github/workflows/no-memory-slugs.yml' + +concurrency: + group: lint-slugs-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + no-memory-slugs: + name: no internal [[feedback_*]] slug references + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v4 + + - name: Run check-no-memory-slugs.py + run: python3 .github/scripts/check-no-memory-slugs.py . diff --git a/agent-network/bin/cli.ts b/agent-network/bin/cli.ts index afe81849..8bb92c3d 100644 --- a/agent-network/bin/cli.ts +++ b/agent-network/bin/cli.ts @@ -1321,8 +1321,8 @@ function createProfileFromOpts(id: string, opts: ReturnType): // over the legacy DSP field, so writing both produced visible // "two-flag" clutter Vincent flagged as redundant). // - claude-code-cli: keeps writing `dangerouslySkipPermissions: true` - // ONLY (Vincent's "cli 不用改" + [[feedback_default_flags]] — - // CC reads DSP directly, not permissionMode). + // ONLY (Vincent's "cli 不用改" — Claude Code reads DSP directly, + // not permissionMode). // - codex-sdk / grok-build-acp: keep DSP for back-compat (legacy // consumers may read it). ...(runtime === "claude-agent-sdk" @@ -8194,9 +8194,9 @@ function sciTeamLifecycle(opts: { dir: string; restart: boolean; cleanup: boolea // finally block. // // Vendor presets must stay in sync with the Vincent-verified list at -// cli.ts L1116+ (1bc03c0 chain): adding a new preset here requires -// per-vendor verify-with-real-call, not byte-copy (see -// [[feedback_vendor_verify_before_hardcode]]). +// cli.ts L1116+ (1bc03c0 chain): adding a new preset here requires a +// real end-to-end API call against the vendor — do not copy parameters +// from another vendor's preset. interface BatchOptions { prefix: string; // alias 前缀, e.g. "工程师" → 工程师1号..工程师N号 diff --git a/agent-network/docs/feishu-quickstart.md b/agent-network/docs/feishu-quickstart.md index b2e39cf8..c247bffb 100644 --- a/agent-network/docs/feishu-quickstart.md +++ b/agent-network/docs/feishu-quickstart.md @@ -95,7 +95,7 @@ export ANET_FEISHU_WORKER_PATH=/path/to/your/worker.js ## E2E smoke checklist(测试派单用) -> 测试一律 Docker mock + 派测试号([[feedback_no_host_test_nodes]] / [[feedback_delegate_testing]]),不连本机 hub、不碰生产 db。Vincent 凭证活体 E2E 由通信龙调度。 +> 测试一律在 Docker mock 容器内由专用测试节点执行:不连本机 hub、不碰生产 db。Vincent 凭证活体 E2E 由项目负责人统一调度。 - [ ] **L0 环境** — Docker 容器内安装 `@sleep2agi/agent-network` + `@sleep2agi/agent-node`;mock 飞书 WSClient server 可达。 - [ ] **L1 配置** — `anet channel add feishu ` 落盘 `.env` + `access.json`,`.env` 权限 = 600,`config.json` `channels` 含 `"feishu"`。 diff --git a/agent-node/src/cli.ts b/agent-node/src/cli.ts index 09f4806c..e516bc64 100644 --- a/agent-node/src/cli.ts +++ b/agent-node/src/cli.ts @@ -1659,9 +1659,9 @@ const CODEX_INSTRUCTIONS = SYSTEM_PROMPT || [ // get the full commhub_send_task / get_all_status / etc. tool set, with // per-node identity (alias / token) inherited from this agent-node process. // -// Vincent retro 2026-06-17: "新成员都用不了 send_task" — new nodes default to -// codex-sdk runtime (per [[feedback_new_node_codex_default]]); for claude- -// agent-sdk runtime agent-node injects commhub via an in-process McpServer +// Vincent retro 2026-06-17: "新成员都用不了 send_task" — new nodes default +// to the codex-sdk runtime; for the claude-agent-sdk runtime agent-node +// injects commhub via an in-process McpServer // (createCommhubSdkMcpServer at line 1126); for codex-sdk no equivalent // in-process channel exists, and CODEX_CONFIG had ZERO mcp_servers field — // so codex inherited only whatever was in `~/.codex/config.toml` (a stale, diff --git a/server/src/db-adapter.ts b/server/src/db-adapter.ts index 5fc3a351..90cc5367 100644 --- a/server/src/db-adapter.ts +++ b/server/src/db-adapter.ts @@ -233,8 +233,8 @@ export class PgAdapter implements DbAdapter { * * Operators who run `bun -e` snippets that touch the DB are expected to * `COMMHUB_DB=/tmp/...` themselves — `bun -e` doesn't set NODE_ENV, so the - * guard can't catch that case. The rule is documented in CLAUDE.md / - * memory ([[feedback_no_prod_db_access.md]]). + * guard can't catch that case. The rule (never read or write the + * production hub database) is documented in CLAUDE.md. */ export function createAdapter(): DbAdapter { const dbUrl = process.env.DATABASE_URL;