Skip to content
Open
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
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ versions per [PEP 440](https://peps.python.org/pep-0440/) /

_Post-1.0.0 work lands here; legis versions independently from the Weft 1.0 launch on._

## [1.3.0] — 2026-06-26

Suite-standard dot-dir hygiene, plus the legis-resident halves of six cross-repo
Weft seam conformance oracles.

### Added

- **Nested `.weft/legis/.gitignore` shipped at install (suite standard,
filigree-4ed8152630).** `legis install` (and `legis install --gitignore`) now
ships a nested `.weft/legis/.gitignore` alongside the existing project-root
`.weft/legis/` rule, so legis's machine-written runtime state stays out of
every commit even in a consuming repo that drops the root rule — closing
legis's part of the cross-suite "every tool ships a complete nested
`.gitignore` for its own dot-dir, with a durable-vs-ephemeral header"
standard. Unlike filigree's durable `filigree.db`, legis is the sole writer of
this subtree and commits **nothing** durable: the ignore enumerates the
governance/posture/pulls/checks/binding SQLite DBs (`legis-*.db`) and their
WAL/SHM/journal sidecars, names the operator-elevation secrets
(`operator.age`, `operator_session.json`) so a reviewer sees by name what is
never committed, and covers the atomic-write staging temps (`*.tmp`,
`.operator.age.*`) that `mkstemp` orphans in the dir on a failed write. The writer is marker-guarded and append-not-clobber (a
user-authored `.weft/legis/.gitignore` is preserved). `legis doctor` gains an
`install.dir_gitignore` check that flags an existing dot-dir missing the
nested ignore and ships it on `--repair` (an absent dot-dir is OK — created
lazily, nothing to protect yet).

### Testing — cross-repo Weft seam conformance oracles

- **Legis-resident halves of six cross-repo Weft seams**, each driving legis's
REAL code over a byte-identical golden with a Layer-1 byte-pin (fail-closed in
the default suite) plus a skip-clean Layer-2 source recheck: SEI
(loomweave→legis), git-renames (legis→loomweave), signoff-binding
(legis→filigree), loomweave-HMAC-wire (legis→loomweave, live-gated), the
warpline preflight read (legis consumer), and the per-SEI `attestation_get`
read (legis producer). Two outstanding peer obligations are recorded rather
than papered over — warpline ships no flat HTTP producer for the preflight
shape, and warpline's `LegisClient.governance_for_sei` is unwired — with the
Layer-2 rechecks armed to fire when each peer lands its half.

## [1.2.0] — 2026-06-25

Warpline federation interfaces: an advisory preflight consumer and a
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "legis"
version = "1.2.0"
version = "1.3.0"

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 Update the runtime version with the release

This release bumps the package metadata to 1.3.0, but src/legis/__init__.py still defines __version__ = "1.2.0", and the CLI, FastAPI health/version, and MCP serverInfo all report that runtime value. In source/editable installs or any path that imports legis.__version__, users and clients will see the previous version for a 1.3.0 release, so the runtime version should be bumped in the same release change.

Useful? React with 👍 / 👎.

description = "Legis — the git/CI + governance layer of the Weft suite"
readme = "README.md"
license = "MIT"
Expand Down
6 changes: 6 additions & 0 deletions src/legis/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ def _run_install(args) -> int:
OperatorKeyCustodyError,
choose_install_backend,
ensure_gitignore,
ensure_legis_dir_gitignore,
inject_instructions,
install_claude_code_hooks,
install_codex_skills,
Expand Down Expand Up @@ -658,6 +659,11 @@ def _do_posture() -> tuple[bool, str]:
(install_all or args.codex_skills, "Codex skill", lambda: install_codex_skills(project_root)),
(install_all or args.hooks, "Claude Code hook", lambda: install_claude_code_hooks(project_root)),
(install_all or args.gitignore, ".gitignore", lambda: ensure_gitignore(project_root)),
(
install_all or args.gitignore,
".weft/legis/.gitignore",
lambda: ensure_legis_dir_gitignore(project_root),
Comment on lines 661 to +665

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 Unignore the shipped nested gitignore

When legis install or legis install --gitignore runs in a Git repo, the preceding step adds .weft/legis/ to the root .gitignore, so Git ignores the nested .weft/legis/.gitignore created here as well (git check-ignore .weft/legis/.gitignore points at the root rule, and git add . will only add the root .gitignore). That means the new nested ignore is not tracked by normal installs, so the promised safety net disappears as soon as a consuming repo drops the root rule; the root ignore needs an exception for this file or a non-directory ignore pattern.

Useful? React with 👍 / 👎.

Comment on lines +663 to +665

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 Ship the ignore before posture-only writes secrets

With legis install --posture on a fresh repo, install_all and args.gitignore are both false, so this nested-ignore step is skipped while the later posture step creates .weft/legis/legis-posture.db and, for the age-file backend, operator.age. That leaves the newly created runtime state and encrypted operator key unignored unless the operator had already run the broader install; include the posture path here or have install_posture ship the nested ignore before writing into the directory.

Useful? React with 👍 / 👎.

),
(install_all or args.mcp, ".mcp.json", lambda: register_mcp_json(project_root, args.agent_id)),
(install_all or args.posture, "posture ledger", _do_posture),
]
Expand Down
38 changes: 38 additions & 0 deletions src/legis/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,43 @@ def check_gitignore(root: Path, *, repair: bool) -> DoctorCheck:
)


