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
50 changes: 48 additions & 2 deletions .agents/scripts/migrate_to_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import argparse
import json
import os
import re
import shutil
import sys
from dataclasses import dataclass, field
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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())
Expand Down
76 changes: 76 additions & 0 deletions .agents/scripts/test_migrate_to_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<actor>/<agent>/."""

# --- 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 #
# --------------------------------------------------------------------------- #
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/state-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
validate-state:
name: State files
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<actor>/<agent>/`. 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,
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ AI agents working on this repo follow the `[Agent] <type>: <summary>` 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

Expand Down
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

---

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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/<actor>/<agent>/handoff.md`. New `migrate_to_v2.py` migration tool. Six-step baseline boot order. |

Expand Down
11 changes: 8 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Loading