fix: harden runtime config helper protocol and lifecycle

This commit is contained in:
2026-08-11 13:03:37 +02:00
parent ec92f7f994
commit 11cc8628cf
4 changed files with 290 additions and 34 deletions
@@ -1,10 +1,15 @@
"""Focused unit coverage for the privileged runtime publication seam."""
import io
import json
import os
import subprocess
from pathlib import Path
import pytest
from tht import runtime_config_lease_io as lease_io
from tht.config import ConfigError, _read_runtime_config_source
def _manifest() -> dict:
@@ -82,3 +87,99 @@ def test_retry_reasserts_parent_durability_before_success(tmp_path, monkeypatch,
events.clear()
lease_io.publish(inp)
assert events.index("config-parent") < events.index("manifest-parent")
def test_protocol_success_shapes_and_version_rejection(monkeypatch):
monkeypatch.setattr(lease_io, "binding", lambda _inp: {
"workspace_id": "abc", "config_fingerprint": "f", "input_fingerprint": "i",
})
monkeypatch.setattr(lease_io.sys, "stdin", io.StringIO(
'{"protocol_version":1,"action":"binding","config_hex":""}'
))
success = io.StringIO()
monkeypatch.setattr(lease_io.sys, "stdout", success)
lease_io.main()
assert set(json.loads(success.getvalue())) == {
"protocol_version", "kind", "workspace_id", "config_fingerprint", "input_fingerprint",
}
# An old/new protocol mismatch must be rejected before action dispatch.
monkeypatch.setattr(lease_io.sys, "stdin", io.StringIO(
'{"protocol_version":999,"action":"binding","config_hex":""}'
))
failure = io.StringIO()
monkeypatch.setattr(lease_io.sys, "stdout", failure)
with pytest.raises(SystemExit):
lease_io.main()
assert json.loads(failure.getvalue())["error"] == "unsupported runtime config protocol"
def test_secure_runtime_reader_binds_path_manifest_and_ignores_legacy_fd(monkeypatch, tmp_path):
revision = "a" * 40
inp = {
"data_root": str(tmp_path / "data"), "workspace_id": "abc",
"workspace_revision": revision, "config_hex": b"runtime: true\n".hex(),
"manifest_base": {
"workspace_id": "abc", "workspace_revision": revision,
"descriptor_git_blob": "b" * 40, "descriptor_sha256": "c" * 64,
"descriptor_dev": "1", "descriptor_ino": "2",
"config_dwh_binding": {"workspace_id": "abc", "config_fingerprint": "e", "input_fingerprint": "f"},
},
}
result = lease_io.publish(inp)
path = Path(result["path"])
monkeypatch.setenv("THT_RUNTIME_CONFIG_MANIFEST_SHA256", result["manifest_sha256"])
monkeypatch.setenv("THT_CONFIG_MANIFEST_FD", "999")
monkeypatch.setenv("THT_CONFIG_MANIFEST_SHA256", "0" * 64)
source, manifest = _read_runtime_config_source(path)
assert source == "runtime: true\n"
assert manifest is not None and manifest["workspace_id"] == "abc"
monkeypatch.setenv("THT_RUNTIME_CONFIG_MANIFEST_SHA256", "0" * 64)
with pytest.raises(ConfigError):
_read_runtime_config_source(path)
with pytest.raises(ConfigError):
_read_runtime_config_source(path.with_name("not-canonical.yaml"))
@pytest.mark.parametrize("object_kind", ["tree", "blob", "tag"])
def test_verified_snapshot_rejects_non_commit_object(tmp_path, object_kind):
repo = tmp_path / "repo"
(repo / "workspaces").mkdir(parents=True)
subprocess.run(["git", "init", "--initial-branch=main"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "config", "user.name", "Fixture"], cwd=repo, check=True)
subprocess.run(["git", "config", "user.email", "fixture@example.invalid"], cwd=repo, check=True)
descriptor = "workspace:\n schema_version: 3\n id: abc\n"
(repo / "workspaces" / "abc.yaml").write_text(descriptor)
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(["git", "commit", "-m", "fixture"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
if object_kind == "tree":
revision = subprocess.check_output(["git", "rev-parse", "HEAD^{tree}"], cwd=repo, text=True).strip()
elif object_kind == "blob":
revision = subprocess.check_output(["git", "rev-parse", "HEAD:workspaces/abc.yaml"], cwd=repo, text=True).strip()
else:
subprocess.run(["git", "tag", "-a", "v1", "-m", "tag"], cwd=repo, check=True)
revision = subprocess.check_output(["git", "rev-parse", "refs/tags/v1^{tag}"], cwd=repo, text=True).strip()
snapshots = tmp_path / "snapshots" / revision
snapshots.mkdir(parents=True, mode=0o700)
(tmp_path / "snapshots").chmod(0o700)
files = {"abc.yaml": descriptor, "abc.env.example": "# fixture\n", "abc.md": "# fixture\n"}
for name, content in files.items():
target = snapshots / name
target.write_text(content)
target.chmod(0o400)
snapshot = {
"head": revision,
"revisions": [{"id": "abc", "commit": revision, "blob": "b" * 40,
"snapshotPath": f"{tmp_path / 'snapshots'}/{revision}/abc.yaml"}],
"files": {name: __import__("hashlib").sha256(content.encode()).hexdigest() for name, content in files.items()},
}
manifest = snapshots / "snapshot.json"
manifest.write_text(json.dumps(snapshot))
manifest.chmod(0o400)
with pytest.raises(RuntimeError, match="exact commit|unavailable"):
lease_io.verified_snapshot({
"snapshots_root": str(tmp_path / "snapshots"), "repository_root": str(repo),
"workspace_revision": revision, "workspace_id": "abc",
})
+11 -1
View File
@@ -492,6 +492,10 @@ def publish(inp: dict) -> dict:
# failed after the no-replace publication on an earlier invocation.
publication_fsync(mandir, "manifest-parent")
return {
"protocol_version": 1,
"kind": "publication",
"workspace_id": wid,
"workspace_revision": rev,
"path": f"{canonical}/sessions/{wid}/preprocessing/runtime-config/{name}",
"manifestPath": f"{canonical}/sessions/{wid}/preprocessing/runtime-config-manifests/{mname}",
"manifest": mb.decode(),
@@ -630,6 +634,8 @@ def verified_snapshot(inp: dict) -> dict:
if blob != record.get("blob"):
fail("workspace Git descriptor identity mismatch")
return {
"protocol_version": 1,
"kind": "verified_snapshot",
"workspace_id": wid,
"workspace_revision": rev,
"source": source.decode(),
@@ -663,7 +669,10 @@ def binding(inp: dict) -> dict:
stream.write(raw)
path = Path(stream.name)
try:
return config_dwh_binding(load_config(path))
result = config_dwh_binding(load_config(path))
if not isinstance(result, dict) or set(result) != {"workspace_id", "config_fingerprint", "input_fingerprint"}:
fail("runtime config binding returned malformed output")
return result
finally:
try:
path.unlink()
@@ -690,6 +699,7 @@ def main() -> None:
result = verified_snapshot(inp)
else:
result = binding(inp)
result = {"protocol_version": 1, "kind": "binding", **result}
print(json.dumps(result))
except Exception as e: # noqa: BLE001
print(json.dumps({"error": str(e)}))