fix: harden runtime config helper protocol and lifecycle
This commit is contained in:
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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)}))
|
||||
|
||||
Reference in New Issue
Block a user