def _nested_gitignore_shipped(legis_dir: Path) -> bool:
"""True iff ``.weft/legis/.gitignore`` carries the legis-managed marker."""
nested = legis_dir / ".gitignore"
try:
return nested.is_file() and _install.LEGIS_DIR_GITIGNORE_MARKER in nested.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return False


def check_dir_gitignore(root: Path, *, repair: bool) -> DoctorCheck:
"""Check that legis's dot-dir ships its nested ``.gitignore`` (suite standard).

An absent ``.weft/legis/`` is OK — it is created lazily and holds nothing to
protect yet (mirrors :func:`check_store_dir`). Once the dir exists, the
nested ignore must be present so legis runtime state never commits even if a
consuming repo drops the root ``.weft/`` rule (filigree-4ed8152630).
"""
cid = "install.dir_gitignore"
legis_dir = _store_dir_for(root)
if _nested_gitignore_shipped(legis_dir):
return DoctorCheck(cid, "ok", repairable=True)
if repair:
# Ship it (creating .weft/legis/ if needed) so the protection exists
# before any DB is opened — robust against the dir being created lazily
# by a later check (e.g. an audit-chain DB open) in the same repair pass.
ok, msg = _install.ensure_legis_dir_gitignore(root)
if ok and _nested_gitignore_shipped(legis_dir):
return DoctorCheck(cid, "ok", fixed=True, repairable=True)
return DoctorCheck(cid, "error", message=msg, repairable=True)
if not legis_dir.is_dir():
# Created lazily; nothing to protect yet (mirrors check_store_dir).
return DoctorCheck(cid, "ok", repairable=True)
return DoctorCheck(
cid, "error", message=".weft/legis/.gitignore missing (run: legis install)", repairable=True
)


# ---------------------------------------------------------------------------
# Task 7: config & store checks
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -937,6 +974,7 @@ def collect_checks(root: Path, *, repair: bool) -> list[DoctorCheck]:
checks.append(check_skill_pack(root, ".agents", repair=repair))
checks.append(check_hook(root, repair=repair))
checks.append(check_gitignore(root, repair=repair))
checks.append(check_dir_gitignore(root, repair=repair))
checks.append(check_mcp_json(root, repair=repair))
checks.append(check_filigree_binding_scope(root))
checks.append(check_weft_toml(root))
Expand Down
94 changes: 94 additions & 0 deletions src/legis/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,100 @@ def ensure_gitignore(project_root: Path) -> tuple[bool, str]:
return True, "Created .gitignore with legis config rules"


# ---------------------------------------------------------------------------
# Nested .weft/legis/.gitignore
# ---------------------------------------------------------------------------

# Idempotency marker — the header line of the shipped nested ignore. A file
# already carrying it is left untouched; a user-authored .gitignore is appended
# to (never clobbered).
LEGIS_DIR_GITIGNORE_MARKER = "managed-by: legis (machine-written runtime state)"

# The shipped nested ignore for legis's own dot-dir. The project-root
# `ensure_gitignore` already adds a `.weft/legis/` rule; this nested file is the
# suite-standard safety net (filigree-4ed8152630): it keeps legis runtime state
# out of every commit even if a consuming repo drops that root rule, and names
# the runtime files so a reviewer sees by name what is never committed.
#
# Unlike filigree (whose filigree.db is durable, committed payload), legis is the
# sole writer of this subtree and commits NOTHING durable here — every file is
# regenerated/local or secret-shaped. Enumerate-with-glob (not a bare `*`) to
# match the sibling tools, leave the nested `.gitignore` itself tracked, and let
# a hypothetical future durable file stay committable (the safe direction).
WEFT_LEGIS_GITIGNORE = f"""\
# .weft/legis/.gitignore — {LEGIS_DIR_GITIGNORE_MARKER}
#
# legis is the sole writer of this subtree and commits NOTHING durable here. The
# project-root .gitignore ignores .weft/ by default; this nested file keeps
# legis's runtime state out of every commit even if a consuming repo drops that
# root rule (the suite standard, filigree-4ed8152630).
#
# Durable (committed when this dir is tracked): none.
# Ephemeral / secret (never committed): everything below.

# Governance / posture / pulls / checks / binding SQLite DBs (regenerated/local)
legis-*.db

# SQLite write-ahead-log sidecars and rollback journals
*.db-wal
*.db-shm
*.db-journal

# Operator-elevation surfaces: encrypted operator key + ephemeral elevation
# session metadata (both secret-shaped — never commit)
operator.age
operator_session.json

# Atomic-write staging temps — mkstemp/temp siblings orphaned in this dir on a
# failed write (legis._atomic_write_text, the posture session/head-anchor
# writers, and the operator-key writer all stage here; the operator-key temp is
# secret-shaped)
*.tmp
.operator.age.*
"""


