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
111 changes: 111 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# gitleaks configuration — mcp-ariel-memory
#
# Why this file exists.
#
# The default ruleset finds credentials. It structurally cannot find the other
# half of what this repository can leak: fixture rows that pin real close-out
# broadcasts from a live operator's history. One of those rows described a
# private host — node names, a tailnet address, the ssh user, a key path, an
# API-key variable name, tunnel parameters and ports. There is no credential
# shape in any of that: it is made of names, paths and roles, so gitleaks had
# nothing to match and the CI scan went green on a tree that already contained it.
#
# These rules are the house layer for infrastructure disclosure.
#
# DESIGN CONSTRAINT — read before extending.
#
# This file is committed to a public repository, so it is itself published. A
# rule that names one of our hosts publishes that host, and a "regression pin"
# listing the exact identifiers would re-leak the very thing it guards. The
# first draft of this file did exactly that. Therefore:
#
# no rule below contains a literal hostname, address, account, key name or
# persona handle of this house. Every rule describes a SHAPE — "this is what a
# tailnet address looks like", "this is what a private key path looks like" —
# and a shape is safe to publish while still being specific enough to fire.
#
# Rules matching our literal identifiers, if ever wanted, belong in an untracked
# local overlay, never here. The generic rules below are what caught the real
# incident (six of them fire on it); literal pins were never the value.
#
# Deliberately NOT matched: generic RFC 1918 addresses. Examples, docker config
# and tests in this repository use 10.x legitimately, and a rule that cries wolf
# gets muted. See CONTRIBUTING.md § Test Data and Fixtures.

[extend]
# Keep every default rule. This adds a layer; it does not trade one for another.
useDefault = true

[[rules]]
id = "house-tailscale-cgnat"
description = "Tailscale / CGNAT address (100.64.0.0/10) — a private tailnet node"
regex = '''\b100\.(?:6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.\d{1,3}\.\d{1,3}\b'''
keywords = ["100."]
tags = ["house", "infrastructure"]

[[rules]]
id = "house-tailnet-fqdn"
description = "Tailnet DNS name (*.ts.net) — names the private network"
regex = '''\b[a-z0-9][a-z0-9-]*\.ts\.net\b'''
keywords = [".ts.net"]
tags = ["house", "infrastructure"]

[[rules]]
id = "house-vps-hostname"
description = "Provider-side machine id (vm followed by 6-9 digits)"
regex = '''\bvm\d{6,9}\b'''
keywords = ["vm"]
tags = ["house", "infrastructure"]

[[rules]]
id = "house-operator-domain"
description = "Hostname-style mail/URL domain under a personal zone (*.cloud.<tld>)"
regex = '''\b[a-z0-9][a-z0-9.-]*\.cloud\.[a-z]{2,}\b'''
keywords = [".cloud."]
tags = ["house", "infrastructure"]

[[rules]]
id = "house-ssh-backup-user"
description = "Backup ssh account, in ssh context or as user@host"
regex = '''(?:ssh|scp|sftp|rsync)\s+(?:-\S+\s+|\S+\s+){0,3}backup-[a-z][a-z0-9_-]{2,}\b|\bbackup-[a-z][a-z0-9_-]{2,}@'''
keywords = ["backup-"]
tags = ["house", "access"]

[[rules]]
id = "house-ssh-key-path"
description = "Private key path under .ssh/ — pairs a key artifact with where it lives"
regex = '''(?:~|/(?:home|root))/?(?:[\w.-]+/)*\.ssh/[\w.-]*(?:key|pem|p12)\b'''
keywords = [".ssh/"]
tags = ["house", "access"]

[[rules]]
id = "house-secret-var-name"
description = "Name of a secret-bearing env var (FOO_API_KEY) — the name alone maps the perimeter"
regex = '''\b[A-Z][A-Z0-9]{2,}_API_KEY\b'''
keywords = ["_API_KEY"]
tags = ["house", "access"]

[[rules.allowlists]]
description = "A workflow that names its own secret (secrets.FOO_API_KEY) discloses nothing about a host — GitHub holds the value. The rule exists for names appearing in fixtures and docs, where a name sits next to a real machine."
paths = ['''\.github/workflows/''']

