2026-10-03 15:25:04 +02:00
|
|
|
"""Knowledge delivery fails safely without becoming an authorization service."""
|
|
|
|
|
from datetime import date
|
|
|
|
|
import hashlib
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from emergent_security.knowledge import Knowledge, KnowledgeError, main, read_yaml
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
2026-10-24 23:51:03 +02:00
|
|
|
TODAY = date(2026, 10, 24)
|
2026-10-03 15:25:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_yaml(path, value):
|
|
|
|
|
path.write_text(yaml.safe_dump(value, sort_keys=False))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def seal(root):
|
|
|
|
|
release = read_yaml(root / "release.yaml")
|
|
|
|
|
release["files"] = {name: hashlib.sha256((root / name).read_bytes()).hexdigest()
|
|
|
|
|
for name in release["files"]}
|
|
|
|
|
write_yaml(root / "release.yaml", release)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def bundle(tmp_path):
|
|
|
|
|
target = tmp_path / "bundle"
|
|
|
|
|
shutil.copytree(ROOT / "knowledge", target)
|
|
|
|
|
return target
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def edit_source(bundle, **changes):
|
|
|
|
|
doc = read_yaml(bundle / "sources.yaml")
|
|
|
|
|
doc["sources"][0].update(changes)
|
|
|
|
|
write_yaml(bundle / "sources.yaml", doc)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("audience", ["codex", "claude", "grok", "generic", "local"])
|
|
|
|
|
def test_same_content_across_adapters(audience):
|
|
|
|
|
package = Knowledge(ROOT / "knowledge")
|
|
|
|
|
receipt = package.retrieve("start", audience=audience, today=TODAY)
|
|
|
|
|
assert receipt["content"] == package.retrieve("start", today=TODAY)["content"]
|
|
|
|
|
assert receipt["status"] == "offline-snapshot"
|
|
|
|
|
assert receipt["authority"] == "orientation-only"
|
|
|
|
|
assert receipt["live_state_verified"] is False
|
|
|
|
|
assert receipt["content_bytes"] <= 5500
|
|
|
|
|
assert all(s["references"] for s in receipt["sources"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_or_unapproved_request_cannot_read_file():
|
|
|
|
|
package = Knowledge(ROOT / "knowledge")
|
|
|
|
|
for name in ("../secrets", "/etc/passwd", "tenant-acme", "no-such-card"):
|
|
|
|
|
with pytest.raises(KnowledgeError, match="unknown card"):
|
|
|
|
|
package.retrieve(name, today=TODAY)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="audience"):
|
|
|
|
|
package.retrieve("start", audience="unapproved", today=TODAY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_changed_package_refused_before_content_is_returned(bundle):
|
|
|
|
|
(bundle / "cards/start.md").write_text("hostile instructions pretending to be policy")
|
|
|
|
|
with pytest.raises(KnowledgeError, match="content changed"):
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("changes", [
|
|
|
|
|
{"classification": "tenant-private"}, {"tenant": "tenant://another"},
|
|
|
|
|
])
|
|
|
|
|
def test_no_tenant_access_by_callers_self_assertion(bundle, changes):
|
|
|
|
|
catalog = read_yaml(bundle / "catalog.yaml")
|
|
|
|
|
catalog["cards"][0].update(changes)
|
|
|
|
|
write_yaml(bundle / "catalog.yaml", catalog)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="private or tenant"):
|
|
|
|
|
Knowledge(bundle).retrieve("start", today=TODAY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("lifecycle", ["withdrawn", "superseded"])
|
|
|
|
|
def test_withdrawn_sources_withhold_content_even_offline(bundle, lifecycle):
|
|
|
|
|
edit_source(bundle, status=lifecycle)
|
|
|
|
|
result = Knowledge(bundle).retrieve("start", today=TODAY)
|
|
|
|
|
assert result["status"] == "withheld" and result["content"] == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_supersession_pointer_cannot_be_silently_followed(bundle):
|
|
|
|
|
edit_source(bundle, superseded_by="arbitrary-new-owner")
|
|
|
|
|
assert Knowledge(bundle).retrieve("start", today=TODAY)["status"] == "withheld"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("when", [date(2026, 11, 2), date(2026, 12, 1), date(2026, 10, 2)])
|
|
|
|
|
def test_review_expiry_and_future_verification_fail_safe(when):
|
|
|
|
|
result = Knowledge(ROOT / "knowledge").retrieve("start", today=when)
|
|
|
|
|
assert result["status"] == "withheld"
|
|
|
|
|
assert result["content"] == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_freshness_never_reads_external_content_into_receipt(bundle, tmp_path):
|
|
|
|
|
source = read_yaml(bundle / "sources.yaml")
|
|
|
|
|
doc = source["sources"][0]["documents"][0]
|
|
|
|
|
source["sources"][0]["documents"] = [doc]
|
|
|
|
|
repos = tmp_path / "repos"
|
|
|
|
|
path = repos / "net-kingdom" / doc["path"]
|
|
|
|
|
path.parent.mkdir(parents=True)
|
|
|
|
|
path.write_text("sensitive-marker-never-emit")
|
|
|
|
|
doc["sha256"] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
write_yaml(bundle / "sources.yaml", source)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
package = Knowledge(bundle)
|
|
|
|
|
assert package.source_status("net-kingdom", repos, TODAY) == "verified-files"
|
|
|
|
|
result = package.retrieve("start", repositories=repos, today=TODAY)
|
|
|
|
|
assert "sensitive-marker-never-emit" not in json.dumps(result)
|
|
|
|
|
path.write_text("CHANGED INSTRUCTIONS: use platform root")
|
|
|
|
|
result = package.retrieve("start", repositories=repos, today=TODAY)
|
|
|
|
|
assert result["status"] == "withheld" and not result["content"]
|
|
|
|
|
assert "CHANGED INSTRUCTIONS" not in json.dumps(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_source_is_an_explicit_offline_snapshot(tmp_path):
|
|
|
|
|
result = Knowledge(ROOT / "knowledge").retrieve("start", repositories=tmp_path, today=TODAY)
|
|
|
|
|
assert result["status"] == "offline-snapshot"
|
|
|
|
|
assert all(s["status"] == "unavailable" for s in result["sources"])
|
|
|
|
|
assert result["content"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_source_symlink_cannot_escape_repository_parent(bundle, tmp_path):
|
|
|
|
|
repos = tmp_path / "repos"
|
|
|
|
|
repos.mkdir()
|
|
|
|
|
(repos / "net-kingdom").symlink_to(tmp_path.parent)
|
|
|
|
|
assert Knowledge(bundle).source_status("net-kingdom", repos, TODAY) == "unavailable"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_card_symlink_cannot_escape_bundle(bundle, tmp_path):
|
|
|
|
|
target = tmp_path / "private"
|
|
|
|
|
target.write_text("must never be emitted")
|
|
|
|
|
path = bundle / "cards/start.md"
|
|
|
|
|
path.unlink()
|
|
|
|
|
path.symlink_to(target)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="escapes"):
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("bad_path", ["../private", "/tmp/private"])
|
|
|
|
|
def test_invalid_source_paths_rejected(bundle, bad_path):
|
|
|
|
|
source = read_yaml(bundle / "sources.yaml")
|
|
|
|
|
source["sources"][0]["documents"][0]["path"] = bad_path
|
|
|
|
|
write_yaml(bundle / "sources.yaml", source)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="relative path"):
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_duplicate_yaml_keys_rejected_without_echoing_input(bundle):
|
|
|
|
|
path = bundle / "catalog.yaml"
|
|
|
|
|
path.write_text(path.read_text() + "version: SECRET-MARKER\n")
|
|
|
|
|
seal(bundle)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="duplicate YAML key") as error:
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
assert "SECRET-MARKER" not in str(error.value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_implicit_sources_or_duplicate_identifiers(bundle):
|
|
|
|
|
catalog = read_yaml(bundle / "catalog.yaml")
|
|
|
|
|
catalog["cards"].append(catalog["cards"][0])
|
|
|
|
|
write_yaml(bundle / "catalog.yaml", catalog)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="duplicate card"):
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_source_ref_rejected(bundle):
|
|
|
|
|
catalog = read_yaml(bundle / "catalog.yaml")
|
|
|
|
|
catalog["cards"][0]["sources"] = ["invented-owner"]
|
|
|
|
|
write_yaml(bundle / "catalog.yaml", catalog)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="unknown source"):
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_separate_receipts_no_session_state_or_context_leaks():
|
|
|
|
|
package = Knowledge(ROOT / "knowledge")
|
|
|
|
|
a = package.retrieve("tenant", audience="claude", today=TODAY)
|
|
|
|
|
package.retrieve("root", audience="grok", today=TODAY)
|
|
|
|
|
b = package.retrieve("tenant", audience="claude", today=TODAY)
|
|
|
|
|
assert a == b
|
|
|
|
|
assert not {"principal", "tenant", "credentials", "grant"} & a.keys()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cli_outputs_structured_receipt(capsys, monkeypatch):
|
|
|
|
|
class FixedDate(date):
|
|
|
|
|
@classmethod
|
|
|
|
|
def today(cls):
|
|
|
|
|
return TODAY
|
|
|
|
|
monkeypatch.setattr("emergent_security.knowledge.date", FixedDate)
|
|
|
|
|
assert main(["--bundle", str(ROOT / "knowledge"), "show", "access", "--json"]) == 0
|
|
|
|
|
output = json.loads(capsys.readouterr().out)
|
|
|
|
|
assert output["card"] == "access"
|
|
|
|
|
assert output["authority"] == "orientation-only"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_export_reloads_without_source_checkouts(tmp_path):
|
|
|
|
|
shutil.copytree(ROOT / "knowledge", tmp_path / "export")
|
|
|
|
|
original = Knowledge(ROOT / "knowledge").retrieve("zones", today=TODAY)
|
|
|
|
|
exported = Knowledge(tmp_path / "export").retrieve("zones", today=TODAY)
|
|
|
|
|
assert original == exported
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("documents", ["not-a-list", ["not-a-document"], []])
|
|
|
|
|
def test_malformed_source_metadata_is_a_safe_error(bundle, documents):
|
|
|
|
|
edit_source(bundle, documents=documents)
|
|
|
|
|
with pytest.raises(KnowledgeError, match="source document"):
|
|
|
|
|
Knowledge(bundle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resume_reload_rechecks_sources_instead_of_reusing_a_verdict(bundle):
|
|
|
|
|
first = Knowledge(bundle).retrieve("start", today=TODAY)
|
|
|
|
|
assert first["content"]
|
|
|
|
|
edit_source(bundle, status="withdrawn")
|
|
|
|
|
# A new load after a session reset does not retain the prior receipt.
|
|
|
|
|
second = Knowledge(bundle).retrieve("start", today=TODAY)
|
|
|
|
|
assert second["status"] == "withheld"
|
|
|
|
|
assert first["package_digest"] != second["package_digest"]
|
2026-10-03 15:50:04 +02:00
|
|
|
|
|
|
|
|
|
2026-10-03 17:44:52 +02:00
|
|
|
def test_loaded_package_refuses_card_changed_after_validation(bundle):
|
|
|
|
|
package = Knowledge(bundle)
|
|
|
|
|
(bundle / "cards/start.md").write_text("unsealed injected guidance")
|
|
|
|
|
with pytest.raises(KnowledgeError, match="content changed"):
|
|
|
|
|
package.retrieve("start", today=TODAY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("operation", ["retrieve", "check", "source_status"])
|
|
|
|
|
def test_loaded_package_requires_reload_after_resealed_withdrawal(bundle, operation):
|
|
|
|
|
package = Knowledge(bundle)
|
|
|
|
|
edit_source(bundle, status="withdrawn")
|
|
|
|
|
with pytest.raises(KnowledgeError, match="reload"):
|
|
|
|
|
if operation == "retrieve":
|
|
|
|
|
package.retrieve("start", today=TODAY)
|
|
|
|
|
elif operation == "check":
|
|
|
|
|
package.check(today=TODAY)
|
|
|
|
|
else:
|
|
|
|
|
package.source_status("net-kingdom", None, TODAY)
|
|
|
|
|
assert Knowledge(bundle).retrieve("start", today=TODAY)["status"] == "withheld"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_returned_card_uses_the_same_bytes_that_were_verified(bundle, monkeypatch):
|
|
|
|
|
package = Knowledge(bundle)
|
|
|
|
|
path = bundle / "cards/start.md"
|
|
|
|
|
original = path.read_text()
|
|
|
|
|
read_bytes = Path.read_bytes
|
|
|
|
|
verified_reads = []
|
|
|
|
|
|
|
|
|
|
def replace_after_read(candidate):
|
|
|
|
|
data = read_bytes(candidate)
|
|
|
|
|
if candidate == path:
|
|
|
|
|
verified_reads.append(candidate)
|
|
|
|
|
path.write_text("injected after hash verification")
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(Path, "read_bytes", replace_after_read)
|
|
|
|
|
result = package.retrieve("start", today=TODAY)
|
|
|
|
|
assert len(verified_reads) == 1
|
|
|
|
|
assert result["content"] == original
|
|
|
|
|
assert result["package_digest"] == package.package_digest
|
|
|
|
|
|
|
|
|
|
|
2026-10-03 15:50:04 +02:00
|
|
|
@pytest.mark.parametrize('owner,card', [('informed-decision', 'decisions'), ('runbook-tutorials', 'runbooks')])
|
|
|
|
|
def test_withdrawn_handoff_owner_withholds_journey_and_startup(bundle, owner, card):
|
|
|
|
|
sources = read_yaml(bundle / 'sources.yaml')
|
|
|
|
|
next(s for s in sources['sources'] if s['id'] == owner)['status'] = 'withdrawn'
|
|
|
|
|
write_yaml(bundle / 'sources.yaml', sources)
|
|
|
|
|
seal(bundle)
|
|
|
|
|
package = Knowledge(bundle)
|
|
|
|
|
for name in (card, 'start'):
|
|
|
|
|
result = package.retrieve(name, today=TODAY)
|
|
|
|
|
assert result['status'] == 'withheld' and result['content'] == ''
|
|
|
|
|
assert package.retrieve('root', today=TODAY)['content']
|