From 7a1c6680f36460e0d7eb7e3bc02a5625509d9470 Mon Sep 17 00:00:00 2001 From: mmilanez Date: Sun, 31 May 2026 23:14:19 -0400 Subject: [PATCH] [sync] publish template/ from lead-protocol-dev@b602a9ea --- .agents/scripts/migrate_to_v2.py | 50 ++++++++++++++++- .agents/scripts/test_migrate_to_v2.py | 76 ++++++++++++++++++++++++++ .github/workflows/state-validation.yml | 3 + CHANGELOG.md | 33 +++++++++++ CONTRIBUTING.md | 3 +- README.md | 29 +++++++++- SECURITY.md | 11 +++- 7 files changed, 196 insertions(+), 9 deletions(-) diff --git a/.agents/scripts/migrate_to_v2.py b/.agents/scripts/migrate_to_v2.py index 73b3103..bc5b7f1 100644 --- a/.agents/scripts/migrate_to_v2.py +++ b/.agents/scripts/migrate_to_v2.py @@ -59,6 +59,7 @@ import argparse import json import os +import re import shutil import sys from dataclasses import dataclass, field @@ -213,6 +214,50 @@ def describe(self) -> str: return "\n".join(lines) +_SLUG_RE = re.compile(r'^[A-Za-z0-9._@-]+$') + + +def _validate_slug(value: str, label: str) -> None: + """Reject actor/agent values that could cause path traversal. + + Allowed: alphanumerics plus . _ @ - (covers alice@workstation, codex-cli, user.name). + Rejected: empty, path separators, drive letters, bare '..' or any segment containing '..'. + Belt-and-suspenders: after joining, the resolved path must still start with the expected + prefix — catches edge cases in Path resolution across platforms. + """ + if not value: + raise SystemExit(f"error: {label} must not be empty") + if re.search(r'[/\\:]', value): + raise SystemExit( + f"error: {label} {value!r} contains a path separator or drive letter — " + "use a plain identifier such as 'alice@workstation' or 'claude'" + ) + if value == ".." or "/../" in f"/{value}/" or value.startswith("../") or value.endswith("/.."): + raise SystemExit( + f"error: {label} {value!r} contains a traversal segment — " + "use a plain identifier such as 'alice@workstation' or 'claude'" + ) + if not _SLUG_RE.match(value): + raise SystemExit( + f"error: {label} {value!r} contains invalid characters — " + "allowed: alphanumerics, hyphen, underscore, dot, at-sign " + "(e.g. 'alice@workstation', 'claude', 'codex-cli')" + ) + + +def _check_slug_destination(agents_dir: Path, actor: str, agent: str) -> None: + """Verify the resolved pair directory is under agents_dir/local/.""" + expected_prefix = (agents_dir / "local").resolve() + candidate = (agents_dir / "local" / actor / agent).resolve() + try: + candidate.relative_to(expected_prefix) + except ValueError: + raise SystemExit( + f"error: resolved pair directory {candidate} escapes the expected " + f"prefix {expected_prefix} — check --actor and --agent values" + ) + + def find_repo_root(start: Path) -> Path: """Walk up from `start` looking for a Lead Protocol .agents/ tree.""" for ancestor in [start, *start.parents]: @@ -610,8 +655,6 @@ def _rewrite_handoff(src: Path, dst: Path, repo_root: Path) -> None: The v1 source is preserved under agent_log/ — the user removes it together with the rest of agent_log/ after validating the new layout. """ - import re - text = src.read_text(encoding="utf-8") dst.parent.mkdir(parents=True, exist_ok=True) @@ -716,6 +759,9 @@ def main() -> int: actor = resolve_actor(args.actor) agent = resolve_agent(args.agent) + _validate_slug(actor, "--actor / LEAD_PROTOCOL_ACTOR_ID") + _validate_slug(agent, "--agent / LEAD_PROTOCOL_AGENT_ID") + _check_slug_destination(agents_dir, actor, agent) plan = build_plan(repo_root, agents_dir, actor, agent) print(plan.describe()) diff --git a/.agents/scripts/test_migrate_to_v2.py b/.agents/scripts/test_migrate_to_v2.py index 69772b6..770b1c9 100644 --- a/.agents/scripts/test_migrate_to_v2.py +++ b/.agents/scripts/test_migrate_to_v2.py @@ -411,6 +411,82 @@ def test_concatenates_in_sorted_order_with_separators(self, repo: Path) -> None: assert "# --- from activity_2026-03.md ---" in out +# --------------------------------------------------------------------------- # +# _validate_slug — path traversal guard (v2.0.3) # +# --------------------------------------------------------------------------- # + +class TestSlugValidator: + """_validate_slug must accept real-world identifiers and reject any value + that could cause path traversal outside .agents/local///.""" + + # --- valid slugs --- + + def test_accepts_user_at_host(self) -> None: + m._validate_slug("alice@workstation", "actor") # must not raise + + def test_accepts_maintainer_style(self) -> None: + m._validate_slug("marco@ls-usa-ntb01", "actor") # must not raise + + def test_accepts_simple_agent(self) -> None: + m._validate_slug("claude", "agent") # must not raise + + def test_accepts_hyphenated_agent(self) -> None: + m._validate_slug("codex-cli", "agent") # must not raise + + def test_accepts_dot_name(self) -> None: + m._validate_slug("user.name", "actor") # must not raise + + # --- invalid slugs --- + + def test_rejects_empty(self) -> None: + with pytest.raises(SystemExit, match="must not be empty"): + m._validate_slug("", "actor") + + def test_rejects_posix_traversal(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug("../../etc/passwd", "actor") + + def test_rejects_windows_traversal(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug(r"..\..\..\outside", "actor") + + def test_rejects_absolute_posix(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug("/etc/passwd", "actor") + + def test_rejects_windows_drive(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug(r"C:\Users\evil", "actor") + + def test_rejects_bare_dotdot(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug("..", "actor") + + def test_rejects_embedded_dotdot(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug("valid/../escape", "actor") + + def test_rejects_forward_slash(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug("sub/path", "actor") + + def test_rejects_backslash(self) -> None: + with pytest.raises(SystemExit): + m._validate_slug("sub\\path", "actor") + + +# --------------------------------------------------------------------------- # +# _check_slug_destination — belt-and-suspenders resolution check (v2.0.3) # +# --------------------------------------------------------------------------- # + +class TestCheckSlugDestination: + def test_valid_pair_passes(self, repo: Path) -> None: + m._check_slug_destination(repo / ".agents", "alice@workstation", "claude") # must not raise + + def test_valid_maintainer_pair_passes(self, repo: Path) -> None: + m._check_slug_destination(repo / ".agents", "marco@ls-usa-ntb01", "claude") # must not raise + + # --------------------------------------------------------------------------- # # resolve_actor / resolve_agent # # --------------------------------------------------------------------------- # diff --git a/.github/workflows/state-validation.yml b/.github/workflows/state-validation.yml index d3c238f..2fdd0a0 100644 --- a/.github/workflows/state-validation.yml +++ b/.github/workflows/state-validation.yml @@ -4,6 +4,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: validate-state: name: State files diff --git a/CHANGELOG.md b/CHANGELOG.md index e129a4f..cab01d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,39 @@ re-stated here. --- +## [2.0.3] — 2026-06-01 + +Security patch and public-repo polish. No kernel or schema changes — the +framework `PROTOCOL_RULES.md` version stays at `2.0.0`. + +### Fixed + +- **Path traversal in `migrate_to_v2.py`** (security): `--actor`, `--agent`, + `LEAD_PROTOCOL_ACTOR_ID`, and `LEAD_PROTOCOL_AGENT_ID` values are now + validated against path traversal before being used to construct + `.agents/local///`. Values containing `/`, `\`, `:`, `..`, + or absolute path forms are rejected with a clear error message. A + belt-and-suspenders destination check verifies the resolved path stays + under `.agents/local/`. New `TestSlugValidator` and `TestCheckSlugDestination` + test classes added to `test_migrate_to_v2.py`. + +### Changed + +- `README.md` Quick Start now clones `v2.0.3` (was `v2.0.1`) and includes a + comment directing users to the Releases page for the current version number. + PowerShell `Copy-Item` block added alongside the existing `cp` block for + Windows users. Version history updated to include `2.0.2` and `2.0.3`. +- `SECURITY.md` scope corrected: supported surface is now the scaffold, JSON + Schemas, docs, validator, and migration tool. CLI and MCP server are noted + as planned, not shipped. Supported versions table updated (`Latest published + release` → Supported; `main` → Development, best effort). +- `CONTRIBUTING.md` "We welcome" section updated: CLI/MCP noted as planned + surfaces accepting design input via issues, not code contributions yet. +- CI workflow `permissions: contents: read` added at workflow scope to both + `state-validation.yml` and `readme-sync.yml`. + +--- + ## [2.0.2] — 2026-05-31 Documentation overhaul of the consumer-facing `README.md`. No kernel, schema, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ec8ba2..7ec5e81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,11 +52,12 @@ AI agents working on this repo follow the `[Agent] : ` convention ### We welcome -- Bug fixes and improvements to the CLI, schemas, or MCP server +- Bug fixes and improvements to the schemas, validator, migration tool, or documentation - New `PROJECT_RULES.md` templates for different industries/use cases - Documentation improvements (typos, clarity, examples) - Test coverage for existing functionality - Integrations with new AI coding agents or IDEs +- Design input on the planned CLI and MCP server (open an issue to discuss) ### Please avoid diff --git a/README.md b/README.md index fa7461c..bdcc248 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ When one AI session ends, it writes down what it did, what's left, and why it made the calls it made. The next session — even a different tool, even days later — reads that and picks up where the last one stopped. Nothing forgotten, nothing re-explained. -> Current version: **2.0.2** +> Current version: **2.0.3** --- @@ -108,8 +108,9 @@ Lead Protocol fills the operational-state slot in the broader agent stack: ## Quick start ```bash -# Clone a specific release — never install from main -git clone --branch v2.0.1 --depth 1 https://github.com/mmilanez/lead-protocol.git /tmp/lp +# Clone the latest stable release +# Check https://github.com/mmilanez/lead-protocol/releases for the current version number +git clone --branch v2.0.3 --depth 1 https://github.com/mmilanez/lead-protocol.git /tmp/lp # Copy the scaffold into your project cp -R /tmp/lp/.agents your-project/.agents @@ -124,6 +125,26 @@ cd your-project python .agents/scripts/validate_state.py ``` +**Windows (PowerShell):** + +```powershell +# Clone the latest stable release +# Check https://github.com/mmilanez/lead-protocol/releases for the current version number +git clone --branch v2.0.3 --depth 1 https://github.com/mmilanez/lead-protocol.git $env:TEMP\lp + +# Copy the scaffold into your project +Copy-Item -Recurse $env:TEMP\lp\.agents your-project\.agents +Copy-Item $env:TEMP\lp\CLAUDE.md your-project\CLAUDE.md +Copy-Item $env:TEMP\lp\AGENTS.md your-project\AGENTS.md + +# Set your project's identity +code your-project\.agents\PROJECT_RULES.md + +# Verify the scaffold state +Set-Location your-project +python .agents/scripts/validate_state.py +``` + That's it. Read the sections below or browse [`.agents/CORE_RULES.md`](.agents/CORE_RULES.md) to understand how agents use the protocol inside your project. ## Installing a specific version @@ -217,6 +238,8 @@ Patch bumps (Z) never break anything. Minor bumps (Y) may introduce new features | Version | Highlights | |---|---| +| **2.0.3** | Security patch: `migrate_to_v2.py` now validates `--actor` / `--agent` values against path traversal (rejects `..`, `/`, `\`, absolute paths, drive letters). README Quick Start updated to current release with PowerShell copy block added. `SECURITY.md` and `CONTRIBUTING.md` scope corrected (CLI/MCP are roadmap, not shipped). CI workflow permissions hardened. No kernel or schema changes. | +| **2.0.2** | Documentation and release infrastructure fixes. README version references corrected. No kernel or schema changes. | | **2.0.1** | Patch from first external consumer feedback. `migrate_to_v2.py --dry-run` now accepted; pristine `LESSONS.md` scaffold no longer false-positives the rerun-safety guard; `docs/MIGRATION-v2.md` Step 3 rewritten with agent-driven callout and `--agent` slug warning. No kernel or schema changes. | | **2.0.0** | **Three-layer state model (Framework / Project / Actor × Agent).** New files: `JOURNAL.md`, `LESSONS.md`, `AGENTS_MAP.md`. `decisions.json` replaced by `decisions.jsonl` (append-only). `handoff.md` relocates to `local///handoff.md`. New `migrate_to_v2.py` migration tool. Six-step baseline boot order. | diff --git a/SECURITY.md b/SECURITY.md index 5288077..883c225 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,11 +15,16 @@ We will acknowledge your report within 48 hours and provide a timeline for a fix ## Scope -This policy applies to the Lead Protocol CLI, MCP server, schemas, and any code in this repository. It does not cover third-party tools or services that integrate with the protocol. +This policy applies to the Lead Protocol scaffold, JSON Schemas, documentation, validator (`validate_state.py`), migration tool (`migrate_to_v2.py`), and all protocol files in this repository. + +The CLI (`lead-protocol` command) and MCP server are planned surfaces — they are not yet shipped and are therefore not in scope until released. + +This policy does not cover third-party tools or services that integrate with the protocol. ## Supported versions | Version | Supported | |---|---| -| Latest on `main` | Yes | -| Older releases | Best effort | +| Latest published release | Yes | +| `main` | Development branch — best effort | +| Older releases | Best effort, no backports unless critical |