[[rules]]
id = "house-amneziawg-params"
description = "AmneziaWG obfuscation parameters (Jc/Jmin/Jmax) — a tunnel fingerprint"
regex = '''\b(?:Jc|Jmin|Jmax)\s*=\s*\d{1,4}\b'''
keywords = ["Jc", "Jmin", "Jmax"]
tags = ["house", "infrastructure"]

[[rules]]
id = "house-amneziawg-config-path"
description = "AmneziaWG interface config path"
regex = '''/etc/amneziawg/[\w.-]+\.conf'''
keywords = ["/etc/amneziawg/"]
tags = ["house", "infrastructure"]

[[rules]]
id = "house-tunnel-key-material"
description = "WireGuard/AmneziaWG key material (base64, 44 chars) on a key= line"
regex = '''(?i)\b(?:private|preshared)[-_]?key\s*=\s*[A-Za-z0-9+/]{43}='''
keywords = ["privatekey", "presharedkey", "preshared"]
tags = ["house", "access"]
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ repos:
args: [--fix]
- id: ruff-format

# House layer: secret scanning catches credentials, and infrastructure
# disclosure has no credential shape in it. Verified against the 2026-09-13
# leak commit: the default ruleset was silent, .gitleaks.toml fires on it
# eight times. Rules and rationale live in .gitleaks.toml; the contributor
# rule in CONTRIBUTING.md § Test Data and Fixtures.
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks

- repo: local
hooks:
- id: skylos
Expand Down
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,40 @@ Format: `<type>(<scope>): <description>`

Types: `feat`, `fix`, `docs`, `chore`, `test`, `refactor`, `perf`, `ci`, `build`

## Test Data and Fixtures

**Fixtures are shipped in the repository. Treat every fixture as public.**

This project pins real rows from its own history — broadcast reports, recall
episodes, session notes. That is deliberate and useful, but it means a fixture
can carry more than the behaviour it is meant to exercise. Most of this codebase's
tests are about memory, and memory is written by real operators about real machines.

Never commit these, in any form, including inside a fixture, a docstring, a test
constant or a comment:

