Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions .github/scripts/check-no-memory-slugs.py
Original file line number Diff line number Diff line change
@@ -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 `[[<type>_<slug>]]` where `<type>`
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"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expand the guard beyond TS/JS/MD/YAML

When a leak lands in other tracked public text files, this extension allowlist skips it entirely; I checked the repo and there are public docs-site Vue components and shell installer scripts, while the workflow path filter also omits those suffixes, so a .vue/.sh-only PR would not even run the guard. That leaves internal [[feedback_*]]-style slugs able to pass CI outside the listed TS/JS/Markdown/YAML files, despite the job being intended to block leaks in public OSS files.

Useful? React with 👍 / 👎.


# 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/",
Comment on lines +81 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep legacy doc allowlists from hiding new leaks

Because these entire documentation trees are skipped, any new [[feedback_*]]-style slug added under docs/rfcs/, docs/sop/, docs/research/, docs/troubleshooting/, or docs/tests/ will pass the new guard along with the existing legacy references; I verified the scanner returns no finding for a fresh docs/rfcs/new.md leak. If the goal is to stop new leaks while old docs are audited later, this needs a baseline/diff-based exemption rather than a whole-tree skip.

Useful? React with 👍 / 👎.

)


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())
59 changes: 59 additions & 0 deletions .github/workflows/no-memory-slugs.yml
Original file line number Diff line number Diff line change
@@ -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 .
10 changes: 5 additions & 5 deletions agent-network/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1321,8 +1321,8 @@ function createProfileFromOpts(id: string, opts: ReturnType<typeof parseOpts>):
// 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"
Expand Down Expand Up @@ -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号
Expand Down
2 changes: 1 addition & 1 deletion agent-network/docs/feishu-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <test-node>` 落盘 `.env` + `access.json`,`.env` 权限 = 600,`config.json` `channels` 含 `"feishu"`。
Expand Down
6 changes: 3 additions & 3 deletions agent-node/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions server/src/db-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading