Skip to content
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b57eac6
test(release): require explicit legal-readiness preflight
seonghobae Aug 16, 2026
850c394
feat(release): add fail-closed legal readiness checker
seonghobae Aug 16, 2026
6761f11
test(release): exercise legal readiness evidence
seonghobae Aug 16, 2026
427dbe3
ci(release): add manual legal-readiness preflight
seonghobae Aug 16, 2026
04e3348
test(release): require CI coverage for legal preflight changes
seonghobae Aug 16, 2026
0ec9fa2
ci(release): test legal preflight changes in Runtime CI
seonghobae Aug 16, 2026
afff28d
test(release): require legal preflight on protected main
seonghobae Aug 16, 2026
ae89445
ci(release): require protected main before legal-readiness preflight
seonghobae Aug 16, 2026
e7eebeb
test(release): reject standard license symlink escape
seonghobae Aug 16, 2026
eb0905b
fix(release): reject escaped standard license symlinks
seonghobae Aug 16, 2026
7ca5fa1
test(release): reject empty license evidence
seonghobae Aug 16, 2026
5a0d373
fix(release): require nonempty license terms evidence
seonghobae Aug 16, 2026
1d3df02
test(release): fail closed on looping license symlink
seonghobae Aug 17, 2026
4a39a36
fix(release): contain malformed license filesystem evidence
seonghobae Aug 17, 2026
c45d6b2
test(release): reject absolute Cargo license-file aliases
seonghobae Aug 17, 2026
38cc838
fix(release): require manifest-relative license-file evidence
seonghobae Aug 17, 2026
d2bc460
test(release): require CI on license evidence changes
seonghobae Aug 17, 2026
73e45e6
fix(release): trigger CI for license evidence changes
seonghobae Aug 17, 2026
b86f5d8
test(session): execute invalid Content-Length and body through the re…
cursoragent Aug 18, 2026
0f07570
Merge branch 'main' into chore/release-legal-readiness-20260817
opencode-agent[bot] Aug 18, 2026
e7d2855
merge(main): reconcile legal readiness after publisher adapter
seonghobae Aug 20, 2026
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,43 @@ on:
paths:
- "Cargo.toml"
- "Cargo.lock"
- "LICENSE*"
- "COPYING*"
- "rust-toolchain.toml"
- "src/**"
- "migrations/**"
- "tests/**"
- "scripts/check_coverage.py"
- "scripts/check_release_legal_readiness.py"
- "docs/**"
- "README.md"
- "ARCHITECTURE.md"
- "AGENTS.md"
- "CLAUDE.md"
- "CHANGELOG.md"
- ".github/workflows/ci.yml"
- ".github/workflows/release-legal-readiness.yml"
push:
branches: [main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "LICENSE*"
- "COPYING*"
- "rust-toolchain.toml"
- "src/**"
- "migrations/**"
- "tests/**"
- "scripts/check_coverage.py"
- "scripts/check_release_legal_readiness.py"
- "docs/**"
- "README.md"
- "ARCHITECTURE.md"
- "AGENTS.md"
- "CLAUDE.md"
- "CHANGELOG.md"
- ".github/workflows/ci.yml"
- ".github/workflows/release-legal-readiness.yml"
workflow_dispatch:

permissions:
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/release-legal-readiness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Release legal readiness

on:
workflow_dispatch:

permissions:
contents: read

jobs:
preflight:
name: Verify explicit source-distribution license evidence
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require protected main
run: test "$GITHUB_REF" = "refs/heads/main"
- name: Checkout exact protected revision
uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Verify readiness checker contracts
run: |
python3 tests/test_release_legal_readiness.py
python3 tests/test_check_release_legal_readiness.py
- name: Evaluate explicit source-distribution license evidence
run: python3 scripts/check_release_legal_readiness.py .
142 changes: 142 additions & 0 deletions scripts/check_release_legal_readiness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Fail closed when source-distribution license evidence is incomplete.

This preflight intentionally does not choose a license, interpret legal terms, or claim that the
repository has sufficient rights to distribute any assessment content. It only checks that a human-
reviewed repository license is discoverable and that Cargo package metadata points at explicit
license terms before an operator treats a source package as release-ready.
"""

from __future__ import annotations

import json
from pathlib import Path
import sys
import tomllib
from typing import Any


LICENSE_CANDIDATES = (
"LICENSE",
"LICENSE.md",
"LICENSE.txt",
"COPYING",
"COPYING.md",
"COPYING.txt",
)


class ReleaseLegalReadinessError(ValueError):
"""Raised when repository legal-readiness inputs cannot be inspected safely."""


def _read_manifest(path: Path) -> dict[str, Any]:
"""Load Cargo.toml as a table or fail with an operator-readable error."""
try:
manifest = tomllib.loads(path.read_text(encoding="utf-8"))
except (OSError, tomllib.TOMLDecodeError) as error:
raise ReleaseLegalReadinessError(f"cannot read {path}: {error}") from error
if not isinstance(manifest, dict):
raise ReleaseLegalReadinessError("Cargo.toml root must be a table")
package = manifest.get("package")
if not isinstance(package, dict):
raise ReleaseLegalReadinessError("Cargo.toml must contain a [package] table")
return package


def _nonempty_string(value: object) -> str | None:
"""Return a stripped non-empty string, otherwise None."""
if not isinstance(value, str):
return None
stripped = value.strip()
return stripped or None


def _resolved_repo_file(root: Path, relative_path: str) -> Path | None:
"""Return non-empty repository-relative terms evidence or fail closed."""
path = Path(relative_path)
if path.is_absolute():
return None
try:
root_resolved = root.resolve()
candidate = (root / path).resolve()
except (OSError, RuntimeError):
return None
try:
candidate.relative_to(root_resolved)
except ValueError:
return None
if not candidate.is_file():
return None
try:
has_terms = bool(candidate.read_bytes().strip())
except OSError:
return None
return candidate if has_terms else None


def evaluate_repository(root: Path) -> dict[str, object]:
"""Return deterministic evidence and blockers without interpreting license sufficiency."""
root = root.resolve()
if not root.is_dir():
raise ReleaseLegalReadinessError(f"repository root is not a directory: {root}")

package = _read_manifest(root / "Cargo.toml")
standard_license_files = [
candidate
for candidate in LICENSE_CANDIDATES
if _resolved_repo_file(root, candidate) is not None
]
license_expression = _nonempty_string(package.get("license"))
license_file_value = _nonempty_string(package.get("license-file"))
declared_license_file = (
_resolved_repo_file(root, license_file_value) if license_file_value is not None else None
)

blockers: list[str] = []
if not standard_license_files and declared_license_file is None:
blockers.append(
"repository has no discoverable license file; an authorized owner must add reviewed license terms"
)
if license_expression is None and license_file_value is None:
blockers.append(
"Cargo.toml [package] has neither license nor license-file metadata"
)
if license_file_value is not None and declared_license_file is None:
blockers.append(
"Cargo.toml license-file is missing, empty, absolute, not a regular file, unreadable, or escapes the repository root"
)

evidence: dict[str, object] = {
"ready": not blockers,
"standard_license_files": sorted(standard_license_files),
"cargo_license_expression_declared": license_expression is not None,
"cargo_license_file": license_file_value,
"cargo_license_file_resolved": (
str(declared_license_file.relative_to(root)) if declared_license_file is not None else None
),
"blockers": blockers,
"limitations": [
"presence checks do not establish copyright ownership, instrument rights, compatibility, or legal sufficiency",
"this checker never selects or interprets license terms",
],
}
return evidence


def main(argv: list[str]) -> int:
"""Print machine-readable evidence and return nonzero while release blockers remain."""
if len(argv) != 2:
print("usage: check_release_legal_readiness.py <repository-root>", file=sys.stderr)
return 2
try:
evidence = evaluate_repository(Path(argv[1]))
except ReleaseLegalReadinessError as error:
print(json.dumps({"ready": False, "error": str(error)}, sort_keys=True), file=sys.stderr)
return 2
print(json.dumps(evidence, indent=2, sort_keys=True))
return 0 if evidence["ready"] else 1


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
12 changes: 12 additions & 0 deletions src/session_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,18 @@ mod tests {
),
std::io::ErrorKind::InvalidData
);
assert_eq!(
framing_kind(
b"POST /v1/sessions HTTP/1.1\r\nIdempotency-Key: ses_badlen\r\nContent-Length: no\r\n\r\n{}"
),
std::io::ErrorKind::InvalidData
);
assert_eq!(
framing_kind(
b"POST /v1/sessions HTTP/1.1\r\nIdempotency-Key: ses_bad_body\r\nContent-Length: 2\r\n\r\n\xff\xfe"
),
std::io::ErrorKind::InvalidData
);
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions tests/session_http_framing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ fn framing_error(request: &[u8]) -> std::io::ErrorKind {
let payload = request.to_vec();
let client = std::thread::spawn(move || {
let mut stream = TcpStream::connect(address).unwrap();
stream.write_all(&payload).unwrap();
stream.shutdown(Shutdown::Write).unwrap();
let _ = stream.write_all(&payload);
let _ = stream.shutdown(Shutdown::Write);
let mut response = String::new();
let _ = stream.read_to_string(&mut response);
});
Expand Down
142 changes: 142 additions & 0 deletions tests/test_check_release_legal_readiness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Unit tests for source-distribution legal readiness evidence."""

from pathlib import Path
import sys
import tempfile
import unittest


sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from check_release_legal_readiness import ( # noqa: E402
ReleaseLegalReadinessError,
evaluate_repository,
)


class ReleaseLegalReadinessTests(unittest.TestCase):
"""Prove that evidence is explicit and blockers fail closed."""

def fixture_root(self, directory: str, package_lines: list[str]) -> Path:
"""Create one isolated Cargo package fixture."""
root = Path(directory)
(root / "Cargo.toml").write_text(
"\n".join(["[package]", 'name = "fixture"', 'version = "0.1.0"', *package_lines])
+ "\n",
encoding="utf-8",
)
return root

def test_missing_license_file_and_manifest_metadata_are_blockers(self) -> None:
"""An owner decision cannot be replaced by an optimistic default."""
with tempfile.TemporaryDirectory() as directory:
evidence = evaluate_repository(self.fixture_root(directory, []))
self.assertFalse(evidence["ready"])
blockers = evidence["blockers"]
self.assertIsInstance(blockers, list)
self.assertEqual(len(blockers), 2)
self.assertIn("no discoverable license file", blockers[0])
self.assertIn("neither license nor license-file", blockers[1])

def test_reviewed_root_file_plus_manifest_expression_is_ready_for_presence_gate(self) -> None:
"""Presence readiness succeeds without claiming the chosen terms are legally sufficient."""
with tempfile.TemporaryDirectory() as directory:
root = self.fixture_root(directory, ['license = "LicenseRef-Reviewed-Terms"'])
(root / "LICENSE").write_text("reviewed fixture terms\n", encoding="utf-8")
evidence = evaluate_repository(root)
self.assertTrue(evidence["ready"])
self.assertEqual(evidence["standard_license_files"], ["LICENSE"])
self.assertTrue(evidence["cargo_license_expression_declared"])
self.assertTrue(evidence["limitations"])

def test_empty_or_whitespace_license_files_are_not_terms_evidence(self) -> None:
"""An empty conventional or declared file cannot satisfy the source-terms presence gate."""
with tempfile.TemporaryDirectory() as directory:
root = self.fixture_root(directory, ['license = "LicenseRef-Reviewed-Terms"'])
(root / "LICENSE").write_text(" \n\t", encoding="utf-8")
evidence = evaluate_repository(root)
self.assertFalse(evidence["ready"])
self.assertEqual(evidence["standard_license_files"], [])
self.assertIn("no discoverable license file", evidence["blockers"][0])

with tempfile.TemporaryDirectory() as directory:
root = self.fixture_root(directory, ['license-file = "legal/source-license.txt"'])
(root / "legal").mkdir()
(root / "legal/source-license.txt").write_bytes(b"")
evidence = evaluate_repository(root)
self.assertFalse(evidence["ready"])
self.assertIsNone(evidence["cargo_license_file_resolved"])
self.assertIn("license-file is missing", evidence["blockers"][-1])

def test_declared_license_file_must_exist_inside_repository(self) -> None:
"""Missing or path-escaping license-file metadata fails closed."""
with tempfile.TemporaryDirectory() as directory:
root = self.fixture_root(directory, ['license-file = "../LICENSE"'])
evidence = evaluate_repository(root)
self.assertFalse(evidence["ready"])
self.assertIn("escapes the repository root", evidence["blockers"][-1])

def test_declared_license_file_must_be_manifest_relative_not_absolute(self) -> None:
"""Cargo license-file metadata is relative to Cargo.toml and cannot be an absolute alias."""
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
absolute_license = root / "legal" / "source-license.txt"
absolute_license.parent.mkdir()
absolute_license.write_text("reviewed fixture terms\n", encoding="utf-8")
root = self.fixture_root(
directory,
[f'license-file = "{absolute_license.as_posix()}"'],
)

evidence = evaluate_repository(root)

self.assertFalse(evidence["ready"])
self.assertIsNone(evidence["cargo_license_file_resolved"])
self.assertIn("license-file is missing", evidence["blockers"][-1])

def test_standard_license_symlink_cannot_escape_repository(self) -> None:
"""A conventional filename is not evidence when its resolved file is outside the source tree."""
with tempfile.TemporaryDirectory() as directory, tempfile.TemporaryDirectory() as outside:
root = self.fixture_root(directory, ['license = "LicenseRef-Reviewed-Terms"'])
outside_license = Path(outside) / "LICENSE"
outside_license.write_text("terms outside the repository\n", encoding="utf-8")
(root / "LICENSE").symlink_to(outside_license)

evidence = evaluate_repository(root)

self.assertFalse(evidence["ready"])
self.assertEqual(evidence["standard_license_files"], [])
self.assertIn("no discoverable license file", evidence["blockers"][0])

def test_symlink_loop_is_a_blocker_not_an_unhandled_preflight_crash(self) -> None:
"""Malformed filesystem evidence must fail closed instead of escaping machine-readable output."""
with tempfile.TemporaryDirectory() as directory:
root = self.fixture_root(directory, ['license = "LicenseRef-Reviewed-Terms"'])
(root / "LICENSE").symlink_to("LICENSE")

evidence = evaluate_repository(root)

self.assertFalse(evidence["ready"])
self.assertEqual(evidence["standard_license_files"], [])
self.assertIn("no discoverable license file", evidence["blockers"][0])

def test_declared_nonstandard_license_file_can_supply_explicit_file_evidence(self) -> None:
"""Cargo's reviewed license-file is acceptable even when its name is nonstandard."""
with tempfile.TemporaryDirectory() as directory:
root = self.fixture_root(directory, ['license-file = "legal/source-license.txt"'])
(root / "legal").mkdir()
(root / "legal/source-license.txt").write_text("reviewed fixture terms\n", encoding="utf-8")
evidence = evaluate_repository(root)
self.assertTrue(evidence["ready"])
self.assertEqual(evidence["cargo_license_file_resolved"], "legal/source-license.txt")

def test_malformed_manifest_is_an_inspection_error_not_ready_evidence(self) -> None:
"""Malformed release metadata cannot be treated as an ordinary missing field."""
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "Cargo.toml").write_text("not = [valid", encoding="utf-8")
with self.assertRaises(ReleaseLegalReadinessError):
evaluate_repository(root)


if __name__ == "__main__":
unittest.main()
Loading
Loading