- hostnames, node names, or machine identifiers — of this project's operators or anyone else's
- private-network addresses: RFC 1918, CGNAT (the `100.64/10` block, which includes
every Tailscale address), link-local, or WireGuard/VPN subnets
- ssh users, key paths, or the *names* of secret-bearing environment variables
- VPN or tunnel parameters: interface names, listen ports, key material, obfuscation settings
- service inventories above the level of the generic ("a backup service listens on
loopback"), and never with ports and paths attached
- personal names of operators and the people they live with

**Substitute, do not delete.** A fixture whose point is that a recon report is
classified as a broadcast must still look like a recon report. Keep the shape,
swap the identifiers — `node-b`, `<ssh-user>`, `<ssh-key-path>`,
`<secret-var-name>`, `10.9.1.0`. Anonymised rows catch regressions just as well.

**Note on secret scanning.** `detect-secrets` runs on every push and it will not
save you here. gitleaks matches credential *shapes* — provider prefixes, high-entropy
strings, private-key headers. Infrastructure disclosure has none of those: it is made
of names, paths and roles. A green scanner says "no credentials", not "nothing sensitive".

If you are unsure whether a fixture row is safe, anonymise it. There is no cost to
being generic and there is no way to un-publish a fork.

## Pull Request Rules

1. Fork the repo and create a branch from `master`
Expand All @@ -61,6 +95,7 @@ Types: `feat`, `fix`, `docs`, `chore`, `test`, `refactor`, `perf`, `ci`, `build`
- Test coverage for new features
- Type annotations (mypy passes)
- No regressions (all 338 tests pass)
- **No operator-identifying data in fixtures** (see [Test Data and Fixtures](#test-data-and-fixtures))
- Documentation updates if behavior changes

## Reporting Issues
Expand Down
4 changes: 2 additions & 2 deletions docs/compose/specs/2026-09-04-phase-fgh-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ WIKI = L4.5 knowledge layer — НАМЕРЕННАЯ запись, НЕ дист

> Мой v1-черновик (co-occurrence/similar_to/follows_in_time) ПОГЛОЩЁН списком из
> `a-memory-graph-miners.md` — он шире (8 сигналов + эмбеддинг-слой) и привязан к
> существующей инфраструктуре. Источник: /home/murat/cow/knowledge/analysis/a-memory-graph-miners.md
> существующей инфраструктуре. Источник: <home>/cow/knowledge/analysis/a-memory-graph-miners.md

**Фундамент (прежде минеров)**: рёбра пишут только builder'ы (B1.3 ночной, A1.6 communities,
MCP relates_to/causal) — реальных связей они не находят; recall-телеметрии пар НЕТ (нужен
Expand Down Expand Up @@ -410,7 +410,7 @@ MCP relates_to/causal) — реальных связей они не наход
5. **8 циклов автономии при 3 LLM-классах (nano/mini/gpt-4.1)** — экономика подтверждает E8-вердикт: гейт инициативы (WhisperGate) — это harness-задача с nano-моделью, ariel остаётся keyless. Патент-pending + «license subject to change» — ещё одна причина не заимствовать механики напрямую (только идеи, как с AGPL у OpenViking).

**Резюме**: MemoryMuse — пятый подряд конкурент с «взять»-листом внутри черновика. Ценное из деталей: per-kind капы и явный порядок inject-блоков (усиление F-спеки инъекции), private-флаг объединяющий C5+scratchpad. Стек (Mongo+Qdrant+Memgraph+docker) — антипример local-first.
- 2026-09-04: v14 — ПАЙПЛАЙН ЭЛИ/ЛИЛИ (a-memory-l0-l4-pipeline.md + a-memory-graph-miners.md, /home/murat/cow/knowledge/analysis/) принят как ОСНОВА Phase F и Phase G. Мой v1 поглощён. Диагноз кода проверен 04.09 — все дыры реальны (L2 без сообщений, staging_-обрубки, decay_rate игнорируется промоцией, remember дублирует в граф, 4 параллельных входа). Поправки к их доку: CLACK не найден (сжатие = A3+zlib), add_edge «не вызывается нигде» устарело (B1.3/A1.6 пишут, минеры не наполняют), sentence-transformers 6.0.0 уже в venv (минер #9 возможен сейчас). Phase F = их конвейер + мои гейты/журнал/replay; Phase G = их 8+1 минеров + моя санитария/graph_enrich-оркестратор. Ключевые новые обязательства: канонические ключи (не обрубки), kind-роутинг инвариант/событие в G1, противоречия → memory_conflicts вместо молчаливого UPDATE, wiki = L4.5 [[fact:]]-linking без дублирования, L0-тиры жизни 30/180 + дистилляция освобождает сырьё, журнал co-retrieval пар (новый), пре-чистка JSON-узлов перед минерами.
- 2026-09-04: v14 — ПАЙПЛАЙН ЭЛИ/ЛИЛИ (a-memory-l0-l4-pipeline.md + a-memory-graph-miners.md, <home>/cow/knowledge/analysis/) принят как ОСНОВА Phase F и Phase G. Мой v1 поглощён. Диагноз кода проверен 04.09 — все дыры реальны (L2 без сообщений, staging_-обрубки, decay_rate игнорируется промоцией, remember дублирует в граф, 4 параллельных входа). Поправки к их доку: CLACK не найден (сжатие = A3+zlib), add_edge «не вызывается нигде» устарело (B1.3/A1.6 пишут, минеры не наполняют), sentence-transformers 6.0.0 уже в venv (минер #9 возможен сейчас). Phase F = их конвейер + мои гейты/журнал/replay; Phase G = их 8+1 минеров + моя санитария/graph_enrich-оркестратор. Ключевые новые обязательства: канонические ключи (не обрубки), kind-роутинг инвариант/событие в G1, противоречия → memory_conflicts вместо молчаливого UPDATE, wiki = L4.5 [[fact:]]-linking без дублирования, L0-тиры жизни 30/180 + дистилляция освобождает сырьё, журнал co-retrieval пар (новый), пре-чистка JSON-узлов перед минерами.

## Sequencing (user decision 2026-09-04)

Expand Down
2 changes: 1 addition & 1 deletion docs/hooks/autohooks-platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Restart after ariel code updates: `systemctl --user restart ariel-autohooks-*`.
CowAgent additionally runs under a **system** unit `cowagent.service`
(Restart=always) — the agent itself, not the daemon; killing its PID
auto-respawns it with new code (do NOT manual-nohup: `-m app` needs
cwd=/home/murat/cowagent).
cwd=<home>/cowagent).

### Hermes — native `MemoryProvider` plugin (preferred path)

Expand Down
2 changes: 1 addition & 1 deletion tests/shared/test_importance_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from shared.importance.models import ImportanceConfig, ImportanceSignals, ScorerResult

# Repo-relative, not an absolute host path: CI checkouts live elsewhere
# (was /home/murat/Projects/repos/... → FileNotFoundError on GitHub Actions).
# (an absolute operator path → FileNotFoundError on GitHub Actions).
ASSET_PATH = Path(__file__).resolve().parents[2] / "shared" / "assets" / "importance_config.json"


Expand Down
6 changes: 3 additions & 3 deletions tests/test_features/test_continuity_actuality.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def test_recap_session_prefers_substantive(monkeypatch):
now = time.time()
rows = [
_sess("Memory audit session — no user interaction", 0, now - 60),
_sess("Migration day complete: opencode + vps2 VPN cutover", 52, now - 7200),
_sess("Migration day complete: opencode + node-b VPN cutover", 52, now - 7200),
]
monkeypatch.setattr("core.session.SessionStore", lambda: _FakeSessionStore(rows))

Expand Down Expand Up @@ -213,7 +213,7 @@ async def test_recap_notes_reads_latest_tail(recap_db, tmp_path, monkeypatch):
new_dir.mkdir(parents=True)
(old_dir / "notes.md").write_text("stale handoff\n", encoding="utf-8")
(new_dir / "notes.md").write_text(
"# Session notes\n\n## day close\n- POLZA key rotation pending\n",
"# Session notes\n\n## day close\n- PROVIDER key rotation pending\n",
encoding="utf-8",
)
monkeypatch.setattr(
Expand All @@ -224,7 +224,7 @@ async def test_recap_notes_reads_latest_tail(recap_db, tmp_path, monkeypatch):

blocks = await session_recap(_FakeMem(), "u1")
notes = next(b for b in blocks if b["axis"] == "recap_notes")
assert "POLZA key rotation pending" in notes["content"]
assert "PROVIDER key rotation pending" in notes["content"]
assert "stale handoff" not in notes["content"]


Expand Down
2 changes: 1 addition & 1 deletion tests/test_features/test_dream_anchor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_mid_text_marker_rejected():


def test_case_insensitive_still_works():
res = detect_dream_marker("dream: memory: server migrated to vm1282008")
res = detect_dream_marker("dream: memory: server migrated to srv-01")
assert res is not None and res["target"] == "memory"


Expand Down
4 changes: 2 additions & 2 deletions tests/test_features/test_dream_markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def test_detect_memory_target() -> None:

def test_detect_fact_case_insensitive() -> None:
# E18: marker must START the message — case-insensitivity still holds
m = detect_dream_marker("Dream: Fact: сервер vm1282045")
m = detect_dream_marker("Dream: Fact: сервер node-a")
assert m is not None
assert m["target"] == "fact"


def test_mid_text_marker_rejected() -> None:
# E18: the mid-text case the old test asserted is now the bug, not a feature
assert detect_dream_marker("drem ignored\nDream: Fact: сервер vm1282045") is None
assert detect_dream_marker("drem ignored\nDream: Fact: сервер node-a") is None


def test_detect_skill_target() -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_features/test_recall_episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ async def test_episode_axis_surfaces_recent_work(tmp_base):
summary="Personas CowAgent memory isolation second leak analysis",
created_at=now - 86400,
),
SimpleNamespace(summary="picoclaw router notes", created_at=now - 60 * 86400),
SimpleNamespace(summary="service-b router notes", created_at=now - 60 * 86400),
]
)
)
blocks = await recall_protocol(mem, None, "default", query="personas CowAgent memory isolation", budget=2000)
eps = [b for b in blocks if b["axis"] == "episodes"]
assert len(eps) == 1
assert "Personas" in eps[0]["content"]
assert "picoclaw" not in eps[0]["content"]
assert "service-b" not in eps[0]["content"]


@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion tests/test_features/test_skill_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def test_promote_creates_skill_page_and_tags():
from features.skill_pipeline import promote_episodes

wiki = _FakeWiki()
mem = _FakeMem([_ep(7, "Deploy ariel: ssh vm1282008, uv sync, restart units")])
mem = _FakeMem([_ep(7, "Deploy ariel: ssh srv-01, uv sync, restart units")])
res = await promote_episodes(mem, wiki, "u1", [7])
assert res["count"] == 1 and not res["skipped"]
page = wiki.pages[0]
Expand Down
Loading
Loading