def _ensure_nested_gitignore(target_dir: Path, body: str, label: str) -> tuple[bool, str]:
"""Idempotently ship a nested ``.gitignore`` (*body*) into *target_dir*.

A file already carrying :data:`LEGIS_DIR_GITIGNORE_MARKER` is left untouched;
a user-authored ``.gitignore`` is appended to rather than clobbered.
"""
try:
nested = project_path(target_dir, ".gitignore")
except UnsafeInstallPathError as exc:
return False, str(exc)

if nested.exists():
content = nested.read_text(encoding="utf-8")
if LEGIS_DIR_GITIGNORE_MARKER in content:
return True, f"{label} already present"
if not content.endswith("\n"):
content += "\n"
content += "\n" + body
_atomic_write_text(nested, content)
return True, f"Added legis ephemeral rules to {label}"
_atomic_write_text(nested, body)
return True, f"Created {label}"


def ensure_legis_dir_gitignore(project_root: Path) -> tuple[bool, str]:
"""Ship ``.weft/legis/.gitignore`` so legis runtime state never commits.

The project-root ``.gitignore`` ignores ``.weft/legis/`` by default (see
:func:`ensure_gitignore`); this nested file is the suite-standard safety net
(filigree-4ed8152630) for projects that drop that root rule. It excludes the
governance/posture/pulls/checks/binding SQLite DBs, their WAL/SHM/journal
sidecars, and the operator-elevation secrets (``operator.age`` /
``operator_session.json``) — legis commits nothing durable here.
"""
try:
legis_dir = ensure_project_dir(project_root, ".weft", "legis")
except UnsafeInstallPathError as exc:
return False, str(exc)
return _ensure_nested_gitignore(legis_dir, WEFT_LEGIS_GITIGNORE, ".weft/legis/.gitignore")


# ---------------------------------------------------------------------------
# .mcp.json (agent MCP server registration)
# ---------------------------------------------------------------------------
Expand Down
22 changes: 22 additions & 0 deletions tests/conformance/fixtures/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,25 @@ fixture from:
It is vendored so Legis CI can run the SEI consumer oracle without requiring the
Loomweave checkout. `tests/conformance/test_sei_oracle.py` compares this copy
against the sibling authority fixture when the checkout is present.

# legis -> warpline per-SEI attestation golden

`legis-warpline-attestation-get.golden.json` is NOT vendored from a sibling — it
is FROZEN from legis's OWN real producer wire. legis is the PRODUCER of per-SEI
governance attestations that warpline reads (an `attestation_get`-style read).

The producer wire is the legis MCP tool `attestation_get`
(`mcp.py::_tool_attestation_get` -> `legis.service.governance.read_sei_attestations`);
there is no HTTP attestation route. The golden was generated by driving that real
wire: a genuine signed `signoff_cleared` (`SignoffGate.request`/`sign_off`) AND a
genuine signed `operator_override` (`ProtectedGate.operator_override`), both keyed
on one opaque SEI, written into a real `AuditStore`, then read back via
`call_tool("attestation_get")`. Determinism comes from a `FixedClock`
(`recorded_at`) and a fresh append-only store (seqs 1/2/3); the response EXCLUDES
the HMAC signature, so the golden is HMAC-key-independent.

It is keyed on the SEI (rename-stable) — the property warpline relies on.
`tests/conformance/test_warpline_attestation_oracle.py` rechecks the live
serializer + MCP wire against it (producer-source recheck) and byte-pins it
(Layer-1, default suite). To re-freeze after an intended producer change, re-run
that real wire and update `GOLDEN_BLOB_SHA` in the oracle.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"attestations": [
{
"content_hash": "blake3:signoff-cleared",
"kind": "signoff_cleared",
"recorded_at": "2026-06-02T12:00:00+00:00",
"seq": 2,
"signoff_seq": 1
},
{
"content_hash": "blake3:operator-override",
"kind": "operator_override",
"recorded_at": "2026-06-02T12:00:00+00:00",
"seq": 3
}
],
"sei": "loomweave:eid:0000000000000000000000000000aaaa",
"status": "checked"
}
Loading
Loading