From afe52c0cb8f827b4d53029c592f5e8b11e2072c8 Mon Sep 17 00:00:00 2001 From: kangkangzi2025 Date: Mon, 6 Jul 2026 22:41:44 +0800 Subject: [PATCH 01/22] codex security --- src/rath/backend/persistence/paths.py | 6 +- src/rath/flow/memory_inject.py | 8 +- src/rath/memory/adapters/local.py | 191 +++++++++++++++++- src/rath/memory/persistence/paths.py | 3 +- src/rath/session/persistence/paths.py | 7 +- src/rath/utils/ids.py | 23 +++ .../backends/persistence/test_registry_gc.py | 11 + tests/flow/test_agent_forward_memory.py | 14 +- tests/flow/test_memory_inject.py | 3 +- tests/memory/local_backend/conftest.py | 16 +- .../local_backend/test_local_memory_commit.py | 18 ++ .../test_local_memory_resource.py | 43 +++- tests/memory/persistence/test_persistence.py | 11 + tests/session/persistence/test_loader_gc.py | 12 ++ 14 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 src/rath/utils/ids.py diff --git a/src/rath/backend/persistence/paths.py b/src/rath/backend/persistence/paths.py index c11f4f9..f4a64a2 100644 --- a/src/rath/backend/persistence/paths.py +++ b/src/rath/backend/persistence/paths.py @@ -15,6 +15,7 @@ from uuid import UUID from rath.config.paths import resolve_config_dir +from rath.utils.ids import coerce_uuid_str __all__ = [ "SANDBOXES_DIR_NAME", @@ -48,7 +49,7 @@ def local_root() -> Path: def local_sandbox_dir(sandbox_id: UUID | str) -> Path: """``/`` — the stable working_dir for a Local sandbox.""" - return local_root() / str(sandbox_id) + return local_root() / coerce_uuid_str(sandbox_id, field="sandbox_id") def opensandbox_root() -> Path: @@ -58,7 +59,8 @@ def opensandbox_root() -> Path: def opensandbox_index_path(sandbox_id: UUID | str) -> Path: """``/.json`` — registry entry for a remote sandbox.""" - return opensandbox_root() / f"{sandbox_id}{OPENSANDBOX_INDEX_SUFFIX}" + sid = coerce_uuid_str(sandbox_id, field="sandbox_id") + return opensandbox_root() / f"{sid}{OPENSANDBOX_INDEX_SUFFIX}" def ensure_local_root() -> Path: diff --git a/src/rath/flow/memory_inject.py b/src/rath/flow/memory_inject.py index 291cc19..4717b23 100644 --- a/src/rath/flow/memory_inject.py +++ b/src/rath/flow/memory_inject.py @@ -3,8 +3,7 @@ A policy reads a :class:`~rath.session.session.Session` and a :class:`~rath.memory.abc.MemoryStore`, then returns a tuple of :class:`~rath.session.chunk.ChunkRow` to prepend to the next loop turn -(typically :attr:`ChunkKind.SYSTEM` notes that summarize relevant -recalled memories). +as untrusted user-context notes that summarize relevant recalled memories. The injection step must NEVER raise into the session loop — on store errors or a closed store, return an empty tuple and log a warning so @@ -101,5 +100,6 @@ def _last_user_message(session: Session) -> str | None: def _hit_to_chunk(hit: MemoryHit) -> ChunkRow: snippet = (hit.snippet or "").strip() - body = f"[memory:{hit.uri}] {snippet}" if snippet else f"[memory:{hit.uri}]" - return ChunkRow(kind=ChunkKind.SYSTEM, payload={"content": body}) + prefix = f"[untrusted memory:{hit.uri}]" + body = f"{prefix} {snippet}" if snippet else prefix + return ChunkRow(kind=ChunkKind.USER, payload={"content": body}) diff --git a/src/rath/memory/adapters/local.py b/src/rath/memory/adapters/local.py index c76602c..19d3014 100644 --- a/src/rath/memory/adapters/local.py +++ b/src/rath/memory/adapters/local.py @@ -13,10 +13,12 @@ from __future__ import annotations import hashlib +import ipaddress import json import logging import math import re +import socket import urllib.error import urllib.parse import urllib.request @@ -77,6 +79,8 @@ _VEC_SUFFIX = ".vec" _META_SUFFIX = ".meta.json" _HIDDEN_SUFFIXES: frozenset[str] = frozenset({_VEC_SUFFIX, _META_SUFFIX}) +_DEFAULT_RESOURCE_MAX_BYTES = 10 * 1024 * 1024 +_SAFE_SEGMENT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") _CAPABILITIES = MemoryCapabilities( @@ -119,6 +123,14 @@ class _LocalHandle: chat_init_failed: bool = field(default=False) +@dataclass(frozen=True, slots=True) +class _ResourcePolicy: + local_roots: tuple[Path, ...] + allowed_http_hosts: frozenset[str] + allow_private_hosts: bool + max_bytes: int + + @register("local") class LocalMemoryBackend(MemoryBackend): """Filesystem-backed memory backend, default for ``pip install openrath``.""" @@ -307,13 +319,22 @@ def _dispatch_resource( if isinstance(target_path, MemoryExecutionFailure): return target_path + policy = _resource_policy(bound.options) try: - raw_bytes, original_name, source_label = _fetch_resource(op.source) + raw_bytes, original_name, source_label = _fetch_resource( + op.source, + policy=policy, + ) except FileNotFoundError as exc: return MemoryExecutionFailure( kind="not_found", message=f"resource source not found: {exc}", ) + except _ResourceAccessDenied as exc: + return MemoryExecutionFailure( + kind="unauthorized", + message=f"resource source not allowed: {exc}", + ) except _ResourceFetchError as exc: return MemoryExecutionFailure( kind="transport", @@ -363,15 +384,26 @@ def _dispatch_commit( kind="invalid_uri", message="MemoryOpCommit requires non-empty session_id", ) + session_id = _safe_storage_segment(op.session_id, field="session_id") + if isinstance(session_id, MemoryExecutionFailure): + return session_id # Archive messages.json under session//commits//. stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%f") - commit_root = bound.path / "session" / op.session_id / "commits" / stamp + commit_root = _contained_child( + bound.path, + "session", + session_id, + "commits", + stamp, + ) + if isinstance(commit_root, MemoryExecutionFailure): + return commit_root commit_root.mkdir(parents=True, exist_ok=True) archive_path = commit_root / "messages.json" normalized = [_normalize_message(m) for m in op.messages] atomic_write_json(archive_path, normalized) archived_uri = ( - f"{MEMORY_URI_PREFIX}session/{op.session_id}/commits/{stamp}/messages.json" + f"{MEMORY_URI_PREFIX}session/{session_id}/commits/{stamp}/messages.json" ) if not op.wait: @@ -979,11 +1011,63 @@ class _ResourceFetchError(Exception): """Wraps transport errors when fetching a remote resource.""" -def _fetch_resource(source: str) -> tuple[bytes, str, str]: +class _ResourceAccessDenied(Exception): + """Raised when a resource source violates the configured policy.""" + + +class _NoRedirect(urllib.request.HTTPRedirectHandler): + """Reject redirects so each fetched URL is policy-checked directly.""" + + def redirect_request(self, *args: Any, **kwargs: Any) -> None: # noqa: D401 + return None + + +_NO_REDIRECT_OPENER = urllib.request.build_opener(_NoRedirect) + + +def _resource_policy(options: dict[str, Any]) -> _ResourcePolicy: + roots = tuple( + Path(p).expanduser().resolve(strict=False) + for p in _option_strings(options.get("resource_import_roots")) + ) + hosts = frozenset( + h.lower() for h in _option_strings(options.get("resource_allowed_http_hosts")) + ) + raw_max = options.get("resource_max_bytes", _DEFAULT_RESOURCE_MAX_BYTES) + try: + max_bytes = int(raw_max) + except (TypeError, ValueError): + max_bytes = _DEFAULT_RESOURCE_MAX_BYTES + return _ResourcePolicy( + local_roots=roots, + allowed_http_hosts=hosts, + allow_private_hosts=bool(options.get("resource_allow_private_hosts", False)), + max_bytes=max(1, max_bytes), + ) + + +def _option_strings(raw: Any) -> tuple[str, ...]: + if raw is None: + return () + if isinstance(raw, str): + return (raw,) + try: + return tuple(str(x) for x in raw) + except TypeError: + return (str(raw),) + + +def _fetch_resource( + source: str, + *, + policy: _ResourcePolicy, +) -> tuple[bytes, str, str]: """Resolve ``source`` to ``(bytes, original_name, source_label)``. - ``source`` can be a local filesystem path, a ``file://`` URI, or an - ``http(s)://`` URL. Anything else is treated as a local path. + Local paths and HTTP(S) URLs are disabled by default. Callers must opt in + with ``resource_import_roots`` or ``resource_allowed_http_hosts`` in the + store options so untrusted resource ingest cannot read host files or SSRF + internal services. """ parsed = urllib.parse.urlparse(source) scheme = parsed.scheme.lower() @@ -991,11 +1075,21 @@ def _fetch_resource(source: str) -> tuple[bytes, str, str]: if len(scheme) == 1 and scheme.isalpha(): scheme = "" if scheme in ("http", "https"): + _validate_resource_url(parsed, policy=policy) try: - with urllib.request.urlopen(source, timeout=30) as resp: # noqa: S310 - data = resp.read() + with _NO_REDIRECT_OPENER.open(source, timeout=30) as resp: + raw_len = resp.headers.get("Content-Length") + if raw_len is not None and int(raw_len) > policy.max_bytes: + raise _ResourceFetchError( + f"resource exceeds {policy.max_bytes} bytes", + ) + data = resp.read(policy.max_bytes + 1) except urllib.error.URLError as exc: raise _ResourceFetchError(str(exc)) from exc + except ValueError as exc: + raise _ResourceFetchError(str(exc)) from exc + if len(data) > policy.max_bytes: + raise _ResourceFetchError(f"resource exceeds {policy.max_bytes} bytes") name = Path(parsed.path).name or "resource" return data, name, source if scheme == "file": @@ -1004,11 +1098,92 @@ def _fetch_resource(source: str) -> tuple[bytes, str, str]: local = Path(source) else: raise _ResourceFetchError(f"unsupported scheme: {scheme!r}") + local = local.expanduser().resolve(strict=False) + _validate_local_resource_path(local, policy=policy) if not local.is_file(): raise FileNotFoundError(str(local)) + size = local.stat().st_size + if size > policy.max_bytes: + raise _ResourceFetchError(f"resource exceeds {policy.max_bytes} bytes") return local.read_bytes(), local.name, str(local) +def _validate_local_resource_path(local: Path, *, policy: _ResourcePolicy) -> None: + if not policy.local_roots: + raise _ResourceAccessDenied("local paths require resource_import_roots") + for root in policy.local_roots: + try: + local.relative_to(root) + except ValueError: + continue + return + raise _ResourceAccessDenied(f"{local} is outside resource_import_roots") + + +def _validate_resource_url( + parsed: urllib.parse.ParseResult, + *, + policy: _ResourcePolicy, +) -> None: + host = (parsed.hostname or "").lower() + if not host: + raise _ResourceAccessDenied("HTTP(S) resource URL requires a host") + if host not in policy.allowed_http_hosts: + raise _ResourceAccessDenied("HTTP(S) host is not allowed") + try: + infos = socket.getaddrinfo( + host, + parsed.port or (443 if parsed.scheme == "https" else 80), + type=socket.SOCK_STREAM, + ) + except OSError as exc: + raise _ResourceFetchError(str(exc)) from exc + for info in infos: + address = info[4][0] + try: + ip = ipaddress.ip_address(address) + except ValueError as exc: + raise _ResourceAccessDenied(f"cannot validate address {address!r}") from exc + if _is_restricted_address(ip) and not policy.allow_private_hosts: + raise _ResourceAccessDenied("HTTP(S) host resolves to a private address") + + +def _is_restricted_address(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool: + return ( + ip.is_loopback + or ip.is_private + or ip.is_link_local + or ip.is_multicast + or ip.is_reserved + or ip.is_unspecified + ) + + +def _safe_storage_segment(value: str, *, field: str) -> str | MemoryExecutionFailure: + text = str(value) + if not text or text in (".", "..") or not _SAFE_SEGMENT_RE.fullmatch(text): + return MemoryExecutionFailure( + kind="invalid_uri", + message=f"{field} must be a single safe path segment", + ) + return text + + +def _contained_child( + root: Path, + *parts: str, +) -> Path | MemoryExecutionFailure: + try: + resolved = root.joinpath(*parts).resolve(strict=False) + resolved.relative_to(root.resolve(strict=False)) + except (OSError, ValueError) as exc: + return MemoryExecutionFailure( + kind="invalid_uri", + message=f"path escapes store root: {exc}", + ) + return resolved + + def _walk_tree( dir_path: Path, uri_base: str, diff --git a/src/rath/memory/persistence/paths.py b/src/rath/memory/persistence/paths.py index 3545681..5ae2d2f 100644 --- a/src/rath/memory/persistence/paths.py +++ b/src/rath/memory/persistence/paths.py @@ -15,6 +15,7 @@ from uuid import UUID from rath.config.paths import resolve_config_dir +from rath.utils.ids import coerce_uuid_str __all__ = [ "MEMORY_DIR_NAME", @@ -41,7 +42,7 @@ def local_memory_root() -> Path: def local_store_dir(store_id: UUID | str) -> Path: """``/`` — the per-store directory.""" - return local_memory_root() / str(store_id) + return local_memory_root() / coerce_uuid_str(store_id, field="store_id") def ensure_local_memory_root() -> Path: diff --git a/src/rath/session/persistence/paths.py b/src/rath/session/persistence/paths.py index 1a15eb9..0ce9fa0 100644 --- a/src/rath/session/persistence/paths.py +++ b/src/rath/session/persistence/paths.py @@ -14,6 +14,7 @@ from uuid import UUID from rath.config.paths import resolve_config_dir +from rath.utils.ids import coerce_uuid_str __all__ = [ "SESSIONS_DIR_NAME", @@ -41,7 +42,8 @@ def session_file(session_id: UUID | str) -> Path: Accepts either a :class:`uuid.UUID` or a string; both are normalized via ``str(id)`` so callers don't have to think about it. """ - return sessions_dir() / f"{session_id}{SESSION_FILE_SUFFIX}" + sid = coerce_uuid_str(session_id, field="session_id") + return sessions_dir() / f"{sid}{SESSION_FILE_SUFFIX}" def session_partial_file(session_id: UUID | str) -> Path: @@ -53,7 +55,8 @@ def session_partial_file(session_id: UUID | str) -> Path: writing process crashed mid-session (or that the runtime drain timed out and abandoned the writer). """ - return sessions_dir() / f"{session_id}{SESSION_PARTIAL_SUFFIX}" + sid = coerce_uuid_str(session_id, field="session_id") + return sessions_dir() / f"{sid}{SESSION_PARTIAL_SUFFIX}" def ensure_sessions_dir() -> Path: diff --git a/src/rath/utils/ids.py b/src/rath/utils/ids.py new file mode 100644 index 0000000..48500f7 --- /dev/null +++ b/src/rath/utils/ids.py @@ -0,0 +1,23 @@ +"""Identifier normalization helpers for filesystem-backed persistence.""" + +from __future__ import annotations + +from uuid import UUID + +__all__ = ["coerce_uuid_str"] + + +def coerce_uuid_str(value: UUID | str, *, field: str = "id") -> str: + """Return ``value`` as a canonical UUID string or raise ``ValueError``. + + Persistence identifiers are used as path components. Accepting arbitrary + strings here would make path traversal possible, so string inputs must be + parseable UUIDs before they can reach filesystem helpers. + """ + + if isinstance(value, UUID): + return str(value) + try: + return str(UUID(str(value))) + except ValueError as exc: + raise ValueError(f"{field} must be a UUID") from exc diff --git a/tests/backends/persistence/test_registry_gc.py b/tests/backends/persistence/test_registry_gc.py index 812d1d6..5649099 100644 --- a/tests/backends/persistence/test_registry_gc.py +++ b/tests/backends/persistence/test_registry_gc.py @@ -56,6 +56,17 @@ def test_ensure_local_idempotent(_isolate_openrath_home: Path) -> None: assert first.is_dir() +@pytest.mark.parametrize("bad_id", ["../escape", "/tmp/escape", "not-a-uuid"]) +def test_sandbox_paths_reject_path_like_ids( + _isolate_openrath_home: Path, + bad_id: str, +) -> None: + with pytest.raises(ValueError, match="sandbox_id must be a UUID"): + local_sandbox_dir(bad_id) + with pytest.raises(ValueError, match="sandbox_id must be a UUID"): + opensandbox_index_path(bad_id) + + def test_list_local_enumerates_uuid_dirs(_isolate_openrath_home: Path) -> None: reg = PersistentSandboxRegistry() ids = {reg.alloc_local_id() for _ in range(3)} diff --git a/tests/flow/test_agent_forward_memory.py b/tests/flow/test_agent_forward_memory.py index 5c7585a..fb6dfda 100644 --- a/tests/flow/test_agent_forward_memory.py +++ b/tests/flow/test_agent_forward_memory.py @@ -119,7 +119,7 @@ def test_forward_without_memory_is_unchanged() -> None: assert kinds[-1] == ChunkKind.ASSISTANT -def test_forward_prepends_injected_system_chunks() -> None: +def test_forward_prepends_injected_user_context_chunks() -> None: backend = _FakeBackend( find_hits=( MemoryHit( @@ -146,13 +146,19 @@ def test_forward_prepends_injected_system_chunks() -> None: # Exactly one MemoryOpFind dispatched, no commit. assert any(isinstance(op, MemoryOpFind) for op in backend.ops_seen) assert not any(isinstance(op, MemoryOpCommit) for op in backend.ops_seen) - # The injected snippet should appear as a system chunk in the output. - sys_bodies = "\n".join( + # The injected snippet should appear as untrusted user-context, not SYSTEM. + user_bodies = "\n".join( str(r.payload.get("content", "")) for r in out.chunk_table.rows + if r.kind == ChunkKind.USER + ) + assert "[untrusted memory:" in user_bodies + assert "loves dark mode" in user_bodies + assert all( + "loves dark mode" not in str(r.payload.get("content", "")) + for r in out.chunk_table.rows if r.kind == ChunkKind.SYSTEM ) - assert "loves dark mode" in sys_bodies def test_forward_with_commit_on_forward_dispatches_one_commit() -> None: diff --git a/tests/flow/test_memory_inject.py b/tests/flow/test_memory_inject.py index 8a4b796..41983cc 100644 --- a/tests/flow/test_memory_inject.py +++ b/tests/flow/test_memory_inject.py @@ -101,9 +101,10 @@ def test_default_recall_emits_one_chunk_per_hit() -> None: assert isinstance(chunks, tuple) assert len(chunks) == 2 for chunk in chunks: - assert chunk.kind == ChunkKind.SYSTEM + assert chunk.kind == ChunkKind.USER assert "content" in chunk.payload bodies = "\n".join(c.payload["content"] for c in chunks) + assert "[untrusted memory:" in bodies assert "dark mode preferred" in bodies assert "GMT+8 timezone" in bodies # The dispatch should carry the configured target_uri + top_k diff --git a/tests/memory/local_backend/conftest.py b/tests/memory/local_backend/conftest.py index b9d0dd5..1e29862 100644 --- a/tests/memory/local_backend/conftest.py +++ b/tests/memory/local_backend/conftest.py @@ -7,7 +7,7 @@ import pytest -from rath.memory import MemoryStore +from rath.memory import MemoryStore, MemoryStoreSpec from rath.memory.adapters.local import LocalMemoryBackend @@ -30,8 +30,18 @@ def backend() -> Iterator[LocalMemoryBackend]: @pytest.fixture -def store(backend: LocalMemoryBackend) -> Iterator[MemoryStore]: - s = backend.open() +def store( + backend: LocalMemoryBackend, + tmp_path: Path, +) -> Iterator[MemoryStore]: + spec = MemoryStoreSpec( + options={ + "resource_import_roots": [str(tmp_path)], + "resource_allowed_http_hosts": ["127.0.0.1"], + "resource_allow_private_hosts": True, + }, + ) + s = backend.open(spec) try: yield s finally: diff --git a/tests/memory/local_backend/test_local_memory_commit.py b/tests/memory/local_backend/test_local_memory_commit.py index 6d15840..b9dfc6f 100644 --- a/tests/memory/local_backend/test_local_memory_commit.py +++ b/tests/memory/local_backend/test_local_memory_commit.py @@ -21,6 +21,7 @@ from rath.memory.op_types import MemoryOpCommit, MemoryOpList, MemoryOpRead from rath.memory.results import ( MemoryCommitResult, + MemoryExecutionFailure, MemoryListResult, MemoryReadResult, ) @@ -130,6 +131,23 @@ def test_commit_with_no_chat_client_skips_extraction( assert not extracted_dir.exists() +def test_commit_rejects_path_like_session_id( + backend: LocalMemoryBackend, store: MemoryStore, tmp_path: Path +) -> None: + outside = tmp_path / "outside" + res = backend.dispatch( + store, + MemoryOpCommit( + session_id=f"../{outside.name}", + messages=[{"role": "user", "content": "x"}], + wait=False, + ), + ) + assert isinstance(res, MemoryExecutionFailure) + assert res.kind == "invalid_uri" + assert not outside.exists() + + # ----------------------------------------------------- Extraction (wait=True + live chat) diff --git a/tests/memory/local_backend/test_local_memory_resource.py b/tests/memory/local_backend/test_local_memory_resource.py index dba316f..158d37b 100644 --- a/tests/memory/local_backend/test_local_memory_resource.py +++ b/tests/memory/local_backend/test_local_memory_resource.py @@ -16,7 +16,7 @@ import pytest -from rath.memory import MemoryStore +from rath.memory import MemoryStore, MemoryStoreSpec from rath.memory.adapters.local import LocalMemoryBackend from rath.memory.op_types import MemoryOpResource from rath.memory.results import ( @@ -99,6 +99,20 @@ def test_resource_ingest_missing_local_file_is_not_found( assert res.kind == "not_found" +def test_resource_ingest_rejects_local_file_without_import_root( + backend: LocalMemoryBackend, tmp_path: Path +) -> None: + src = tmp_path / "secret.txt" + src.write_text("secret", encoding="utf-8") + unsafe_store = backend.open() + try: + res = backend.dispatch(unsafe_store, MemoryOpResource(source=str(src))) + finally: + backend.close(unsafe_store) + assert isinstance(res, MemoryExecutionFailure) + assert res.kind == "unauthorized" + + def test_resource_ingest_rejects_unknown_target_scope( backend: LocalMemoryBackend, store: MemoryStore, tmp_path: Path ) -> None: @@ -182,3 +196,30 @@ def test_resource_ingest_http_url_persists_body( assert blob.read_bytes() == _ServeBody.BODY meta_body = (root / "meta.md").read_text(encoding="utf-8") assert "127.0.0.1" in meta_body # original URL in meta + + +def test_resource_ingest_rejects_http_url_without_allowed_host( + backend: LocalMemoryBackend, http_server: str +) -> None: + unsafe_store = backend.open() + try: + res = backend.dispatch(unsafe_store, MemoryOpResource(source=http_server)) + finally: + backend.close(unsafe_store) + assert isinstance(res, MemoryExecutionFailure) + assert res.kind == "unauthorized" + + +def test_resource_ingest_blocks_private_host_without_opt_in( + backend: LocalMemoryBackend, http_server: str +) -> None: + spec = MemoryStoreSpec( + options={"resource_allowed_http_hosts": ["127.0.0.1"]}, + ) + unsafe_store = backend.open(spec) + try: + res = backend.dispatch(unsafe_store, MemoryOpResource(source=http_server)) + finally: + backend.close(unsafe_store) + assert isinstance(res, MemoryExecutionFailure) + assert res.kind == "unauthorized" diff --git a/tests/memory/persistence/test_persistence.py b/tests/memory/persistence/test_persistence.py index e1e6f41..74d5d93 100644 --- a/tests/memory/persistence/test_persistence.py +++ b/tests/memory/persistence/test_persistence.py @@ -13,6 +13,8 @@ from pathlib import Path from uuid import UUID, uuid4 +import pytest + from rath.memory.persistence import ( PersistentMemoryRegistry, local_memory_root, @@ -56,6 +58,15 @@ def test_local_store_dir_accepts_str(_isolate_openrath_home: Path) -> None: assert local_store_dir(str(sid)) == local_store_dir(sid) +@pytest.mark.parametrize("bad_id", ["../escape", "/tmp/escape", "not-a-uuid"]) +def test_local_store_dir_rejects_path_like_ids( + _isolate_openrath_home: Path, + bad_id: str, +) -> None: + with pytest.raises(ValueError, match="store_id must be a UUID"): + local_store_dir(bad_id) + + def test_ensure_local_memory_root_creates_and_is_idempotent( _isolate_openrath_home: Path, ) -> None: diff --git a/tests/session/persistence/test_loader_gc.py b/tests/session/persistence/test_loader_gc.py index d906381..baa55f8 100644 --- a/tests/session/persistence/test_loader_gc.py +++ b/tests/session/persistence/test_loader_gc.py @@ -33,6 +33,7 @@ load_session, prune_sessions, session_file, + session_partial_file, ) from rath.session.session import Session @@ -213,6 +214,17 @@ def test_session_file_path_under_resolved_dir(_isolate_openrath_home: Path) -> N assert str(expected).endswith(f"{sid}.jsonl") +@pytest.mark.parametrize("bad_id", ["../escape", "/tmp/escape", "not-a-uuid"]) +def test_session_paths_reject_path_like_ids( + _isolate_openrath_home: Path, + bad_id: str, +) -> None: + with pytest.raises(ValueError, match="session_id must be a UUID"): + session_file(bad_id) + with pytest.raises(ValueError, match="session_id must be a UUID"): + session_partial_file(bad_id) + + # --------------------------------------------------------------------------- # GC: delete + prune # --------------------------------------------------------------------------- From aaff9f5cd02231978a92e75824963dcbd03010c3 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 11:41:09 +0800 Subject: [PATCH 02/22] feat(v2): establish security and request context --- src/rath/__init__.py | 3 +- src/rath/_json.py | 49 +++++ src/rath/backend/__init__.py | 3 + src/rath/backend/local.py | 77 ++++++-- src/rath/context.py | 108 +++++++++++ src/rath/errors.py | 60 +++++++ src/rath/security/__init__.py | 53 ++++++ src/rath/security/audit.py | 110 ++++++++++++ src/rath/security/context.py | 136 ++++++++++++++ src/rath/security/policy.py | 226 ++++++++++++++++++++++++ src/rath/security/secrets.py | 64 +++++++ tests/backends/test_local.py | 91 +++++++++- tests/security/test_context.py | 75 ++++++++ tests/security/test_policy.py | 99 +++++++++++ tests/security/test_secrets_audit.py | 55 ++++++ tests/session/test_arun_session_loop.py | 6 +- tests/unit/test_errors_v2.py | 28 +++ 17 files changed, 1219 insertions(+), 24 deletions(-) create mode 100644 src/rath/_json.py create mode 100644 src/rath/context.py create mode 100644 src/rath/errors.py create mode 100644 src/rath/security/__init__.py create mode 100644 src/rath/security/audit.py create mode 100644 src/rath/security/context.py create mode 100644 src/rath/security/policy.py create mode 100644 src/rath/security/secrets.py create mode 100644 tests/security/test_context.py create mode 100644 tests/security/test_policy.py create mode 100644 tests/security/test_secrets_audit.py create mode 100644 tests/unit/test_errors_v2.py diff --git a/src/rath/__init__.py b/src/rath/__init__.py index ffaa334..887a94e 100644 --- a/src/rath/__init__.py +++ b/src/rath/__init__.py @@ -21,9 +21,10 @@ backend, flow, memory, + security, ) -__all__ = ["backend", "flow", "memory", "session"] +__all__ = ["backend", "flow", "memory", "security", "session"] def __getattr__(name: str) -> Any: diff --git a/src/rath/_json.py b/src/rath/_json.py new file mode 100644 index 0000000..bb62f27 --- /dev/null +++ b/src/rath/_json.py @@ -0,0 +1,49 @@ +"""Small immutable JSON helpers shared by public v2 contracts.""" + +from __future__ import annotations + +from collections.abc import Mapping +from types import MappingProxyType +from typing import TypeAlias + +JSONScalar: TypeAlias = None | bool | int | float | str +JSONValue: TypeAlias = JSONScalar | tuple["JSONValue", ...] | Mapping[str, "JSONValue"] + + +def freeze_json(value: object, *, field: str = "value") -> JSONValue: + """Return an immutable, detached representation of a JSON-compatible value.""" + if value is None or isinstance(value, (bool, int, float, str)): + return value + if isinstance(value, Mapping): + frozen: dict[str, JSONValue] = {} + for key, item in value.items(): + if not isinstance(key, str): + raise TypeError(f"{field} mapping keys must be strings") + frozen[key] = freeze_json(item, field=f"{field}.{key}") + return MappingProxyType(frozen) + if isinstance(value, (list, tuple)): + return tuple( + freeze_json(item, field=f"{field}[{index}]") + for index, item in enumerate(value) + ) + raise TypeError(f"{field} must be JSON-compatible, got {type(value).__name__}") + + +def freeze_mapping( + value: Mapping[str, object] | None, + *, + field: str, +) -> Mapping[str, JSONValue]: + """Freeze a JSON object and return an immutable mapping.""" + frozen = freeze_json(value or {}, field=field) + assert isinstance(frozen, Mapping) + return frozen + + +def thaw_json(value: JSONValue) -> object: + """Return a mutable JSON-compatible copy suitable for serialization.""" + if isinstance(value, Mapping): + return {key: thaw_json(item) for key, item in value.items()} + if isinstance(value, tuple): + return [thaw_json(item) for item in value] + return value diff --git a/src/rath/backend/__init__.py b/src/rath/backend/__init__.py index aad2141..e737263 100644 --- a/src/rath/backend/__init__.py +++ b/src/rath/backend/__init__.py @@ -11,6 +11,7 @@ BackendSandboxClosed, UnsupportedBackendTool, ) +from rath.backend.local import LocalBackend, TrustedHostBackend from rath.backend.registry import ( current, get, @@ -49,6 +50,8 @@ __all__ = [ "Backend", + "LocalBackend", + "TrustedHostBackend", "BackendSandbox", "BackendSandboxSpec", "BackendTool", diff --git a/src/rath/backend/local.py b/src/rath/backend/local.py index 4cea93c..d8e5992 100644 --- a/src/rath/backend/local.py +++ b/src/rath/backend/local.py @@ -1,7 +1,10 @@ -"""Host-process backend: subprocesses and filesystem under a temp working directory. +"""Trusted-host backend: subprocesses and filesystem under a working directory. -Always available. Relative paths in tool calls are resolved against the sandbox -working directory; absolute paths pass through unchanged. +Always available, but **not an isolation boundary**. Filesystem tool paths and +command working directories are contained under the configured workspace; +executed code still inherits the host process privileges and may access the +host through normal language or shell APIs. Service deployments must reject +this backend unless an explicit trusted-host policy allows it. The implementation is async-internal: ``_aopen`` / ``_aclose`` / ``_adispatch`` are the canonical entry points. Each blocking primitive (``subprocess.run``, @@ -63,11 +66,15 @@ from rath.utils.decoding import decode_subprocess_output -@register("local") -class LocalBackend(Backend): - """Run tool calls as host-side subprocesses with a per-sandbox working dir.""" +class _PathViolation(ValueError): + """A filesystem operation escaped the configured workspace.""" - name: ClassVar[str] = "local" + +@register("trusted-host") +class TrustedHostBackend(Backend): + """Run tool calls on the host under an explicitly trusted policy.""" + + name: ClassVar[str] = "trusted-host" _CAPABILITIES: ClassVar[Capabilities] = Capabilities( isolation=IsolationLevel.PROCESS, @@ -117,9 +124,9 @@ async def _aopen(self, spec: BackendSandboxSpec | None = None) -> BackendSandbox else: owns_working_dir = False working_dir = spec.working_dir - await asyncio.to_thread( - lambda: Path(working_dir).mkdir(parents=True, exist_ok=True) - ) + working_path = Path(working_dir).expanduser() + await asyncio.to_thread(working_path.mkdir, parents=True, exist_ok=True) + working_dir = str(await asyncio.to_thread(working_path.resolve)) sandbox = BackendSandbox(backend=self, handle=working_dir, spec=spec) # Mutate handle sets only on the runtime loop thread. self._open_handles.add(working_dir) @@ -168,15 +175,30 @@ async def _adispatch( ) def _resolve(self, sandbox: BackendSandbox, path: str) -> Path: - p = Path(path) - if p.is_absolute(): - return p - return Path(sandbox.handle) / path + root = Path(sandbox.handle).resolve(strict=False) + candidate = Path(path).expanduser() + if not candidate.is_absolute(): + candidate = root / candidate + resolved = candidate.resolve(strict=False) + try: + resolved.relative_to(root) + except ValueError as exc: + raise _PathViolation( + f"path {path!r} is outside sandbox workspace {str(root)!r}" + ) from exc + return resolved def _command_run( self, sandbox: BackendSandbox, call: BackendToolCommandRun ) -> CommandResult | ToolExecutionFailure: - cwd = self._resolve(sandbox, call.cwd) if call.cwd else Path(sandbox.handle) + try: + cwd = ( + self._resolve(sandbox, call.cwd) + if call.cwd + else Path(sandbox.handle).resolve(strict=False) + ) + except _PathViolation as exc: + return tool_failure_from("path_violation", exc) env_arg: dict[str, str] | None = None if call.env is not None: env_arg = {**os.environ, **call.env} @@ -221,11 +243,13 @@ def _command_run( def _files_read( self, sandbox: BackendSandbox, call: BackendToolFilesRead ) -> FileContent | ToolExecutionFailure: - p = self._resolve(sandbox, call.path) try: + p = self._resolve(sandbox, call.path) if call.encoding is None: return FileContent(data=p.read_bytes()) return FileContent(data=p.read_text(encoding=call.encoding)) + except _PathViolation as exc: + return tool_failure_from("path_violation", exc) except FileNotFoundError as exc: return tool_failure_from("file_not_found", exc, detail=str(p)) except OSError as exc: @@ -234,13 +258,15 @@ def _files_read( def _files_write( self, sandbox: BackendSandbox, call: BackendToolFilesWrite ) -> FileWriteResult | ToolExecutionFailure: - p = self._resolve(sandbox, call.path) payload_bytes = ( call.data.encode("utf-8") if isinstance(call.data, str) else call.data ) try: + p = self._resolve(sandbox, call.path) p.parent.mkdir(parents=True, exist_ok=True) p.write_bytes(payload_bytes) + except _PathViolation as exc: + return tool_failure_from("path_violation", exc) except OSError as exc: return tool_failure_from("os_error", exc) with contextlib.suppress(OSError): @@ -250,8 +276,8 @@ def _files_write( def _files_list( self, sandbox: BackendSandbox, call: BackendToolFilesList ) -> FileEntries | ToolExecutionFailure: - p = self._resolve(sandbox, call.path) try: + p = self._resolve(sandbox, call.path) entries = [ FileEntry( name=child.name, @@ -260,6 +286,8 @@ def _files_list( ) for child in p.iterdir() ] + except _PathViolation as exc: + return tool_failure_from("path_violation", exc) except OSError as exc: return tool_failure_from("os_error", exc) entries.sort(key=lambda e: e.name) @@ -270,7 +298,7 @@ def _files_exists( ) -> bool: try: return self._resolve(sandbox, call.path).exists() - except OSError: + except (OSError, _PathViolation): # Treat permission errors / unreadable parent dirs as "not present" # rather than raising into the loop; matches POSIX stat() semantics # from the caller's perspective. @@ -310,3 +338,14 @@ def _code_run( stderr=proc.stderr if proc.stderr is not None else b"", error=error, ) + + +@register("local") +class LocalBackend(TrustedHostBackend): + """Compatibility alias for :class:`TrustedHostBackend`. + + The ``local`` name remains available for v1 compatibility. New code should + request ``trusted-host`` so the lack of process isolation is explicit. + """ + + name: ClassVar[str] = "local" diff --git a/src/rath/context.py b/src/rath/context.py new file mode 100644 index 0000000..8760c6e --- /dev/null +++ b/src/rath/context.py @@ -0,0 +1,108 @@ +"""Request, trace, and durable run context contracts.""" + +from __future__ import annotations + +import secrets +from dataclasses import dataclass, field +from datetime import datetime, timezone +from uuid import UUID, uuid4 + +from rath.errors import ErrorCode, RathError +from rath.security.context import SecurityContext + +__all__ = [ + "DeadlineExceededError", + "RunContext", + "TraceContext", +] + + +def _validate_hex(value: str, *, length: int, field_name: str) -> str: + normalized = value.lower() + if len(normalized) != length: + raise ValueError(f"{field_name} must contain {length} hexadecimal characters") + try: + int(normalized, 16) + except ValueError as exc: + raise ValueError(f"{field_name} must be hexadecimal") from exc + return normalized + + +@dataclass(frozen=True, slots=True) +class TraceContext: + """Minimal W3C-compatible trace correlation identifiers.""" + + trace_id: str + span_id: str + sampled: bool = True + + def __post_init__(self) -> None: + object.__setattr__( + self, + "trace_id", + _validate_hex(self.trace_id, length=32, field_name="trace_id"), + ) + object.__setattr__( + self, + "span_id", + _validate_hex(self.span_id, length=16, field_name="span_id"), + ) + + @classmethod + def new(cls, *, sampled: bool = True) -> "TraceContext": + return cls( + trace_id=secrets.token_hex(16), + span_id=secrets.token_hex(8), + sampled=sampled, + ) + + +class DeadlineExceededError(RathError): + def __init__(self) -> None: + super().__init__( + ErrorCode.DEADLINE_EXCEEDED, + "run deadline has been exceeded", + retryable=False, + ) + + +@dataclass(frozen=True, slots=True) +class RunContext: + """Explicit context propagated through runtime and adapter calls.""" + + security: SecurityContext + revision_id: UUID + request_id: UUID = field(default_factory=uuid4) + trace_context: TraceContext = field(default_factory=TraceContext.new) + deadline: datetime | None = None + + def __post_init__(self) -> None: + if self.deadline is not None and self.deadline.tzinfo is None: + raise ValueError("deadline must be timezone-aware") + + @classmethod + def local( + cls, + *, + revision_id: UUID, + deadline: datetime | None = None, + ) -> "RunContext": + return cls( + security=SecurityContext.local(), + revision_id=revision_id, + deadline=deadline, + ) + + def remaining_seconds(self, *, now: datetime | None = None) -> float | None: + if self.deadline is None: + return None + current = now or datetime.now(timezone.utc) + if current.tzinfo is None: + raise ValueError("now must be timezone-aware") + return max(0.0, (self.deadline - current).total_seconds()) + + def ensure_active(self, *, now: datetime | None = None) -> None: + remaining = self.remaining_seconds(now=now) + if remaining is not None and remaining <= 0: + raise DeadlineExceededError() + diff --git a/src/rath/errors.py b/src/rath/errors.py new file mode 100644 index 0000000..70e2043 --- /dev/null +++ b/src/rath/errors.py @@ -0,0 +1,60 @@ +"""Stable machine-readable errors for OpenRath v2 public contracts.""" + +from __future__ import annotations + +from collections.abc import Mapping +from enum import Enum +from typing import Any + +from rath._json import JSONValue, freeze_mapping, thaw_json + +__all__ = ["ErrorCode", "RathError"] + + +class ErrorCode(str, Enum): + """Stable error identifiers; enum values are part of the public API.""" + + INVALID_ARGUMENT = "request.invalid_argument" + UNAUTHENTICATED = "security.unauthenticated" + FORBIDDEN = "security.forbidden" + APPROVAL_REQUIRED = "security.approval_required" + POLICY_ERROR = "security.policy_error" + CONFLICT = "resource.conflict" + NOT_FOUND = "resource.not_found" + DEADLINE_EXCEEDED = "runtime.deadline_exceeded" + CANCELLED = "runtime.cancelled" + UNAVAILABLE = "runtime.unavailable" + INTERNAL = "internal.error" + + +class RathError(RuntimeError): + """Base exception with a stable code and serialization contract.""" + + def __init__( + self, + code: ErrorCode, + message: str, + *, + retryable: bool = False, + details: Mapping[str, object] | None = None, + ) -> None: + if not message: + raise ValueError("message must not be empty") + super().__init__(message) + self.code = code + self.message = message + self.retryable = bool(retryable) + self.details: Mapping[str, JSONValue] = freeze_mapping( + details, + field="details", + ) + + def to_dict(self) -> dict[str, Any]: + """Return the stable transport representation.""" + return { + "code": self.code.value, + "message": self.message, + "retryable": self.retryable, + "details": thaw_json(self.details), + } + diff --git a/src/rath/security/__init__.py b/src/rath/security/__init__.py new file mode 100644 index 0000000..af4efd8 --- /dev/null +++ b/src/rath/security/__init__.py @@ -0,0 +1,53 @@ +"""Public security contracts for identity, policy, secrets, and audit.""" + +from rath.security.audit import AuditEvent, AuditKind, AuditSink, InMemoryAuditSink +from rath.security.context import ( + Principal, + PrincipalKind, + Provenance, + SecurityContext, + TrustLevel, +) +from rath.security.policy import ( + Action, + ApprovalRequiredError, + AuthorizationError, + DenyAllPolicy, + LocalTrustedPolicy, + PolicyConstraints, + PolicyDecision, + PolicyEffect, + PolicyEngine, + PolicyEvaluationError, + ResourceRef, + authorize, +) +from rath.security.secrets import ResolvedSecret, SecretRef, SecretResolver + +__all__ = [ + "Action", + "ApprovalRequiredError", + "AuditEvent", + "AuditKind", + "AuditSink", + "AuthorizationError", + "authorize", + "DenyAllPolicy", + "InMemoryAuditSink", + "LocalTrustedPolicy", + "PolicyConstraints", + "PolicyDecision", + "PolicyEffect", + "PolicyEngine", + "PolicyEvaluationError", + "Principal", + "PrincipalKind", + "Provenance", + "ResolvedSecret", + "ResourceRef", + "SecretRef", + "SecretResolver", + "SecurityContext", + "TrustLevel", +] + diff --git a/src/rath/security/audit.py b/src/rath/security/audit.py new file mode 100644 index 0000000..b553a08 --- /dev/null +++ b/src/rath/security/audit.py @@ -0,0 +1,110 @@ +"""Security audit events kept distinct from diagnostic traces.""" + +from __future__ import annotations + +import threading +from collections.abc import Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from typing import Protocol, runtime_checkable +from uuid import UUID, uuid4 + +from rath._json import JSONValue, freeze_mapping +from rath.context import RunContext +from rath.security.policy import Action, PolicyDecision, ResourceRef + +__all__ = [ + "AuditEvent", + "AuditKind", + "AuditSink", + "InMemoryAuditSink", +] + + +class AuditKind(str, Enum): + AUTHENTICATION = "authentication" + POLICY_DECISION = "policy_decision" + SECRET_RESOLUTION = "secret_resolution" + TOOL_ACCESS = "tool_access" + SANDBOX_ACCESS = "sandbox_access" + MEMORY_ACCESS = "memory_access" + RUN_CONTROL = "run_control" + OPERATOR_OVERRIDE = "operator_override" + + +@dataclass(frozen=True, slots=True) +class AuditEvent: + id: UUID + kind: AuditKind + occurred_at: datetime + tenant_id: str + principal_id: str + request_id: UUID + trace_id: str + action: str + resource_kind: str + resource_id: str + outcome: str + reason: str + policy_id: str | None = None + attributes: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + if self.occurred_at.tzinfo is None: + raise ValueError("audit occurred_at must be timezone-aware") + object.__setattr__( + self, + "attributes", + freeze_mapping(self.attributes, field="audit.attributes"), + ) + + @classmethod + def for_policy_decision( + cls, + *, + kind: AuditKind, + action: Action, + resource: ResourceRef, + context: RunContext, + decision: PolicyDecision, + attributes: Mapping[str, object] | None = None, + ) -> "AuditEvent": + return cls( + id=uuid4(), + kind=kind, + occurred_at=datetime.now(timezone.utc), + tenant_id=context.security.tenant_id, + principal_id=context.security.principal.id, + request_id=context.request_id, + trace_id=context.trace_context.trace_id, + action=action.name, + resource_kind=resource.kind, + resource_id=resource.id, + outcome=decision.effect.value, + reason=decision.reason, + policy_id=decision.policy_id, + attributes=freeze_mapping(attributes, field="audit.attributes"), + ) + + +@runtime_checkable +class AuditSink(Protocol): + async def emit(self, event: AuditEvent) -> None: ... + + +class InMemoryAuditSink: + """Deterministic reference sink for embedded mode and contract tests.""" + + def __init__(self) -> None: + self._events: list[AuditEvent] = [] + self._lock = threading.Lock() + + @property + def events(self) -> tuple[AuditEvent, ...]: + with self._lock: + return tuple(self._events) + + async def emit(self, event: AuditEvent) -> None: + with self._lock: + self._events.append(event) diff --git a/src/rath/security/context.py b/src/rath/security/context.py new file mode 100644 index 0000000..298e4d3 --- /dev/null +++ b/src/rath/security/context.py @@ -0,0 +1,136 @@ +"""Identity, tenancy, trust, and provenance contracts.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from enum import Enum + +from rath._json import JSONValue, freeze_mapping + +__all__ = [ + "Principal", + "PrincipalKind", + "Provenance", + "SecurityContext", + "TrustLevel", +] + + +class PrincipalKind(str, Enum): + USER = "user" + SERVICE = "service" + SYSTEM = "system" + + +class TrustLevel(str, Enum): + UNTRUSTED = "untrusted" + TRUSTED = "trusted" + SYSTEM = "system" + + +def _required(value: str, *, field_name: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError(f"{field_name} must not be empty") + return normalized + + +@dataclass(frozen=True, slots=True) +class Principal: + """Authenticated caller identity detached from transport concerns.""" + + id: str + kind: PrincipalKind + claims: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__(self, "id", _required(self.id, field_name="principal.id")) + object.__setattr__( + self, + "claims", + freeze_mapping(self.claims, field="principal.claims"), + ) + + +@dataclass(frozen=True, slots=True) +class SecurityContext: + """Run-scoped identity and tenant boundary.""" + + principal: Principal + tenant_id: str + project_id: str | None = None + grants: frozenset[str] = field(default_factory=frozenset) + attributes: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "tenant_id", + _required(self.tenant_id, field_name="tenant_id"), + ) + if self.project_id is not None: + object.__setattr__( + self, + "project_id", + _required(self.project_id, field_name="project_id"), + ) + object.__setattr__( + self, + "grants", + frozenset(_required(item, field_name="grant") for item in self.grants), + ) + object.__setattr__( + self, + "attributes", + freeze_mapping(self.attributes, field="security.attributes"), + ) + + @classmethod + def local( + cls, + *, + grants: Iterable[str] = ("trusted_host",), + ) -> "SecurityContext": + """Create the explicit trusted-process context for embedded local mode.""" + return cls( + principal=Principal( + id="local-process", + kind=PrincipalKind.SYSTEM, + claims={"mode": "embedded"}, + ), + tenant_id="local", + grants=frozenset(grants), + attributes={"deployment_mode": "embedded"}, + ) + + def has_grant(self, grant: str) -> bool: + return grant in self.grants + + +@dataclass(frozen=True, slots=True) +class Provenance: + """Origin metadata carried by untrusted and trusted content.""" + + source_type: str + source_id: str + producer: str | None = None + metadata: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "source_type", + _required(self.source_type, field_name="source_type"), + ) + object.__setattr__( + self, + "source_id", + _required(self.source_id, field_name="source_id"), + ) + object.__setattr__( + self, + "metadata", + freeze_mapping(self.metadata, field="provenance.metadata"), + ) + diff --git a/src/rath/security/policy.py b/src/rath/security/policy.py new file mode 100644 index 0000000..2801b29 --- /dev/null +++ b/src/rath/security/policy.py @@ -0,0 +1,226 @@ +"""Fail-closed authorization contracts and reference policies.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from enum import Enum +from typing import Protocol, runtime_checkable + +from rath._json import JSONValue, freeze_mapping +from rath.context import RunContext +from rath.errors import ErrorCode, RathError + +__all__ = [ + "Action", + "ApprovalRequiredError", + "AuthorizationError", + "DenyAllPolicy", + "LocalTrustedPolicy", + "PolicyConstraints", + "PolicyDecision", + "PolicyEffect", + "PolicyEngine", + "PolicyEvaluationError", + "ResourceRef", + "authorize", +] + + +def _required(value: str, *, field_name: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError(f"{field_name} must not be empty") + return normalized + + +@dataclass(frozen=True, slots=True) +class Action: + name: str + + def __post_init__(self) -> None: + object.__setattr__(self, "name", _required(self.name, field_name="action")) + + def __str__(self) -> str: + return self.name + + +@dataclass(frozen=True, slots=True) +class ResourceRef: + kind: str + id: str + tenant_id: str | None = None + attributes: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "kind", + _required(self.kind, field_name="resource.kind"), + ) + object.__setattr__( + self, + "id", + _required(self.id, field_name="resource.id"), + ) + object.__setattr__( + self, + "attributes", + freeze_mapping(self.attributes, field="resource.attributes"), + ) + + +@dataclass(frozen=True, slots=True) +class PolicyConstraints: + timeout_seconds: float | None = None + max_output_bytes: int | None = None + allowed_network_hosts: frozenset[str] = field(default_factory=frozenset) + filesystem_root: str | None = None + read_only: bool = False + redactions: frozenset[str] = field(default_factory=frozenset) + + def __post_init__(self) -> None: + if self.timeout_seconds is not None and self.timeout_seconds <= 0: + raise ValueError("timeout_seconds must be greater than zero") + if self.max_output_bytes is not None and self.max_output_bytes <= 0: + raise ValueError("max_output_bytes must be greater than zero") + object.__setattr__( + self, + "allowed_network_hosts", + frozenset(host.lower() for host in self.allowed_network_hosts), + ) + object.__setattr__(self, "redactions", frozenset(self.redactions)) + + +class PolicyEffect(str, Enum): + ALLOW = "allow" + DENY = "deny" + REQUIRE_APPROVAL = "require_approval" + ALLOW_WITH_CONSTRAINTS = "allow_with_constraints" + + +@dataclass(frozen=True, slots=True) +class PolicyDecision: + effect: PolicyEffect + reason: str + policy_id: str + constraints: PolicyConstraints = field(default_factory=PolicyConstraints) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "reason", + _required(self.reason, field_name="policy reason"), + ) + object.__setattr__( + self, + "policy_id", + _required(self.policy_id, field_name="policy_id"), + ) + + +@runtime_checkable +class PolicyEngine(Protocol): + async def evaluate( + self, + action: Action, + resource: ResourceRef, + context: RunContext, + ) -> PolicyDecision: ... + + +class AuthorizationError(RathError): + def __init__(self, decision: PolicyDecision) -> None: + super().__init__( + ErrorCode.FORBIDDEN, + decision.reason, + retryable=False, + details={ + "effect": decision.effect.value, + "policy_id": decision.policy_id, + }, + ) + self.decision = decision + + +class ApprovalRequiredError(RathError): + def __init__(self, decision: PolicyDecision) -> None: + super().__init__( + ErrorCode.APPROVAL_REQUIRED, + decision.reason, + retryable=False, + details={"policy_id": decision.policy_id}, + ) + self.decision = decision + + +class PolicyEvaluationError(RathError): + def __init__(self) -> None: + super().__init__( + ErrorCode.POLICY_ERROR, + "policy evaluation failed closed", + retryable=False, + ) + + +class DenyAllPolicy: + """Safe default for service and untrusted deployment profiles.""" + + async def evaluate( + self, + action: Action, + resource: ResourceRef, + context: RunContext, + ) -> PolicyDecision: + return PolicyDecision( + effect=PolicyEffect.DENY, + reason="no policy grant allows this action", + policy_id="deny-all", + ) + + +class LocalTrustedPolicy: + """Explicit opt-in policy for the embedded trusted-process profile.""" + + async def evaluate( + self, + action: Action, + resource: ResourceRef, + context: RunContext, + ) -> PolicyDecision: + allowed = ( + context.security.tenant_id == "local" + and context.security.has_grant("trusted_host") + ) + return PolicyDecision( + effect=PolicyEffect.ALLOW if allowed else PolicyEffect.DENY, + reason=( + "explicit embedded trusted-host context" + if allowed + else "trusted-host policy is restricted to embedded local context" + ), + policy_id="local-trusted", + ) + + +async def authorize( + engine: PolicyEngine, + *, + action: Action, + resource: ResourceRef, + context: RunContext, +) -> PolicyDecision: + """Evaluate a policy and turn non-allow effects into stable exceptions.""" + context.ensure_active() + try: + decision = await engine.evaluate(action, resource, context) + except RathError: + raise + except Exception as exc: + raise PolicyEvaluationError() from exc + if decision.effect is PolicyEffect.DENY: + raise AuthorizationError(decision) + if decision.effect is PolicyEffect.REQUIRE_APPROVAL: + raise ApprovalRequiredError(decision) + return decision + diff --git a/src/rath/security/secrets.py b/src/rath/security/secrets.py new file mode 100644 index 0000000..4f03964 --- /dev/null +++ b/src/rath/security/secrets.py @@ -0,0 +1,64 @@ +"""Secret references and explicit resolution boundaries.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol, runtime_checkable + +from rath.context import RunContext + +__all__ = [ + "ResolvedSecret", + "SecretRef", + "SecretResolver", +] + + +@dataclass(frozen=True, slots=True) +class SecretRef: + provider: str + key: str + version: str | None = None + + def __post_init__(self) -> None: + if not self.provider.strip(): + raise ValueError("secret provider must not be empty") + if not self.key.strip(): + raise ValueError("secret key must not be empty") + + def __str__(self) -> str: + version = f"@{self.version}" if self.version else "" + return f"{self.provider}:{self.key}{version}" + + +@dataclass(frozen=True, slots=True, repr=False) +class ResolvedSecret: + """Short-lived secret value whose representation is always redacted.""" + + ref: SecretRef + value: str + + def __post_init__(self) -> None: + if not self.value: + raise ValueError("resolved secret value must not be empty") + + def __repr__(self) -> str: + return f"ResolvedSecret(ref={self.ref!s}, value=)" + + def __str__(self) -> str: + return f"" + + def reveal(self) -> str: + """Return the value at the adapter boundary; callers must not log it.""" + return self.value + + +@runtime_checkable +class SecretResolver(Protocol): + async def resolve( + self, + ref: SecretRef, + *, + context: RunContext, + ) -> ResolvedSecret: ... + diff --git a/tests/backends/test_local.py b/tests/backends/test_local.py index 30cc7b2..14feb17 100644 --- a/tests/backends/test_local.py +++ b/tests/backends/test_local.py @@ -19,7 +19,7 @@ ToolExecutionFailure, get, ) -from rath.backend.local import LocalBackend +from rath.backend.local import LocalBackend, TrustedHostBackend def test_is_available_is_true() -> None: @@ -53,6 +53,13 @@ def test_local_is_registered_under_name_local() -> None: assert inst.name == "local" +def test_trusted_host_is_the_canonical_explicit_backend_name() -> None: + inst = get("trusted-host") + assert isinstance(inst, TrustedHostBackend) + assert not isinstance(inst, LocalBackend) + assert inst.name == "trusted-host" + + def test_handle_is_a_real_working_directory() -> None: backend = get("local") sb = backend.open() @@ -98,6 +105,88 @@ def test_close_does_not_remove_user_supplied_working_dir(tmp_path: object) -> No assert sentinel.read_text(encoding="utf-8") == "do not delete me" +def test_filesystem_calls_reject_absolute_paths_outside_workspace(tmp_path: object) -> None: + import pathlib + + root = pathlib.Path(str(tmp_path)) / "workspace" # type: ignore[arg-type] + outside = pathlib.Path(str(tmp_path)) / "outside.txt" # type: ignore[arg-type] + root.mkdir() + outside.write_text("secret", encoding="utf-8") + backend = LocalBackend() + sb = backend.open(BackendSandboxSpec(working_dir=str(root))) + try: + result = sb.dispatch(BackendToolFilesRead(path=str(outside))) + finally: + backend.close(sb) + + assert isinstance(result, ToolExecutionFailure) + assert result.kind == "path_violation" + assert "outside sandbox workspace" in result.message + + +def test_filesystem_calls_reject_parent_traversal(tmp_path: object) -> None: + import pathlib + + root = pathlib.Path(str(tmp_path)) / "workspace" # type: ignore[arg-type] + root.mkdir() + backend = LocalBackend() + sb = backend.open(BackendSandboxSpec(working_dir=str(root))) + try: + result = sb.dispatch(BackendToolFilesWrite(path="../escape.txt", data="x")) + finally: + backend.close(sb) + + assert isinstance(result, ToolExecutionFailure) + assert result.kind == "path_violation" + assert not (root.parent / "escape.txt").exists() + + +def test_command_cwd_must_remain_in_workspace(tmp_path: object) -> None: + import pathlib + + root = pathlib.Path(str(tmp_path)) / "workspace" # type: ignore[arg-type] + root.mkdir() + backend = LocalBackend() + sb = backend.open(BackendSandboxSpec(working_dir=str(root))) + try: + result = sb.dispatch( + BackendToolCommandRun( + cmd=[sys.executable, "-c", "print('should not run')"], + cwd="..", + ) + ) + finally: + backend.close(sb) + + assert isinstance(result, ToolExecutionFailure) + assert result.kind == "path_violation" + + +def test_symlink_escape_is_rejected_when_supported(tmp_path: object) -> None: + import pathlib + + root = pathlib.Path(str(tmp_path)) / "workspace" # type: ignore[arg-type] + outside = pathlib.Path(str(tmp_path)) / "outside" # type: ignore[arg-type] + root.mkdir() + outside.mkdir() + (outside / "secret.txt").write_text("secret", encoding="utf-8") + link = root / "link" + try: + link.symlink_to(outside, target_is_directory=True) + except OSError: + pytest.skip("symlink creation is unavailable") + + backend = LocalBackend() + sb = backend.open(BackendSandboxSpec(working_dir=str(root))) + try: + result = sb.dispatch(BackendToolFilesRead(path="link/secret.txt")) + finally: + backend.close(sb) + + assert isinstance(result, ToolExecutionFailure) + assert result.kind == "path_violation" + + def test_command_missing_executable_returns_failure() -> None: backend = get("local") with backend.open() as sb: diff --git a/tests/security/test_context.py b/tests/security/test_context.py new file mode 100644 index 0000000..30ee344 --- /dev/null +++ b/tests/security/test_context.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from uuid import UUID, uuid4 + +import pytest + +from rath.context import DeadlineExceededError, RunContext, TraceContext +from rath.security import Principal, PrincipalKind, SecurityContext + + +def test_security_context_is_deeply_immutable() -> None: + claims = {"roles": ["developer"], "profile": {"region": "cn"}} + principal = Principal( + id="user-1", + kind=PrincipalKind.USER, + claims=claims, + ) + context = SecurityContext( + principal=principal, + tenant_id="tenant-1", + project_id="project-1", + grants={"tool.search", "memory.read"}, + ) + + claims["roles"].append("admin") + claims["profile"]["region"] = "other" # type: ignore[index] + + assert principal.claims["roles"] == ("developer",) + assert principal.claims["profile"]["region"] == "cn" # type: ignore[index] + assert context.grants == frozenset({"tool.search", "memory.read"}) + with pytest.raises(TypeError): + principal.claims["new"] = True # type: ignore[index] + + +def test_local_context_is_explicit_and_not_anonymous() -> None: + context = SecurityContext.local() + + assert context.tenant_id == "local" + assert context.principal.id == "local-process" + assert context.principal.kind is PrincipalKind.SYSTEM + assert "trusted_host" in context.grants + + +def test_trace_context_uses_w3c_sized_hex_identifiers() -> None: + trace = TraceContext.new() + + assert len(trace.trace_id) == 32 + assert len(trace.span_id) == 16 + int(trace.trace_id, 16) + int(trace.span_id, 16) + + +def test_run_context_rejects_naive_deadline() -> None: + with pytest.raises(ValueError, match="timezone-aware"): + RunContext( + security=SecurityContext.local(), + revision_id=uuid4(), + deadline=datetime.now(), + ) + + +def test_run_context_deadline_check_uses_stable_error_code() -> None: + context = RunContext( + security=SecurityContext.local(), + revision_id=uuid4(), + deadline=datetime.now(timezone.utc) - timedelta(seconds=1), + ) + + with pytest.raises(DeadlineExceededError) as raised: + context.ensure_active() + + assert raised.value.code.value == "runtime.deadline_exceeded" + assert isinstance(context.request_id, UUID) + diff --git a/tests/security/test_policy.py b/tests/security/test_policy.py new file mode 100644 index 0000000..04328d4 --- /dev/null +++ b/tests/security/test_policy.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import asyncio +from dataclasses import replace +from uuid import uuid4 + +import pytest + +from rath.context import RunContext +from rath.security import ( + Action, + ApprovalRequiredError, + AuthorizationError, + DenyAllPolicy, + LocalTrustedPolicy, + PolicyConstraints, + PolicyDecision, + PolicyEffect, + ResourceRef, + authorize, +) + + +def _context() -> RunContext: + return RunContext.local(revision_id=uuid4()) + + +def test_deny_all_policy_fails_closed() -> None: + async def exercise() -> None: + with pytest.raises(AuthorizationError) as raised: + await authorize( + DenyAllPolicy(), + action=Action("tool.execute"), + resource=ResourceRef(kind="tool", id="search"), + context=_context(), + ) + assert raised.value.code.value == "security.forbidden" + + asyncio.run(exercise()) + + +def test_local_trusted_policy_only_allows_explicit_local_context() -> None: + async def exercise() -> None: + decision = await authorize( + LocalTrustedPolicy(), + action=Action("sandbox.execute"), + resource=ResourceRef(kind="sandbox", id="local"), + context=_context(), + ) + assert decision.effect is PolicyEffect.ALLOW + + remote_context = replace( + _context(), + security=replace(_context().security, tenant_id="tenant-1"), + ) + with pytest.raises(AuthorizationError): + await authorize( + LocalTrustedPolicy(), + action=Action("sandbox.execute"), + resource=ResourceRef(kind="sandbox", id="local"), + context=remote_context, + ) + + asyncio.run(exercise()) + + +def test_approval_decision_is_not_misreported_as_denial() -> None: + class ApprovalPolicy: + async def evaluate( + self, + action: Action, + resource: ResourceRef, + context: RunContext, + ) -> PolicyDecision: + return PolicyDecision( + effect=PolicyEffect.REQUIRE_APPROVAL, + reason="non-idempotent tool", + policy_id="test", + ) + + async def exercise() -> None: + with pytest.raises(ApprovalRequiredError) as raised: + await authorize( + ApprovalPolicy(), + action=Action("tool.execute"), + resource=ResourceRef(kind="tool", id="email.send"), + context=_context(), + ) + assert raised.value.code.value == "security.approval_required" + + asyncio.run(exercise()) + + +def test_policy_constraints_validate_resource_budgets() -> None: + with pytest.raises(ValueError, match="max_output_bytes"): + PolicyConstraints(max_output_bytes=0) + with pytest.raises(ValueError, match="timeout_seconds"): + PolicyConstraints(timeout_seconds=-1) + diff --git a/tests/security/test_secrets_audit.py b/tests/security/test_secrets_audit.py new file mode 100644 index 0000000..5a171e4 --- /dev/null +++ b/tests/security/test_secrets_audit.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import asyncio +from uuid import uuid4 + +from rath.context import RunContext +from rath.security import ( + Action, + AuditEvent, + AuditKind, + InMemoryAuditSink, + PolicyDecision, + PolicyEffect, + ResolvedSecret, + ResourceRef, + SecretRef, +) + + +def test_resolved_secret_never_exposes_value_in_repr_or_str() -> None: + secret = ResolvedSecret( + ref=SecretRef(provider="env", key="OPENAI_API_KEY"), + value="super-secret-value", + ) + + assert "super-secret-value" not in repr(secret) + assert "super-secret-value" not in str(secret) + assert secret.reveal() == "super-secret-value" + + +def test_audit_sink_preserves_security_correlation_without_secret_values() -> None: + async def exercise() -> None: + context = RunContext.local(revision_id=uuid4()) + event = AuditEvent.for_policy_decision( + kind=AuditKind.POLICY_DECISION, + action=Action("provider.invoke"), + resource=ResourceRef(kind="provider", id="openai-main"), + context=context, + decision=PolicyDecision( + effect=PolicyEffect.ALLOW, + reason="local trusted mode", + policy_id="local", + ), + attributes={"secret_ref": "env:OPENAI_API_KEY"}, + ) + sink = InMemoryAuditSink() + await sink.emit(event) + + assert sink.events == (event,) + assert event.request_id == context.request_id + assert event.trace_id == context.trace_context.trace_id + assert event.tenant_id == "local" + + asyncio.run(exercise()) + diff --git a/tests/session/test_arun_session_loop.py b/tests/session/test_arun_session_loop.py index e352061..7a1b5ab 100644 --- a/tests/session/test_arun_session_loop.py +++ b/tests/session/test_arun_session_loop.py @@ -21,7 +21,7 @@ from rath._async.aloop import _arun_session_loop from rath._async.runtime import runtime -from rath.backend import get +from rath.backend import BackendSandboxSpec, get from rath.flow.agent_param import AgentParam, Provider from rath.flow.tool import FlowToolCall from rath.llm import ( @@ -147,7 +147,7 @@ def test_arun_session_loop_stop_without_tools() -> None: def test_arun_session_loop_write_file_via_tool_then_stop(tmp_path: Any) -> None: body = { - "path": str(tmp_path / "_arun_probe.txt"), + "path": "_arun_probe.txt", "content": "ASYNC_LOOP_MARKER", } first = _tool_round( @@ -160,7 +160,7 @@ def test_arun_session_loop_write_file_via_tool_then_stop(tmp_path: Any) -> None: agent = AgentParam(Session.from_agent_prompt("scripted assistant"), Provider()) backend = get("local") - with backend.open() as sandbox: + with backend.open(BackendSandboxSpec(working_dir=str(tmp_path))) as sandbox: user = Session.from_user_message("Please write the file.").bind_sandbox(sandbox) out = runtime().run( _arun_session_loop( diff --git a/tests/unit/test_errors_v2.py b/tests/unit/test_errors_v2.py new file mode 100644 index 0000000..6f7db2e --- /dev/null +++ b/tests/unit/test_errors_v2.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from rath.errors import ErrorCode, RathError + + +def test_rath_error_has_stable_machine_contract() -> None: + error = RathError( + ErrorCode.INVALID_ARGUMENT, + "invalid input", + retryable=False, + details={"field": "name"}, + ) + + assert error.to_dict() == { + "code": "request.invalid_argument", + "message": "invalid input", + "retryable": False, + "details": {"field": "name"}, + } + + +def test_error_details_are_immutable_copies() -> None: + details = {"nested": {"value": 1}} + error = RathError(ErrorCode.INTERNAL, "internal", details=details) + details["nested"]["value"] = 2 + + assert error.details["nested"]["value"] == 1 # type: ignore[index] + From c740c627fff687e0f3547ec409aa522c7fbbcebe Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 11:44:40 +0800 Subject: [PATCH 03/22] feat(v2): add immutable events and workflow compiler --- src/rath/__init__.py | 3 +- src/rath/definition/__init__.py | 32 +++ src/rath/definition/compiler.py | 200 ++++++++++++++++++ src/rath/definition/decorators.py | 97 +++++++++ src/rath/definition/model.py | 215 ++++++++++++++++++++ src/rath/events.py | 148 ++++++++++++++ src/rath/flow/__init__.py | 5 + src/rath/flow/compile.py | 13 +- src/rath/flow/workflow.py | 22 +- tests/core/test_events.py | 69 +++++++ tests/definition/test_compiler_v2.py | 126 ++++++++++++ tests/definition/test_plan_serialization.py | 44 ++++ 12 files changed, 971 insertions(+), 3 deletions(-) create mode 100644 src/rath/definition/__init__.py create mode 100644 src/rath/definition/compiler.py create mode 100644 src/rath/definition/decorators.py create mode 100644 src/rath/definition/model.py create mode 100644 src/rath/events.py create mode 100644 tests/core/test_events.py create mode 100644 tests/definition/test_compiler_v2.py create mode 100644 tests/definition/test_plan_serialization.py diff --git a/src/rath/__init__.py b/src/rath/__init__.py index 887a94e..8682bdd 100644 --- a/src/rath/__init__.py +++ b/src/rath/__init__.py @@ -19,12 +19,13 @@ from rath import ( backend, + definition, flow, memory, security, ) -__all__ = ["backend", "flow", "memory", "security", "session"] +__all__ = ["backend", "definition", "flow", "memory", "security", "session"] def __getattr__(name: str) -> Any: diff --git a/src/rath/definition/__init__.py b/src/rath/definition/__init__.py new file mode 100644 index 0000000..e9a5002 --- /dev/null +++ b/src/rath/definition/__init__.py @@ -0,0 +1,32 @@ +"""Public workflow definition, compiler, and execution-plan contracts.""" + +from rath.definition.compiler import DefinitionError, WorkflowCompiler +from rath.definition.decorators import router, step +from rath.definition.model import ( + EdgeSpec, + EffectClass, + ExecutionPlan, + NodeKind, + NodeSpec, + ProviderResource, + ResourceManifestV2, + RetryPolicy, + WorkflowDefinition, +) + +__all__ = [ + "DefinitionError", + "EdgeSpec", + "EffectClass", + "ExecutionPlan", + "NodeKind", + "NodeSpec", + "ProviderResource", + "ResourceManifestV2", + "RetryPolicy", + "router", + "step", + "WorkflowCompiler", + "WorkflowDefinition", +] + diff --git a/src/rath/definition/compiler.py b/src/rath/definition/compiler.py new file mode 100644 index 0000000..06c3e73 --- /dev/null +++ b/src/rath/definition/compiler.py @@ -0,0 +1,200 @@ +"""Deterministic compiler from Python Workflow declarations to ExecutionPlan.""" + +from __future__ import annotations + +import hashlib +import inspect +import json +from collections.abc import Callable, Mapping +from uuid import NAMESPACE_URL, UUID, uuid5 + +from rath._json import JSONValue, thaw_json +from rath.definition.decorators import _metadata +from rath.definition.model import ( + EdgeSpec, + EffectClass, + ExecutionPlan, + NodeKind, + NodeSpec, + ProviderResource, + ResourceManifestV2, + RetryPolicy, + WorkflowDefinition, +) + +__all__ = ["DefinitionError", "WorkflowCompiler"] + + +class DefinitionError(ValueError): + """Workflow declaration cannot produce a safe deterministic plan.""" + + +class WorkflowCompiler: + """Compile explicit Python step boundaries without executing workflow code.""" + + def compile( + self, + workflow: object, + *, + revision_id: UUID, + input_schema: Mapping[str, JSONValue] | None = None, + state_schema: Mapping[str, JSONValue] | None = None, + policy_manifest: Mapping[str, JSONValue] | None = None, + ) -> ExecutionPlan: + name = f"{type(workflow).__module__}.{type(workflow).__qualname__}" + version = str(getattr(workflow, "workflow_version", "1")) + nodes, entrypoint, durable, issues = self._nodes(workflow) + self._validate(nodes, entrypoint) + edges = tuple( + EdgeSpec(source=node.id, target=target) + for node in nodes + for target in node.successors + ) + definition_payload = { + "name": name, + "version": version, + "entrypoint": entrypoint, + "nodes": [node.to_dict() for node in nodes], + "edges": [edge.to_dict() for edge in edges], + "input_schema": thaw_json(input_schema or {}), + "state_schema": thaw_json(state_schema or {}), + } + definition_json = json.dumps( + definition_payload, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + definition_hash = hashlib.sha256(definition_json.encode("utf-8")).hexdigest() + definition_id = uuid5(NAMESPACE_URL, f"openrath:definition:{definition_hash}") + definition = WorkflowDefinition( + id=definition_id, + name=name, + version=version, + entrypoint=entrypoint, + nodes=nodes, + edges=edges, + input_schema=input_schema or {}, + state_schema=state_schema or {}, + ) + resources = self._resources(workflow) + plan_seed = json.dumps( + { + "definition_hash": definition_hash, + "revision_id": str(revision_id), + "resources": resources.to_dict(), + "policy_manifest": thaw_json(policy_manifest or {}), + }, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + plan_id = uuid5(NAMESPACE_URL, f"openrath:plan:{plan_seed}") + return ExecutionPlan( + id=plan_id, + definition_hash=definition_hash, + revision_id=revision_id, + definition=definition, + nodes=nodes, + resources=resources, + policy_manifest=policy_manifest or {}, + durable=durable, + compatibility_issues=issues, + ) + + def _nodes( + self, + workflow: object, + ) -> tuple[tuple[NodeSpec, ...], str, bool, tuple[str, ...]]: + discovered: list[tuple[str, Callable[..., object]]] = [] + for name, function in inspect.getmembers(type(workflow), predicate=callable): + if _metadata(function) is not None: + discovered.append((name, function)) + if not discovered: + opaque = NodeSpec( + id="legacy.forward", + kind=NodeKind.OPAQUE, + handler=f"{type(workflow).__module__}.{type(workflow).__qualname__}.forward", + is_async=inspect.iscoroutinefunction(getattr(workflow, "forward", None)), + retry=RetryPolicy(), + effects=EffectClass.NON_IDEMPOTENT, + checkpoint=False, + ) + return ( + (opaque,), + opaque.id, + False, + ( + "legacy forward() is opaque and cannot resume across checkpoint boundaries", + ), + ) + + nodes: list[NodeSpec] = [] + entries: list[str] = [] + for name, function in discovered: + metadata = _metadata(function) + assert metadata is not None + if metadata.entry: + entries.append(name) + nodes.append( + NodeSpec( + id=name, + kind=metadata.kind, + handler=f"{type(workflow).__module__}.{type(workflow).__qualname__}.{name}", + is_async=inspect.iscoroutinefunction(function), + retry=metadata.retry, + effects=metadata.effects, + idempotency_key=metadata.idempotency_key, + timeout_seconds=metadata.timeout_seconds, + checkpoint=metadata.checkpoint, + successors=metadata.successors, + ) + ) + if len(entries) != 1: + raise DefinitionError( + f"workflow must declare exactly one entrypoint; found {len(entries)}" + ) + return tuple(nodes), entries[0], True, () + + def _validate(self, nodes: tuple[NodeSpec, ...], entrypoint: str) -> None: + ids = {node.id for node in nodes} + if len(ids) != len(nodes): + raise DefinitionError("workflow node ids must be unique") + for node in nodes: + for successor in node.successors: + if successor not in ids: + raise DefinitionError( + f"node {node.id!r} references unknown successor {successor!r}" + ) + if node.kind is NodeKind.ROUTER and not node.successors: + raise DefinitionError(f"router {node.id!r} requires successors") + + reachable: set[str] = set() + pending = [entrypoint] + by_id = {node.id: node for node in nodes} + while pending: + current = pending.pop() + if current in reachable: + continue + reachable.add(current) + pending.extend(by_id[current].successors) + unreachable = sorted(ids - reachable) + if unreachable: + raise DefinitionError(f"unreachable workflow nodes: {unreachable}") + + def _resources(self, workflow: object) -> ResourceManifestV2: + providers: list[ProviderResource] = [] + named_agents = getattr(workflow, "named_agents", None) + if callable(named_agents): + for path, agent in named_agents(): + provider = agent.provider + providers.append( + ProviderResource( + path=path, + provider_kind=provider.provider_kind or "openai", + model=provider.model, + has_memory=agent.memory is not None, + ) + ) + return ResourceManifestV2(providers=tuple(providers)) + diff --git a/src/rath/definition/decorators.py b/src/rath/definition/decorators.py new file mode 100644 index 0000000..d178fc9 --- /dev/null +++ b/src/rath/definition/decorators.py @@ -0,0 +1,97 @@ +"""Explicit durable step and router boundaries for Python workflows.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any, TypeVar, cast + +from rath.definition.model import EffectClass, NodeKind, RetryPolicy + +__all__ = ["router", "step"] + +F = TypeVar("F", bound=Callable[..., Any]) +_METADATA_ATTR = "__openrath_node_spec__" + + +@dataclass(frozen=True, slots=True) +class _NodeMetadata: + kind: NodeKind + entry: bool + successors: tuple[str, ...] + retry: RetryPolicy = field(default_factory=RetryPolicy) + effects: EffectClass = EffectClass.NON_IDEMPOTENT + idempotency_key: str | None = None + timeout_seconds: float | None = None + checkpoint: bool = True + + +def _decorate(function: F, metadata: _NodeMetadata) -> F: + if hasattr(function, _METADATA_ATTR): + raise ValueError(f"{function.__qualname__} already has OpenRath node metadata") + if ( + metadata.effects is EffectClass.NON_IDEMPOTENT + and metadata.retry.max_attempts > 1 + and not metadata.idempotency_key + ): + raise ValueError("non-idempotent retries require idempotency_key") + setattr(function, _METADATA_ATTR, metadata) + return function + + +def step( + *, + entry: bool = False, + successors: tuple[str, ...] = (), + retry: RetryPolicy | None = None, + effects: EffectClass = EffectClass.NON_IDEMPOTENT, + idempotency_key: str | None = None, + timeout_seconds: float | None = None, + checkpoint: bool = True, +) -> Callable[[F], F]: + """Mark a method as a checkpointable execution step.""" + + def decorator(function: F) -> F: + return _decorate( + function, + _NodeMetadata( + kind=NodeKind.STEP, + entry=entry, + successors=tuple(successors), + retry=retry or RetryPolicy(), + effects=effects, + idempotency_key=idempotency_key, + timeout_seconds=timeout_seconds, + checkpoint=checkpoint, + ), + ) + + return decorator + + +def router( + *, + successors: tuple[str, ...], + entry: bool = False, +) -> Callable[[F], F]: + """Mark a pure routing method with an explicit successor allowlist.""" + if not successors: + raise ValueError("router successors must not be empty") + + def decorator(function: F) -> F: + return _decorate( + function, + _NodeMetadata( + kind=NodeKind.ROUTER, + entry=entry, + successors=tuple(successors), + effects=EffectClass.NONE, + ), + ) + + return decorator + + +def _metadata(function: Callable[..., Any]) -> _NodeMetadata | None: + return cast(_NodeMetadata | None, getattr(function, _METADATA_ATTR, None)) + diff --git a/src/rath/definition/model.py b/src/rath/definition/model.py new file mode 100644 index 0000000..ebdd3bc --- /dev/null +++ b/src/rath/definition/model.py @@ -0,0 +1,215 @@ +"""Versioned workflow-definition and executable-plan value objects.""" + +from __future__ import annotations + +import json +from collections.abc import Mapping +from dataclasses import dataclass, field +from enum import Enum +from uuid import UUID + +from rath._json import JSONValue, freeze_mapping, thaw_json + +__all__ = [ + "EdgeSpec", + "EffectClass", + "ExecutionPlan", + "NodeKind", + "NodeSpec", + "ProviderResource", + "ResourceManifestV2", + "RetryPolicy", + "WorkflowDefinition", +] + + +class EffectClass(str, Enum): + NONE = "none" + READ_ONLY = "read_only" + IDEMPOTENT = "idempotent" + NON_IDEMPOTENT = "non_idempotent" + + +class NodeKind(str, Enum): + STEP = "step" + ROUTER = "router" + OPAQUE = "opaque" + + +@dataclass(frozen=True, slots=True) +class RetryPolicy: + max_attempts: int = 1 + base_seconds: float = 0.25 + max_seconds: float = 30.0 + + def __post_init__(self) -> None: + if self.max_attempts < 1: + raise ValueError("max_attempts must be at least 1") + if self.base_seconds <= 0: + raise ValueError("base_seconds must be greater than zero") + if self.max_seconds < self.base_seconds: + raise ValueError("max_seconds must be greater than or equal to base_seconds") + + def to_dict(self) -> dict[str, object]: + return { + "max_attempts": self.max_attempts, + "base_seconds": self.base_seconds, + "max_seconds": self.max_seconds, + } + + +@dataclass(frozen=True, slots=True) +class NodeSpec: + id: str + kind: NodeKind + handler: str + is_async: bool + retry: RetryPolicy = field(default_factory=RetryPolicy) + effects: EffectClass = EffectClass.NON_IDEMPOTENT + idempotency_key: str | None = None + timeout_seconds: float | None = None + checkpoint: bool = True + successors: tuple[str, ...] = () + + def __post_init__(self) -> None: + if not self.id.strip(): + raise ValueError("node id must not be empty") + if not self.handler.strip(): + raise ValueError("node handler must not be empty") + if self.timeout_seconds is not None and self.timeout_seconds <= 0: + raise ValueError("node timeout_seconds must be greater than zero") + if ( + self.effects is EffectClass.NON_IDEMPOTENT + and self.retry.max_attempts > 1 + and not self.idempotency_key + ): + raise ValueError( + "non-idempotent retries require a stable idempotency key" + ) + + def to_dict(self) -> dict[str, object]: + return { + "id": self.id, + "kind": self.kind.value, + "handler": self.handler, + "is_async": self.is_async, + "retry": self.retry.to_dict(), + "effects": self.effects.value, + "idempotency_key": self.idempotency_key, + "timeout_seconds": self.timeout_seconds, + "checkpoint": self.checkpoint, + "successors": list(self.successors), + } + + +@dataclass(frozen=True, slots=True) +class EdgeSpec: + source: str + target: str + + def to_dict(self) -> dict[str, str]: + return {"source": self.source, "target": self.target} + + +@dataclass(frozen=True, slots=True) +class WorkflowDefinition: + id: UUID + name: str + version: str + entrypoint: str + nodes: tuple[NodeSpec, ...] + edges: tuple[EdgeSpec, ...] + input_schema: Mapping[str, JSONValue] = field(default_factory=dict) + state_schema: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "input_schema", + freeze_mapping(self.input_schema, field="definition.input_schema"), + ) + object.__setattr__( + self, + "state_schema", + freeze_mapping(self.state_schema, field="definition.state_schema"), + ) + + def to_dict(self) -> dict[str, object]: + return { + "id": str(self.id), + "name": self.name, + "version": self.version, + "entrypoint": self.entrypoint, + "nodes": [node.to_dict() for node in self.nodes], + "edges": [edge.to_dict() for edge in self.edges], + "input_schema": thaw_json(self.input_schema), + "state_schema": thaw_json(self.state_schema), + } + + +@dataclass(frozen=True, slots=True) +class ProviderResource: + path: str + provider_kind: str + model: str | None + has_memory: bool + + def to_dict(self) -> dict[str, object]: + return { + "path": self.path, + "provider_kind": self.provider_kind, + "model": self.model, + "has_memory": self.has_memory, + } + + +@dataclass(frozen=True, slots=True) +class ResourceManifestV2: + providers: tuple[ProviderResource, ...] = () + + def to_dict(self) -> dict[str, object]: + return { + "providers": [provider.to_dict() for provider in self.providers], + } + + +@dataclass(frozen=True, slots=True) +class ExecutionPlan: + id: UUID + definition_hash: str + revision_id: UUID + definition: WorkflowDefinition + nodes: tuple[NodeSpec, ...] + resources: ResourceManifestV2 + policy_manifest: Mapping[str, JSONValue] + durable: bool + compatibility_issues: tuple[str, ...] = () + + def __post_init__(self) -> None: + object.__setattr__( + self, + "policy_manifest", + freeze_mapping(self.policy_manifest, field="plan.policy_manifest"), + ) + + def to_dict(self) -> dict[str, object]: + return { + "id": str(self.id), + "definition_hash": self.definition_hash, + "revision_id": str(self.revision_id), + "definition": self.definition.to_dict(), + "nodes": [node.to_dict() for node in self.nodes], + "resources": self.resources.to_dict(), + "policy_manifest": thaw_json(self.policy_manifest), + "durable": self.durable, + "compatibility_issues": list(self.compatibility_issues), + } + + def canonical_json(self) -> str: + return json.dumps( + self.to_dict(), + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + diff --git a/src/rath/events.py b/src/rath/events.py new file mode 100644 index 0000000..ce1cd5e --- /dev/null +++ b/src/rath/events.py @@ -0,0 +1,148 @@ +"""Immutable session event and lineage-friendly event-log contracts.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from uuid import UUID, uuid4 + +from rath._json import JSONValue, freeze_mapping +from rath.context import TraceContext +from rath.security import Provenance, TrustLevel + +__all__ = [ + "Event", + "EventKind", + "ProducerRef", + "SessionEventLog", +] + + +class EventKind(str, Enum): + MESSAGE_CREATED = "session.message.created" + SESSION_FORKED = "session.forked" + SESSION_MERGED = "session.merged" + RUN_STATE_CHANGED = "run.state.changed" + NODE_STARTED = "run.node.started" + NODE_COMPLETED = "run.node.completed" + OUTPUT_DELTA = "run.output.delta" + INTERRUPT_CREATED = "run.interrupt.created" + TOOL_INVOCATION_CHANGED = "run.tool_invocation.changed" + + +@dataclass(frozen=True, slots=True) +class ProducerRef: + kind: str + id: str + revision_id: UUID | None = None + + def __post_init__(self) -> None: + if not self.kind.strip(): + raise ValueError("producer kind must not be empty") + if not self.id.strip(): + raise ValueError("producer id must not be empty") + + +@dataclass(frozen=True, slots=True) +class Event: + """Deeply immutable event ordered within one Session.""" + + id: UUID + session_id: UUID + sequence: int + kind: EventKind + payload: Mapping[str, JSONValue] + producer: ProducerRef + trust: TrustLevel + provenance: Provenance + created_at: datetime + trace_context: TraceContext | None = None + schema_version: int = 1 + + def __post_init__(self) -> None: + if self.sequence < 1: + raise ValueError("event sequence must be greater than zero") + if self.schema_version < 1: + raise ValueError("event schema_version must be greater than zero") + if self.created_at.tzinfo is None: + raise ValueError("event created_at must be timezone-aware") + object.__setattr__( + self, + "payload", + freeze_mapping(self.payload, field="event.payload"), + ) + + @classmethod + def create( + cls, + *, + session_id: UUID, + sequence: int, + kind: EventKind, + payload: Mapping[str, object], + producer: ProducerRef, + trust: TrustLevel, + provenance: Provenance, + trace_context: TraceContext | None = None, + ) -> "Event": + return cls( + id=uuid4(), + session_id=session_id, + sequence=sequence, + kind=kind, + payload=freeze_mapping(payload, field="event.payload"), + producer=producer, + trust=trust, + provenance=provenance, + created_at=datetime.now(timezone.utc), + trace_context=trace_context, + ) + + +@dataclass(frozen=True, slots=True) +class SessionEventLog: + """Immutable ordered Event view; live runtime state is intentionally absent.""" + + id: UUID = field(default_factory=uuid4) + events: tuple[Event, ...] = () + parent_session_ids: tuple[UUID, ...] = () + + def __post_init__(self) -> None: + expected = 1 + for event in self.events: + if event.session_id != self.id: + raise ValueError("event session_id does not match event log id") + if event.sequence != expected: + raise ValueError("event sequence must be contiguous and start at 1") + expected += 1 + if self.id in self.parent_session_ids: + raise ValueError("session cannot be its own lineage parent") + + def append( + self, + *, + kind: EventKind, + payload: Mapping[str, object], + producer: ProducerRef, + trust: TrustLevel, + provenance: Provenance, + trace_context: TraceContext | None = None, + ) -> "SessionEventLog": + event = Event.create( + session_id=self.id, + sequence=len(self.events) + 1, + kind=kind, + payload=payload, + producer=producer, + trust=trust, + provenance=provenance, + trace_context=trace_context, + ) + return SessionEventLog( + id=self.id, + events=(*self.events, event), + parent_session_ids=self.parent_session_ids, + ) + diff --git a/src/rath/flow/__init__.py b/src/rath/flow/__init__.py index 09506c5..042f3ab 100644 --- a/src/rath/flow/__init__.py +++ b/src/rath/flow/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from rath.definition import EffectClass, RetryPolicy, router, step from rath.flow.agent import Agent from rath.flow.agent_param import AgentParam, Provider from rath.flow.compile import CompiledWorkflow, ResourceManifest @@ -20,4 +21,8 @@ "Selector", "CompiledWorkflow", "ResourceManifest", + "EffectClass", + "RetryPolicy", + "router", + "step", ] diff --git a/src/rath/flow/compile.py b/src/rath/flow/compile.py index bf688a9..07bf9b5 100644 --- a/src/rath/flow/compile.py +++ b/src/rath/flow/compile.py @@ -17,6 +17,7 @@ from collections.abc import Iterator from dataclasses import dataclass, field from typing import TYPE_CHECKING +from uuid import NAMESPACE_URL, uuid5 if TYPE_CHECKING: from rath.flow.agent_param import AgentParam @@ -128,11 +129,21 @@ class CompiledWorkflow: static module tree (P5.1) to build the manifest. """ - __slots__ = ("workflow", "manifest", "_acquired") + __slots__ = ("workflow", "manifest", "execution_plan", "_acquired") def __init__(self, workflow: "Workflow") -> None: + from rath.definition import WorkflowCompiler + self.workflow = workflow self.manifest = collect_manifest(workflow) + revision_id = uuid5( + NAMESPACE_URL, + f"openrath:embedded-revision:{type(workflow).__module__}.{type(workflow).__qualname__}", + ) + self.execution_plan = WorkflowCompiler().compile( + workflow, + revision_id=revision_id, + ) self._acquired: list[MemoryStore] = [] # stores acquired by __enter__ def __call__(self, session): # type: ignore[no-untyped-def] diff --git a/src/rath/flow/workflow.py b/src/rath/flow/workflow.py index b54d50f..4c0df16 100644 --- a/src/rath/flow/workflow.py +++ b/src/rath/flow/workflow.py @@ -2,7 +2,11 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any +from uuid import UUID + +if TYPE_CHECKING: + from rath.definition import ExecutionPlan from rath.flow.agent_param import AgentParam from rath.llm.provider import Provider @@ -103,6 +107,22 @@ def compile(self) -> "object": return CompiledWorkflow(self) + def compile_plan( + self, + *, + revision_id: UUID, + ) -> "ExecutionPlan": + """Compile explicit ``@step`` boundaries into an immutable v2 plan.""" + from rath.definition import WorkflowCompiler + + return WorkflowCompiler().compile(self, revision_id=revision_id) + + def inspect_resources(self) -> "object": + """Return the v1 static resource inventory without compiling a v2 plan.""" + from rath.flow.compile import collect_manifest + + return collect_manifest(self) + def forward(self, session: Session) -> Session: """Subclasses orchestrate Sessions (blocking).""" diff --git a/tests/core/test_events.py b/tests/core/test_events.py new file mode 100644 index 0000000..51b37d5 --- /dev/null +++ b/tests/core/test_events.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import uuid4 + +import pytest + +from rath.events import Event, EventKind, ProducerRef, SessionEventLog +from rath.security import Provenance, TrustLevel + + +def _event(*, session_id, sequence: int, payload=None) -> Event: # type: ignore[no-untyped-def] + return Event.create( + session_id=session_id, + sequence=sequence, + kind=EventKind.MESSAGE_CREATED, + payload=payload or {"content": "hello"}, + producer=ProducerRef(kind="user", id="user-1"), + trust=TrustLevel.UNTRUSTED, + provenance=Provenance(source_type="user", source_id="user-1"), + ) + + +def test_event_payload_is_deeply_immutable() -> None: + session_id = uuid4() + payload = {"parts": [{"text": "hello"}]} + event = _event(session_id=session_id, sequence=1, payload=payload) + + payload["parts"][0]["text"] = "changed" + + assert event.payload["parts"][0]["text"] == "hello" # type: ignore[index] + with pytest.raises(TypeError): + event.payload["new"] = True # type: ignore[index] + assert event.created_at.tzinfo is not None + + +def test_session_event_log_requires_monotonic_contiguous_sequence() -> None: + session_id = uuid4() + first = _event(session_id=session_id, sequence=1) + third = _event(session_id=session_id, sequence=3) + + with pytest.raises(ValueError, match="contiguous"): + SessionEventLog(id=session_id, events=(first, third)) + + +def test_session_event_log_rejects_cross_session_event() -> None: + session_id = uuid4() + with pytest.raises(ValueError, match="session_id"): + SessionEventLog( + id=session_id, + events=(_event(session_id=uuid4(), sequence=1),), + ) + + +def test_append_assigns_next_sequence_without_mutating_original() -> None: + session_id = uuid4() + log = SessionEventLog(id=session_id) + updated = log.append( + kind=EventKind.MESSAGE_CREATED, + payload={"content": "hello"}, + producer=ProducerRef(kind="user", id="user-1"), + trust=TrustLevel.UNTRUSTED, + provenance=Provenance(source_type="user", source_id="user-1"), + ) + + assert log.events == () + assert updated.events[0].sequence == 1 + assert isinstance(updated.events[0].created_at, datetime) + diff --git a/tests/definition/test_compiler_v2.py b/tests/definition/test_compiler_v2.py new file mode 100644 index 0000000..8d6dd78 --- /dev/null +++ b/tests/definition/test_compiler_v2.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from uuid import uuid4 + +import pytest + +from rath.definition import ( + DefinitionError, + EffectClass, + NodeKind, + RetryPolicy, + WorkflowCompiler, + router, + step, +) +from rath.flow import Workflow +from rath.session import Session + + +class _Research(Workflow): + @step( + entry=True, + successors=("route",), + retry=RetryPolicy(max_attempts=3), + effects=EffectClass.READ_ONLY, + ) + async def search(self, state, context): # type: ignore[no-untyped-def] + return state + + @router(successors=("write", "review")) + def route(self, state): # type: ignore[no-untyped-def] + return "write" + + @step() + def review(self, state, context): # type: ignore[no-untyped-def] + return state + + @step() + def write(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + +def test_compiler_produces_deterministic_immutable_plan() -> None: + revision_id = uuid4() + first = WorkflowCompiler().compile(_Research(), revision_id=revision_id) + second = WorkflowCompiler().compile(_Research(), revision_id=revision_id) + + assert first.id == second.id + assert first.definition_hash == second.definition_hash + assert first.revision_id == revision_id + assert tuple(node.id for node in first.nodes) == ( + "review", + "route", + "search", + "write", + ) + route_node = next(node for node in first.nodes if node.id == "route") + assert route_node.kind is NodeKind.ROUTER + assert route_node.successors == ("write", "review") + search_node = next(node for node in first.nodes if node.id == "search") + assert search_node.retry.max_attempts == 3 + assert search_node.effects is EffectClass.READ_ONLY + assert first.durable is True + + +def test_workflow_compile_plan_is_public_convenience_api() -> None: + plan = _Research().compile_plan(revision_id=uuid4()) + assert plan.definition.entrypoint == "search" + assert plan.definition.version == "1" + + +def test_unknown_router_successor_fails_compile() -> None: + class _Broken(Workflow): + @router(entry=True, successors=("missing",)) + def route(self, state): # type: ignore[no-untyped-def] + return "missing" + + def forward(self, session: Session) -> Session: + return session + + with pytest.raises(DefinitionError, match="unknown successor"): + WorkflowCompiler().compile(_Broken(), revision_id=uuid4()) + + +def test_multiple_entry_steps_fail_compile() -> None: + class _Broken(Workflow): + @step(entry=True) + def one(self, state, context): # type: ignore[no-untyped-def] + return state + + @step(entry=True) + def two(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + with pytest.raises(DefinitionError, match="exactly one entrypoint"): + WorkflowCompiler().compile(_Broken(), revision_id=uuid4()) + + +def test_legacy_workflow_compiles_as_opaque_non_durable_plan() -> None: + class _Legacy(Workflow): + def forward(self, session: Session) -> Session: + return session + + plan = WorkflowCompiler().compile(_Legacy(), revision_id=uuid4()) + + assert plan.durable is False + assert plan.nodes[0].kind is NodeKind.OPAQUE + assert any("checkpoint" in issue for issue in plan.compatibility_issues) + + +def test_step_metadata_rejects_unsafe_retry_contract() -> None: + with pytest.raises(ValueError, match="idempotency"): + + @step( + retry=RetryPolicy(max_attempts=2), + effects=EffectClass.NON_IDEMPOTENT, + ) + def unsafe(state, context): # type: ignore[no-untyped-def] + return state + diff --git a/tests/definition/test_plan_serialization.py b/tests/definition/test_plan_serialization.py new file mode 100644 index 0000000..c85323f --- /dev/null +++ b/tests/definition/test_plan_serialization.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import json +from uuid import uuid4 + +from rath.definition import EffectClass, WorkflowCompiler, step +from rath.flow import AgentParam, Workflow +from rath.llm import Provider +from rath.session import Session + + +class _WithSecretProvider(Workflow): + def __init__(self) -> None: + super().__init__() + self.agent = AgentParam( + Session.from_agent_prompt("system"), + Provider(model="model", api_key="must-not-leak"), + ) + + @step(entry=True, effects=EffectClass.READ_ONLY) + def execute(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + +def test_canonical_plan_never_serializes_provider_secret_values() -> None: + plan = WorkflowCompiler().compile(_WithSecretProvider(), revision_id=uuid4()) + encoded = plan.canonical_json() + + assert "must-not-leak" not in encoded + payload = json.loads(encoded) + assert payload["resources"]["providers"][0]["model"] == "model" + assert "api_key" not in payload["resources"]["providers"][0] + + +def test_existing_compiled_workflow_exposes_v2_execution_plan() -> None: + workflow = _WithSecretProvider() + compiled = workflow.compile() + + assert compiled.execution_plan.definition_hash + assert compiled.execution_plan.durable is True + From 86b086693a739b96db4051abf1b2550c83d05dc8 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 11:50:29 +0800 Subject: [PATCH 04/22] feat(v2): add durable run state and sqlite store --- src/rath/__init__.py | 11 +- src/rath/runtime/__init__.py | 33 ++ src/rath/runtime/models.py | 324 +++++++++++++ src/rath/runtime/sqlite.py | 636 +++++++++++++++++++++++++ src/rath/runtime/store.py | 63 +++ tests/runtime/test_run_state.py | 61 +++ tests/runtime/test_sqlite_run_store.py | 167 +++++++ 7 files changed, 1294 insertions(+), 1 deletion(-) create mode 100644 src/rath/runtime/__init__.py create mode 100644 src/rath/runtime/models.py create mode 100644 src/rath/runtime/sqlite.py create mode 100644 src/rath/runtime/store.py create mode 100644 tests/runtime/test_run_state.py create mode 100644 tests/runtime/test_sqlite_run_store.py diff --git a/src/rath/__init__.py b/src/rath/__init__.py index 8682bdd..c4ea9c3 100644 --- a/src/rath/__init__.py +++ b/src/rath/__init__.py @@ -22,10 +22,19 @@ definition, flow, memory, + runtime, security, ) -__all__ = ["backend", "definition", "flow", "memory", "security", "session"] +__all__ = [ + "backend", + "definition", + "flow", + "memory", + "runtime", + "security", + "session", +] def __getattr__(name: str) -> Any: diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py new file mode 100644 index 0000000..ecb3702 --- /dev/null +++ b/src/rath/runtime/__init__.py @@ -0,0 +1,33 @@ +"""Public durable runtime state and persistence contracts.""" + +from rath.runtime.models import ( + ApprovalDecision, + ApprovalDecisionKind, + Checkpoint, + ConflictError, + Interrupt, + InterruptKind, + InvalidRunTransition, + Run, + RunEvent, + RunStatus, + assert_transition, +) +from rath.runtime.sqlite import SQLiteRunStore +from rath.runtime.store import RunStore + +__all__ = [ + "ApprovalDecision", + "ApprovalDecisionKind", + "assert_transition", + "Checkpoint", + "ConflictError", + "Interrupt", + "InterruptKind", + "InvalidRunTransition", + "Run", + "RunEvent", + "RunStatus", + "RunStore", + "SQLiteRunStore", +] diff --git a/src/rath/runtime/models.py b/src/rath/runtime/models.py new file mode 100644 index 0000000..a01414c --- /dev/null +++ b/src/rath/runtime/models.py @@ -0,0 +1,324 @@ +"""Durable Run, Checkpoint, Interrupt, and event state models.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from uuid import UUID, uuid4 + +from rath._json import JSONValue, freeze_mapping +from rath.errors import ErrorCode, RathError + +__all__ = [ + "ApprovalDecision", + "ApprovalDecisionKind", + "Checkpoint", + "ConflictError", + "Interrupt", + "InterruptKind", + "InvalidRunTransition", + "Run", + "RunEvent", + "RunStatus", + "assert_transition", +] + + +class RunStatus(str, Enum): + QUEUED = "queued" + RUNNING = "running" + WAITING = "waiting" + SUCCEEDED = "succeeded" + FAILED = "failed" + CANCELLED = "cancelled" + TIMED_OUT = "timed_out" + NEEDS_REVIEW = "needs_review" + + +TERMINAL_RUN_STATUSES = frozenset( + { + RunStatus.SUCCEEDED, + RunStatus.FAILED, + RunStatus.CANCELLED, + RunStatus.TIMED_OUT, + } +) + +_TRANSITIONS: Mapping[RunStatus, frozenset[RunStatus]] = { + RunStatus.QUEUED: frozenset( + { + RunStatus.RUNNING, + RunStatus.CANCELLED, + RunStatus.TIMED_OUT, + } + ), + RunStatus.RUNNING: frozenset( + { + RunStatus.QUEUED, + RunStatus.WAITING, + RunStatus.SUCCEEDED, + RunStatus.FAILED, + RunStatus.CANCELLED, + RunStatus.TIMED_OUT, + RunStatus.NEEDS_REVIEW, + } + ), + RunStatus.WAITING: frozenset( + { + RunStatus.QUEUED, + RunStatus.CANCELLED, + RunStatus.TIMED_OUT, + RunStatus.FAILED, + } + ), + RunStatus.NEEDS_REVIEW: frozenset( + { + RunStatus.QUEUED, + RunStatus.FAILED, + RunStatus.CANCELLED, + } + ), + RunStatus.SUCCEEDED: frozenset(), + RunStatus.FAILED: frozenset(), + RunStatus.CANCELLED: frozenset(), + RunStatus.TIMED_OUT: frozenset(), +} + + +class ConflictError(RathError): + def __init__(self, message: str, *, details: Mapping[str, object] | None = None): + super().__init__( + ErrorCode.CONFLICT, + message, + retryable=False, + details=details, + ) + + +class InvalidRunTransition(ConflictError): + def __init__(self, source: RunStatus, target: RunStatus) -> None: + super().__init__( + f"invalid run transition from {source.value!r} to {target.value!r}", + details={"source": source.value, "target": target.value}, + ) + self.source = source + self.target = target + + +def assert_transition(source: RunStatus, target: RunStatus) -> None: + if target not in _TRANSITIONS[source]: + raise InvalidRunTransition(source, target) + + +def _aware(value: datetime, *, field_name: str) -> None: + if value.tzinfo is None: + raise ValueError(f"{field_name} must be timezone-aware") + + +@dataclass(frozen=True, slots=True) +class Run: + id: UUID + plan_id: UUID + revision_id: UUID + session_id: UUID + tenant_id: str + status: RunStatus + state: Mapping[str, JSONValue] + next_nodes: tuple[str, ...] + created_at: datetime + updated_at: datetime + version: int = 0 + idempotency_key: str | None = None + + def __post_init__(self) -> None: + if not self.tenant_id.strip(): + raise ValueError("run tenant_id must not be empty") + if self.version < 0: + raise ValueError("run version must not be negative") + _aware(self.created_at, field_name="run.created_at") + _aware(self.updated_at, field_name="run.updated_at") + object.__setattr__(self, "state", freeze_mapping(self.state, field="run.state")) + object.__setattr__(self, "next_nodes", tuple(self.next_nodes)) + + @classmethod + def create( + cls, + *, + plan_id: UUID, + revision_id: UUID, + session_id: UUID, + tenant_id: str, + status: RunStatus = RunStatus.QUEUED, + state: Mapping[str, object] | None = None, + next_nodes: tuple[str, ...] = (), + idempotency_key: str | None = None, + id: UUID | None = None, + ) -> "Run": + now = datetime.now(timezone.utc) + return cls( + id=id or uuid4(), + plan_id=plan_id, + revision_id=revision_id, + session_id=session_id, + tenant_id=tenant_id, + status=status, + state=freeze_mapping(state, field="run.state"), + next_nodes=next_nodes, + idempotency_key=idempotency_key, + created_at=now, + updated_at=now, + ) + + +@dataclass(frozen=True, slots=True) +class RunEvent: + run_id: UUID + sequence: int + type: str + data: Mapping[str, JSONValue] + created_at: datetime + + def __post_init__(self) -> None: + if self.sequence < 1: + raise ValueError("run event sequence must be greater than zero") + _aware(self.created_at, field_name="run_event.created_at") + object.__setattr__( + self, + "data", + freeze_mapping(self.data, field="run_event.data"), + ) + + +@dataclass(frozen=True, slots=True) +class Checkpoint: + id: UUID + run_id: UUID + sequence: int + plan_hash: str + state: Mapping[str, JSONValue] + next_nodes: tuple[str, ...] + pending_interrupts: tuple[UUID, ...] + effect_watermark: int + created_at: datetime + + def __post_init__(self) -> None: + if self.sequence < 1: + raise ValueError("checkpoint sequence must be greater than zero") + if self.effect_watermark < 0: + raise ValueError("effect_watermark must not be negative") + if len(self.plan_hash) != 64: + raise ValueError("plan_hash must be a SHA-256 hexadecimal digest") + try: + int(self.plan_hash, 16) + except ValueError as exc: + raise ValueError("plan_hash must be hexadecimal") from exc + _aware(self.created_at, field_name="checkpoint.created_at") + object.__setattr__( + self, + "state", + freeze_mapping(self.state, field="checkpoint.state"), + ) + object.__setattr__(self, "next_nodes", tuple(self.next_nodes)) + object.__setattr__( + self, + "pending_interrupts", + tuple(self.pending_interrupts), + ) + + @classmethod + def create( + cls, + *, + run_id: UUID, + sequence: int, + plan_hash: str, + state: Mapping[str, object], + next_nodes: tuple[str, ...], + effect_watermark: int, + pending_interrupts: tuple[UUID, ...] = (), + ) -> "Checkpoint": + return cls( + id=uuid4(), + run_id=run_id, + sequence=sequence, + plan_hash=plan_hash, + state=freeze_mapping(state, field="checkpoint.state"), + next_nodes=next_nodes, + pending_interrupts=pending_interrupts, + effect_watermark=effect_watermark, + created_at=datetime.now(timezone.utc), + ) + + +class InterruptKind(str, Enum): + APPROVAL = "approval" + INPUT = "input" + REVIEW = "review" + + +class ApprovalDecisionKind(str, Enum): + APPROVE = "approve" + EDIT = "edit" + REJECT = "reject" + RESPOND = "respond" + + +@dataclass(frozen=True, slots=True) +class ApprovalDecision: + kind: ApprovalDecisionKind + actor_id: str + reason: str + payload: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.actor_id.strip(): + raise ValueError("decision actor_id must not be empty") + if not self.reason.strip(): + raise ValueError("decision reason must not be empty") + object.__setattr__( + self, + "payload", + freeze_mapping(self.payload, field="decision.payload"), + ) + + +@dataclass(frozen=True, slots=True) +class Interrupt: + id: UUID + run_id: UUID + kind: InterruptKind + request: Mapping[str, JSONValue] + created_at: datetime + decision: ApprovalDecision | None = None + decided_at: datetime | None = None + + def __post_init__(self) -> None: + _aware(self.created_at, field_name="interrupt.created_at") + if self.decided_at is not None: + _aware(self.decided_at, field_name="interrupt.decided_at") + if (self.decision is None) != (self.decided_at is None): + raise ValueError("decision and decided_at must be set together") + object.__setattr__( + self, + "request", + freeze_mapping(self.request, field="interrupt.request"), + ) + + @classmethod + def create( + cls, + *, + run_id: UUID, + kind: InterruptKind, + request: Mapping[str, object], + ) -> "Interrupt": + return cls( + id=uuid4(), + run_id=run_id, + kind=kind, + request=freeze_mapping(request, field="interrupt.request"), + created_at=datetime.now(timezone.utc), + ) + diff --git a/src/rath/runtime/sqlite.py b/src/rath/runtime/sqlite.py new file mode 100644 index 0000000..cb144ac --- /dev/null +++ b/src/rath/runtime/sqlite.py @@ -0,0 +1,636 @@ +"""Transactional SQLite reference store for embedded durable execution.""" + +from __future__ import annotations + +import hashlib +import json +import sqlite3 +import threading +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from datetime import datetime, timezone +from pathlib import Path +from typing import cast +from uuid import UUID + +from rath._json import freeze_json, thaw_json +from rath.runtime.models import ( + ApprovalDecision, + ApprovalDecisionKind, + Checkpoint, + ConflictError, + Interrupt, + InterruptKind, + Run, + RunEvent, + RunStatus, + assert_transition, +) + +__all__ = ["SQLiteRunStore"] + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + applied_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS runs ( + id TEXT PRIMARY KEY, + plan_id TEXT NOT NULL, + revision_id TEXT NOT NULL, + session_id TEXT NOT NULL, + tenant_id TEXT NOT NULL, + status TEXT NOT NULL, + state_json TEXT NOT NULL, + next_nodes_json TEXT NOT NULL, + idempotency_key TEXT, + request_fingerprint TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + version INTEGER NOT NULL, + UNIQUE (tenant_id, idempotency_key) +); + +CREATE INDEX IF NOT EXISTS runs_tenant_status_idx + ON runs (tenant_id, status, created_at); + +CREATE TABLE IF NOT EXISTS run_events ( + run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + sequence INTEGER NOT NULL, + type TEXT NOT NULL, + data_json TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (run_id, sequence) +); + +CREATE TABLE IF NOT EXISTS checkpoints ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + sequence INTEGER NOT NULL, + plan_hash TEXT NOT NULL, + state_json TEXT NOT NULL, + next_nodes_json TEXT NOT NULL, + pending_interrupts_json TEXT NOT NULL, + effect_watermark INTEGER NOT NULL, + created_at TEXT NOT NULL, + UNIQUE (run_id, sequence) +); + +CREATE TABLE IF NOT EXISTS interrupts ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + kind TEXT NOT NULL, + request_json TEXT NOT NULL, + created_at TEXT NOT NULL, + decision_kind TEXT, + decision_actor_id TEXT, + decision_reason TEXT, + decision_payload_json TEXT, + decided_at TEXT +); + +CREATE INDEX IF NOT EXISTS interrupts_run_pending_idx + ON interrupts (run_id, decided_at); +""" + + +def _dump(value: object) -> str: + frozen = freeze_json(value, field="persistence value") + return json.dumps( + thaw_json(frozen), + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + + +def _load(value: str) -> object: + return json.loads(value) + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _parse_time(value: str) -> datetime: + parsed = datetime.fromisoformat(value) + if parsed.tzinfo is None: + raise ValueError("persisted timestamp must be timezone-aware") + return parsed + + +class SQLiteRunStore: + """SQLite source of truth for local mode; every mutation is transactional.""" + + def __init__(self, path: str | Path) -> None: + self.path = Path(path).expanduser().resolve(strict=False) + self.path.parent.mkdir(parents=True, exist_ok=True) + self._closed = False + self._migration_lock = threading.Lock() + self._migrate() + + def _connect(self) -> sqlite3.Connection: + if self._closed: + raise RuntimeError("SQLiteRunStore is closed") + connection = sqlite3.connect( + str(self.path), + timeout=30.0, + isolation_level=None, + ) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA journal_mode = WAL") + connection.execute("PRAGMA busy_timeout = 30000") + return connection + + def _migrate(self) -> None: + with self._migration_lock: + connection = self._connect() + try: + connection.executescript(_SCHEMA) + connection.execute( + """ + INSERT OR IGNORE INTO schema_migrations(version, applied_at) + VALUES (1, ?) + """, + (_now().isoformat(),), + ) + finally: + connection.close() + + @contextmanager + def _transaction(self) -> Iterator[sqlite3.Connection]: + connection = self._connect() + try: + connection.execute("BEGIN IMMEDIATE") + yield connection + connection.commit() + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + def close(self) -> None: + self._closed = True + + def create_run(self, run: Run) -> Run: + fingerprint = self._fingerprint(run) + with self._transaction() as connection: + if run.idempotency_key is not None: + existing = connection.execute( + """ + SELECT * FROM runs + WHERE tenant_id = ? AND idempotency_key = ? + """, + (run.tenant_id, run.idempotency_key), + ).fetchone() + if existing is not None: + if existing["request_fingerprint"] != fingerprint: + raise ConflictError( + "idempotency key was already used for a different request" + ) + return self._run_from_row(existing) + try: + connection.execute( + """ + INSERT INTO runs( + id, plan_id, revision_id, session_id, tenant_id, status, + state_json, next_nodes_json, idempotency_key, + request_fingerprint, created_at, updated_at, version + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(run.id), + str(run.plan_id), + str(run.revision_id), + str(run.session_id), + run.tenant_id, + run.status.value, + _dump(run.state), + _dump(run.next_nodes), + run.idempotency_key, + fingerprint, + run.created_at.isoformat(), + run.updated_at.isoformat(), + run.version, + ), + ) + except sqlite3.IntegrityError as exc: + raise ConflictError("run already exists") from exc + self._append_event( + connection, + run.id, + "run.created", + {"status": run.status.value}, + ) + return run + + def get_run(self, run_id: UUID) -> Run: + connection = self._connect() + try: + row = connection.execute( + "SELECT * FROM runs WHERE id = ?", + (str(run_id),), + ).fetchone() + finally: + connection.close() + if row is None: + raise KeyError(str(run_id)) + return self._run_from_row(row) + + def list_runs(self, *, tenant_id: str) -> tuple[Run, ...]: + connection = self._connect() + try: + rows = connection.execute( + "SELECT * FROM runs WHERE tenant_id = ? ORDER BY created_at, id", + (tenant_id,), + ).fetchall() + finally: + connection.close() + return tuple(self._run_from_row(row) for row in rows) + + def transition_run( + self, + run_id: UUID, + *, + expected_version: int, + target: RunStatus, + state: Mapping[str, object] | None = None, + next_nodes: tuple[str, ...] | None = None, + ) -> Run: + with self._transaction() as connection: + row = self._required_run_row(connection, run_id) + current = self._run_from_row(row) + if current.version != expected_version: + raise ConflictError( + "run version conflict", + details={ + "expected_version": expected_version, + "actual_version": current.version, + }, + ) + assert_transition(current.status, target) + updated_at = _now() + next_state = state if state is not None else current.state + next_queue = next_nodes if next_nodes is not None else current.next_nodes + cursor = connection.execute( + """ + UPDATE runs + SET status = ?, state_json = ?, next_nodes_json = ?, + updated_at = ?, version = version + 1 + WHERE id = ? AND version = ? + """, + ( + target.value, + _dump(next_state), + _dump(next_queue), + updated_at.isoformat(), + str(run_id), + expected_version, + ), + ) + if cursor.rowcount != 1: + raise ConflictError("run version conflict") + self._append_event( + connection, + run_id, + "run.state.changed", + {"from": current.status.value, "to": target.value}, + ) + updated_row = self._required_run_row(connection, run_id) + return self._run_from_row(updated_row) + + def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: + connection = self._connect() + try: + rows = connection.execute( + """ + SELECT * FROM run_events + WHERE run_id = ? ORDER BY sequence + """, + (str(run_id),), + ).fetchall() + finally: + connection.close() + return tuple( + RunEvent( + run_id=UUID(row["run_id"]), + sequence=int(row["sequence"]), + type=str(row["type"]), + data=_load(row["data_json"]), # type: ignore[arg-type] + created_at=_parse_time(row["created_at"]), + ) + for row in rows + ) + + def append_checkpoint(self, checkpoint: Checkpoint) -> None: + with self._transaction() as connection: + self._required_run_row(connection, checkpoint.run_id) + row = connection.execute( + """ + SELECT COALESCE(MAX(sequence), 0) AS sequence + FROM checkpoints WHERE run_id = ? + """, + (str(checkpoint.run_id),), + ).fetchone() + expected = int(row["sequence"]) + 1 + if checkpoint.sequence != expected: + raise ConflictError( + f"checkpoint sequence must be {expected}, got {checkpoint.sequence}" + ) + connection.execute( + """ + INSERT INTO checkpoints( + id, run_id, sequence, plan_hash, state_json, + next_nodes_json, pending_interrupts_json, + effect_watermark, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(checkpoint.id), + str(checkpoint.run_id), + checkpoint.sequence, + checkpoint.plan_hash, + _dump(checkpoint.state), + _dump(checkpoint.next_nodes), + _dump(tuple(str(item) for item in checkpoint.pending_interrupts)), + checkpoint.effect_watermark, + checkpoint.created_at.isoformat(), + ), + ) + self._append_event( + connection, + checkpoint.run_id, + "run.checkpoint.created", + { + "checkpoint_id": str(checkpoint.id), + "sequence": checkpoint.sequence, + }, + ) + + def latest_checkpoint(self, run_id: UUID) -> Checkpoint | None: + connection = self._connect() + try: + row = connection.execute( + """ + SELECT * FROM checkpoints + WHERE run_id = ? ORDER BY sequence DESC LIMIT 1 + """, + (str(run_id),), + ).fetchone() + finally: + connection.close() + return None if row is None else self._checkpoint_from_row(row) + + def create_interrupt( + self, + interrupt: Interrupt, + *, + expected_run_version: int, + ) -> Run: + with self._transaction() as connection: + row = self._required_run_row(connection, interrupt.run_id) + current = self._run_from_row(row) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + assert_transition(current.status, RunStatus.WAITING) + connection.execute( + """ + INSERT INTO interrupts( + id, run_id, kind, request_json, created_at + ) VALUES (?, ?, ?, ?, ?) + """, + ( + str(interrupt.id), + str(interrupt.run_id), + interrupt.kind.value, + _dump(interrupt.request), + interrupt.created_at.isoformat(), + ), + ) + self._update_status( + connection, + current, + target=RunStatus.WAITING, + expected_version=expected_run_version, + ) + self._append_event( + connection, + interrupt.run_id, + "run.interrupt.created", + {"interrupt_id": str(interrupt.id), "kind": interrupt.kind.value}, + ) + return self._run_from_row( + self._required_run_row(connection, interrupt.run_id) + ) + + def get_interrupt(self, interrupt_id: UUID) -> Interrupt: + connection = self._connect() + try: + row = connection.execute( + "SELECT * FROM interrupts WHERE id = ?", + (str(interrupt_id),), + ).fetchone() + finally: + connection.close() + if row is None: + raise KeyError(str(interrupt_id)) + return self._interrupt_from_row(row) + + def decide_interrupt( + self, + interrupt_id: UUID, + *, + decision: ApprovalDecision, + expected_run_version: int, + ) -> Run: + with self._transaction() as connection: + row = connection.execute( + "SELECT * FROM interrupts WHERE id = ?", + (str(interrupt_id),), + ).fetchone() + if row is None: + raise KeyError(str(interrupt_id)) + if row["decision_kind"] is not None: + raise ConflictError("interrupt was already decided") + run_id = UUID(row["run_id"]) + current = self._run_from_row(self._required_run_row(connection, run_id)) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + assert_transition(current.status, RunStatus.QUEUED) + decided_at = _now() + connection.execute( + """ + UPDATE interrupts + SET decision_kind = ?, decision_actor_id = ?, + decision_reason = ?, decision_payload_json = ?, + decided_at = ? + WHERE id = ? AND decision_kind IS NULL + """, + ( + decision.kind.value, + decision.actor_id, + decision.reason, + _dump(decision.payload), + decided_at.isoformat(), + str(interrupt_id), + ), + ) + self._update_status( + connection, + current, + target=RunStatus.QUEUED, + expected_version=expected_run_version, + ) + self._append_event( + connection, + run_id, + "run.interrupt.decided", + { + "interrupt_id": str(interrupt_id), + "decision": decision.kind.value, + "actor_id": decision.actor_id, + }, + ) + return self._run_from_row(self._required_run_row(connection, run_id)) + + def _update_status( + self, + connection: sqlite3.Connection, + current: Run, + *, + target: RunStatus, + expected_version: int, + ) -> None: + cursor = connection.execute( + """ + UPDATE runs + SET status = ?, updated_at = ?, version = version + 1 + WHERE id = ? AND version = ? + """, + ( + target.value, + _now().isoformat(), + str(current.id), + expected_version, + ), + ) + if cursor.rowcount != 1: + raise ConflictError("run version conflict") + + def _append_event( + self, + connection: sqlite3.Connection, + run_id: UUID, + type: str, + data: Mapping[str, object], + ) -> None: + row = connection.execute( + """ + SELECT COALESCE(MAX(sequence), 0) AS sequence + FROM run_events WHERE run_id = ? + """, + (str(run_id),), + ).fetchone() + sequence = int(row["sequence"]) + 1 + connection.execute( + """ + INSERT INTO run_events(run_id, sequence, type, data_json, created_at) + VALUES (?, ?, ?, ?, ?) + """, + (str(run_id), sequence, type, _dump(data), _now().isoformat()), + ) + + def _required_run_row( + self, + connection: sqlite3.Connection, + run_id: UUID, + ) -> sqlite3.Row: + row = connection.execute( + "SELECT * FROM runs WHERE id = ?", + (str(run_id),), + ).fetchone() + if row is None: + raise KeyError(str(run_id)) + return cast(sqlite3.Row, row) + + def _fingerprint(self, run: Run) -> str: + payload = _dump( + { + "plan_id": str(run.plan_id), + "revision_id": str(run.revision_id), + "session_id": str(run.session_id), + "tenant_id": run.tenant_id, + "state": run.state, + "next_nodes": run.next_nodes, + } + ) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + def _run_from_row(self, row: sqlite3.Row) -> Run: + state = _load(row["state_json"]) + next_nodes = _load(row["next_nodes_json"]) + assert isinstance(state, dict) + assert isinstance(next_nodes, list) + return Run( + id=UUID(row["id"]), + plan_id=UUID(row["plan_id"]), + revision_id=UUID(row["revision_id"]), + session_id=UUID(row["session_id"]), + tenant_id=row["tenant_id"], + status=RunStatus(row["status"]), + state=state, + next_nodes=tuple(str(item) for item in next_nodes), + idempotency_key=row["idempotency_key"], + created_at=_parse_time(row["created_at"]), + updated_at=_parse_time(row["updated_at"]), + version=int(row["version"]), + ) + + def _checkpoint_from_row(self, row: sqlite3.Row) -> Checkpoint: + state = _load(row["state_json"]) + next_nodes = _load(row["next_nodes_json"]) + pending = _load(row["pending_interrupts_json"]) + assert isinstance(state, dict) + assert isinstance(next_nodes, list) + assert isinstance(pending, list) + return Checkpoint( + id=UUID(row["id"]), + run_id=UUID(row["run_id"]), + sequence=int(row["sequence"]), + plan_hash=row["plan_hash"], + state=state, + next_nodes=tuple(str(item) for item in next_nodes), + pending_interrupts=tuple(UUID(str(item)) for item in pending), + effect_watermark=int(row["effect_watermark"]), + created_at=_parse_time(row["created_at"]), + ) + + def _interrupt_from_row(self, row: sqlite3.Row) -> Interrupt: + request = _load(row["request_json"]) + assert isinstance(request, dict) + decision: ApprovalDecision | None = None + if row["decision_kind"] is not None: + payload = _load(row["decision_payload_json"]) + assert isinstance(payload, dict) + decision = ApprovalDecision( + kind=ApprovalDecisionKind(row["decision_kind"]), + actor_id=row["decision_actor_id"], + reason=row["decision_reason"], + payload=payload, + ) + return Interrupt( + id=UUID(row["id"]), + run_id=UUID(row["run_id"]), + kind=InterruptKind(row["kind"]), + request=request, + created_at=_parse_time(row["created_at"]), + decision=decision, + decided_at=( + _parse_time(row["decided_at"]) + if row["decided_at"] is not None + else None + ), + ) diff --git a/src/rath/runtime/store.py b/src/rath/runtime/store.py new file mode 100644 index 0000000..41f3a9f --- /dev/null +++ b/src/rath/runtime/store.py @@ -0,0 +1,63 @@ +"""Persistence protocol shared by embedded and production Run stores.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Protocol, runtime_checkable +from uuid import UUID + +from rath.runtime.models import ( + ApprovalDecision, + Checkpoint, + Interrupt, + Run, + RunEvent, + RunStatus, +) + +__all__ = ["RunStore"] + + +@runtime_checkable +class RunStore(Protocol): + def create_run(self, run: Run) -> Run: ... + + def get_run(self, run_id: UUID) -> Run: ... + + def list_runs(self, *, tenant_id: str) -> tuple[Run, ...]: ... + + def transition_run( + self, + run_id: UUID, + *, + expected_version: int, + target: RunStatus, + state: Mapping[str, object] | None = None, + next_nodes: tuple[str, ...] | None = None, + ) -> Run: ... + + def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: ... + + def append_checkpoint(self, checkpoint: Checkpoint) -> None: ... + + def latest_checkpoint(self, run_id: UUID) -> Checkpoint | None: ... + + def create_interrupt( + self, + interrupt: Interrupt, + *, + expected_run_version: int, + ) -> Run: ... + + def get_interrupt(self, interrupt_id: UUID) -> Interrupt: ... + + def decide_interrupt( + self, + interrupt_id: UUID, + *, + decision: ApprovalDecision, + expected_run_version: int, + ) -> Run: ... + + def close(self) -> None: ... + diff --git a/tests/runtime/test_run_state.py b/tests/runtime/test_run_state.py new file mode 100644 index 0000000..6947d9a --- /dev/null +++ b/tests/runtime/test_run_state.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from uuid import uuid4 + +import pytest + +from rath.runtime import ( + InvalidRunTransition, + Run, + RunStatus, + assert_transition, +) + + +def _run(status: RunStatus = RunStatus.QUEUED) -> Run: + return Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant-1", + status=status, + state={"query": "hello"}, + next_nodes=("search",), + ) + + +@pytest.mark.parametrize( + ("source", "target"), + [ + (RunStatus.QUEUED, RunStatus.RUNNING), + (RunStatus.RUNNING, RunStatus.WAITING), + (RunStatus.WAITING, RunStatus.QUEUED), + (RunStatus.RUNNING, RunStatus.SUCCEEDED), + (RunStatus.RUNNING, RunStatus.NEEDS_REVIEW), + (RunStatus.NEEDS_REVIEW, RunStatus.QUEUED), + ], +) +def test_valid_run_transitions(source: RunStatus, target: RunStatus) -> None: + assert_transition(source, target) + + +def test_terminal_run_is_immutable() -> None: + with pytest.raises(InvalidRunTransition): + assert_transition(RunStatus.SUCCEEDED, RunStatus.RUNNING) + + +def test_run_state_is_deeply_immutable() -> None: + state = {"nested": {"value": 1}} + run = Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant-1", + state=state, + next_nodes=("search",), + ) + state["nested"]["value"] = 2 + + assert run.state["nested"]["value"] == 1 # type: ignore[index] + assert run.version == 0 + diff --git a/tests/runtime/test_sqlite_run_store.py b/tests/runtime/test_sqlite_run_store.py new file mode 100644 index 0000000..f42074b --- /dev/null +++ b/tests/runtime/test_sqlite_run_store.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from uuid import uuid4 + +import pytest + +from rath.runtime import ( + ApprovalDecision, + ApprovalDecisionKind, + Checkpoint, + ConflictError, + Interrupt, + InterruptKind, + Run, + RunStatus, + SQLiteRunStore, +) + + +def _run(*, idempotency_key: str | None = None) -> Run: + return Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant-1", + state={"count": 0}, + next_nodes=("start",), + idempotency_key=idempotency_key, + ) + + +def test_run_survives_store_reopen(tmp_path: Path) -> None: + path = tmp_path / "runtime.db" + first = SQLiteRunStore(path) + created = first.create_run(_run()) + first.close() + + second = SQLiteRunStore(path) + loaded = second.get_run(created.id) + second.close() + + assert loaded == created + assert loaded.state["count"] == 0 + + +def test_transition_is_compare_and_swap_and_appends_event(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + created = store.create_run(_run()) + running = store.transition_run( + created.id, + expected_version=0, + target=RunStatus.RUNNING, + ) + + assert running.status is RunStatus.RUNNING + assert running.version == 1 + assert [event.type for event in store.list_run_events(created.id)] == [ + "run.created", + "run.state.changed", + ] + with pytest.raises(ConflictError): + store.transition_run( + created.id, + expected_version=0, + target=RunStatus.FAILED, + ) + + +def test_idempotency_key_returns_same_run_and_rejects_payload_mismatch( + tmp_path: Path, +) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + original = _run(idempotency_key="request-1") + first = store.create_run(original) + second = store.create_run(original) + + assert second.id == first.id + + mismatch = Run.create( + plan_id=uuid4(), + revision_id=original.revision_id, + session_id=original.session_id, + tenant_id=original.tenant_id, + idempotency_key=original.idempotency_key, + ) + with pytest.raises(ConflictError, match="different request"): + store.create_run(mismatch) + + +def test_concurrent_idempotent_create_has_single_winner(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + candidate = _run(idempotency_key="same-request") + with ThreadPoolExecutor(max_workers=8) as pool: + ids = list(pool.map(lambda _: store.create_run(candidate).id, range(16))) + + assert set(ids) == {candidate.id} + assert len(store.list_runs(tenant_id="tenant-1")) == 1 + + +def test_checkpoints_are_ordered_and_survive_restart(tmp_path: Path) -> None: + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + run = store.create_run(_run()) + checkpoint = Checkpoint.create( + run_id=run.id, + sequence=1, + plan_hash="a" * 64, + state={"count": 1}, + next_nodes=("next",), + effect_watermark=0, + ) + store.append_checkpoint(checkpoint) + with pytest.raises(ConflictError, match="sequence"): + store.append_checkpoint(checkpoint) + store.close() + + reopened = SQLiteRunStore(path) + loaded = reopened.latest_checkpoint(run.id) + assert loaded == checkpoint + + +def test_interrupt_decision_and_waiting_resume_are_atomic(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + queued = store.create_run(_run()) + running = store.transition_run( + queued.id, + expected_version=queued.version, + target=RunStatus.RUNNING, + ) + interrupt = Interrupt.create( + run_id=running.id, + kind=InterruptKind.APPROVAL, + request={"tool": "email.send"}, + ) + waiting = store.create_interrupt( + interrupt, + expected_run_version=running.version, + ) + assert waiting.status is RunStatus.WAITING + + resumed = store.decide_interrupt( + interrupt.id, + decision=ApprovalDecision( + kind=ApprovalDecisionKind.APPROVE, + actor_id="user-1", + reason="approved", + ), + expected_run_version=waiting.version, + ) + + assert resumed.status is RunStatus.QUEUED + decided = store.get_interrupt(interrupt.id) + assert decided.decision is not None + assert decided.decision.actor_id == "user-1" + with pytest.raises(ConflictError, match="already decided"): + store.decide_interrupt( + interrupt.id, + decision=ApprovalDecision( + kind=ApprovalDecisionKind.REJECT, + actor_id="user-2", + reason="late", + ), + expected_run_version=resumed.version, + ) + From d63f92568a59c1ffeb3b447890e3c0cf512604fc Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 11:52:38 +0800 Subject: [PATCH 05/22] feat(v2): add worker leases and orphan recovery --- src/rath/runtime/__init__.py | 4 + src/rath/runtime/models.py | 33 ++++ src/rath/runtime/sqlite.py | 258 ++++++++++++++++++++++++- src/rath/runtime/store.py | 36 +++- tests/runtime/test_scheduler_leases.py | 98 ++++++++++ 5 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 tests/runtime/test_scheduler_leases.py diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py index ecb3702..357cf7b 100644 --- a/src/rath/runtime/__init__.py +++ b/src/rath/runtime/__init__.py @@ -4,10 +4,12 @@ ApprovalDecision, ApprovalDecisionKind, Checkpoint, + ClaimedRun, ConflictError, Interrupt, InterruptKind, InvalidRunTransition, + ResourceLease, Run, RunEvent, RunStatus, @@ -21,6 +23,7 @@ "ApprovalDecisionKind", "assert_transition", "Checkpoint", + "ClaimedRun", "ConflictError", "Interrupt", "InterruptKind", @@ -28,6 +31,7 @@ "Run", "RunEvent", "RunStatus", + "ResourceLease", "RunStore", "SQLiteRunStore", ] diff --git a/src/rath/runtime/models.py b/src/rath/runtime/models.py index a01414c..c6d4ae0 100644 --- a/src/rath/runtime/models.py +++ b/src/rath/runtime/models.py @@ -22,6 +22,8 @@ "Run", "RunEvent", "RunStatus", + "ClaimedRun", + "ResourceLease", "assert_transition", ] @@ -322,3 +324,34 @@ def create( created_at=datetime.now(timezone.utc), ) + +@dataclass(frozen=True, slots=True) +class ResourceLease: + id: UUID + resource_type: str + resource_id: str + owner_run_id: UUID + holder_worker_id: str + expires_at: datetime + fencing_token: int + created_at: datetime + updated_at: datetime + + def __post_init__(self) -> None: + if not self.resource_type.strip(): + raise ValueError("lease resource_type must not be empty") + if not self.resource_id.strip(): + raise ValueError("lease resource_id must not be empty") + if not self.holder_worker_id.strip(): + raise ValueError("lease holder_worker_id must not be empty") + if self.fencing_token < 1: + raise ValueError("lease fencing_token must be positive") + _aware(self.expires_at, field_name="lease.expires_at") + _aware(self.created_at, field_name="lease.created_at") + _aware(self.updated_at, field_name="lease.updated_at") + + +@dataclass(frozen=True, slots=True) +class ClaimedRun: + run: Run + lease: ResourceLease diff --git a/src/rath/runtime/sqlite.py b/src/rath/runtime/sqlite.py index cb144ac..822d3a1 100644 --- a/src/rath/runtime/sqlite.py +++ b/src/rath/runtime/sqlite.py @@ -8,7 +8,7 @@ import threading from collections.abc import Iterator, Mapping from contextlib import contextmanager -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import cast from uuid import UUID @@ -18,9 +18,11 @@ ApprovalDecision, ApprovalDecisionKind, Checkpoint, + ClaimedRun, ConflictError, Interrupt, InterruptKind, + ResourceLease, Run, RunEvent, RunStatus, @@ -92,6 +94,20 @@ CREATE INDEX IF NOT EXISTS interrupts_run_pending_idx ON interrupts (run_id, decided_at); + +CREATE TABLE IF NOT EXISTS run_leases ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL UNIQUE REFERENCES runs(id) ON DELETE CASCADE, + holder_worker_id TEXT NOT NULL, + expires_at TEXT NOT NULL, + fencing_token INTEGER NOT NULL, + active INTEGER NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS run_leases_expiry_idx + ON run_leases (active, expires_at); """ @@ -496,6 +512,225 @@ def decide_interrupt( ) return self._run_from_row(self._required_run_row(connection, run_id)) + def claim_next( + self, + *, + worker_id: str, + lease_seconds: float, + now: datetime | None = None, + ) -> ClaimedRun | None: + if not worker_id.strip(): + raise ValueError("worker_id must not be empty") + if lease_seconds <= 0: + raise ValueError("lease_seconds must be greater than zero") + claimed_at = now or _now() + if claimed_at.tzinfo is None: + raise ValueError("now must be timezone-aware") + with self._transaction() as connection: + row = connection.execute( + """ + SELECT * FROM runs + WHERE status = ? + ORDER BY created_at, id + LIMIT 1 + """, + (RunStatus.QUEUED.value,), + ).fetchone() + if row is None: + return None + current = self._run_from_row(row) + assert_transition(current.status, RunStatus.RUNNING) + cursor = connection.execute( + """ + UPDATE runs + SET status = ?, updated_at = ?, version = version + 1 + WHERE id = ? AND version = ? AND status = ? + """, + ( + RunStatus.RUNNING.value, + claimed_at.isoformat(), + str(current.id), + current.version, + RunStatus.QUEUED.value, + ), + ) + if cursor.rowcount != 1: + raise ConflictError("run claim conflict") + previous = connection.execute( + "SELECT * FROM run_leases WHERE run_id = ?", + (str(current.id),), + ).fetchone() + lease_id = UUID(previous["id"]) if previous is not None else UUID( + bytes=hashlib.sha256(str(current.id).encode("utf-8")).digest()[:16] + ) + token = ( + int(previous["fencing_token"]) + 1 if previous is not None else 1 + ) + created_at = ( + _parse_time(previous["created_at"]) + if previous is not None + else claimed_at + ) + expires_at = claimed_at + timedelta(seconds=lease_seconds) + connection.execute( + """ + INSERT INTO run_leases( + id, run_id, holder_worker_id, expires_at, fencing_token, + active, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, 1, ?, ?) + ON CONFLICT(run_id) DO UPDATE SET + holder_worker_id = excluded.holder_worker_id, + expires_at = excluded.expires_at, + fencing_token = excluded.fencing_token, + active = 1, + updated_at = excluded.updated_at + """, + ( + str(lease_id), + str(current.id), + worker_id, + expires_at.isoformat(), + token, + created_at.isoformat(), + claimed_at.isoformat(), + ), + ) + self._append_event( + connection, + current.id, + "run.claimed", + {"worker_id": worker_id, "fencing_token": token}, + ) + run = self._run_from_row(self._required_run_row(connection, current.id)) + return ClaimedRun( + run=run, + lease=ResourceLease( + id=lease_id, + resource_type="run", + resource_id=str(current.id), + owner_run_id=current.id, + holder_worker_id=worker_id, + expires_at=expires_at, + fencing_token=token, + created_at=created_at, + updated_at=claimed_at, + ), + ) + + def renew_lease( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + lease_seconds: float, + now: datetime | None = None, + ) -> ResourceLease: + if lease_seconds <= 0: + raise ValueError("lease_seconds must be greater than zero") + renewed_at = now or _now() + expires_at = renewed_at + timedelta(seconds=lease_seconds) + with self._transaction() as connection: + row = self._required_lease( + connection, + run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + connection.execute( + """ + UPDATE run_leases + SET expires_at = ?, updated_at = ? + WHERE run_id = ? + """, + (expires_at.isoformat(), renewed_at.isoformat(), str(run_id)), + ) + return ResourceLease( + id=UUID(row["id"]), + resource_type="run", + resource_id=str(run_id), + owner_run_id=run_id, + holder_worker_id=worker_id, + expires_at=expires_at, + fencing_token=fencing_token, + created_at=_parse_time(row["created_at"]), + updated_at=renewed_at, + ) + + def assert_fencing_token( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + ) -> None: + connection = self._connect() + try: + self._required_lease( + connection, + run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + finally: + connection.close() + + def requeue_expired_leases( + self, + *, + now: datetime | None = None, + ) -> tuple[UUID, ...]: + recovered_at = now or _now() + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT l.*, r.status, r.version + FROM run_leases l + JOIN runs r ON r.id = l.run_id + WHERE l.active = 1 AND l.expires_at <= ? + ORDER BY l.expires_at, l.run_id + """, + (recovered_at.isoformat(),), + ).fetchall() + recovered: list[UUID] = [] + for row in rows: + run_id = UUID(row["run_id"]) + if RunStatus(row["status"]) is not RunStatus.RUNNING: + connection.execute( + "UPDATE run_leases SET active = 0 WHERE run_id = ?", + (str(run_id),), + ) + continue + connection.execute( + """ + UPDATE runs + SET status = ?, updated_at = ?, version = version + 1 + WHERE id = ? AND version = ? + """, + ( + RunStatus.QUEUED.value, + recovered_at.isoformat(), + str(run_id), + int(row["version"]), + ), + ) + connection.execute( + """ + UPDATE run_leases + SET active = 0, updated_at = ? + WHERE run_id = ? + """, + (recovered_at.isoformat(), str(run_id)), + ) + self._append_event( + connection, + run_id, + "run.lease.expired", + {"fencing_token": int(row["fencing_token"])}, + ) + recovered.append(run_id) + return tuple(recovered) + def _update_status( self, connection: sqlite3.Connection, @@ -556,6 +791,27 @@ def _required_run_row( raise KeyError(str(run_id)) return cast(sqlite3.Row, row) + def _required_lease( + self, + connection: sqlite3.Connection, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + ) -> sqlite3.Row: + row = connection.execute( + "SELECT * FROM run_leases WHERE run_id = ?", + (str(run_id),), + ).fetchone() + if ( + row is None + or not bool(row["active"]) + or row["holder_worker_id"] != worker_id + or int(row["fencing_token"]) != fencing_token + ): + raise ConflictError("lease fencing token is stale or not owned") + return cast(sqlite3.Row, row) + def _fingerprint(self, run: Run) -> str: payload = _dump( { diff --git a/src/rath/runtime/store.py b/src/rath/runtime/store.py index 41f3a9f..48e793d 100644 --- a/src/rath/runtime/store.py +++ b/src/rath/runtime/store.py @@ -3,13 +3,16 @@ from __future__ import annotations from collections.abc import Mapping +from datetime import datetime from typing import Protocol, runtime_checkable from uuid import UUID from rath.runtime.models import ( ApprovalDecision, Checkpoint, + ClaimedRun, Interrupt, + ResourceLease, Run, RunEvent, RunStatus, @@ -59,5 +62,36 @@ def decide_interrupt( expected_run_version: int, ) -> Run: ... - def close(self) -> None: ... + def claim_next( + self, + *, + worker_id: str, + lease_seconds: float, + now: datetime | None = None, + ) -> ClaimedRun | None: ... + + def renew_lease( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + lease_seconds: float, + now: datetime | None = None, + ) -> ResourceLease: ... + def assert_fencing_token( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + ) -> None: ... + + def requeue_expired_leases( + self, + *, + now: datetime | None = None, + ) -> tuple[UUID, ...]: ... + + def close(self) -> None: ... diff --git a/tests/runtime/test_scheduler_leases.py b/tests/runtime/test_scheduler_leases.py new file mode 100644 index 0000000..4cb4e71 --- /dev/null +++ b/tests/runtime/test_scheduler_leases.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta, timezone +from pathlib import Path +from uuid import uuid4 + +import pytest + +from rath.runtime import ConflictError, Run, RunStatus, SQLiteRunStore + + +def _run() -> Run: + return Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant-1", + next_nodes=("start",), + ) + + +def test_claim_is_exclusive_and_moves_run_to_running(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + queued = store.create_run(_run()) + + claim = store.claim_next(worker_id="worker-1", lease_seconds=30) + + assert claim is not None + assert claim.run.id == queued.id + assert claim.run.status is RunStatus.RUNNING + assert claim.lease.holder_worker_id == "worker-1" + assert claim.lease.fencing_token == 1 + assert store.claim_next(worker_id="worker-2", lease_seconds=30) is None + + +def test_concurrent_claim_has_single_winner(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + store.create_run(_run()) + with ThreadPoolExecutor(max_workers=8) as pool: + claims = list( + pool.map( + lambda index: store.claim_next( + worker_id=f"worker-{index}", + lease_seconds=30, + ), + range(8), + ) + ) + + winners = [claim for claim in claims if claim is not None] + assert len(winners) == 1 + + +def test_lease_renewal_rejects_stale_fencing_token(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + store.create_run(_run()) + claim = store.claim_next(worker_id="worker-1", lease_seconds=30) + assert claim is not None + + renewed = store.renew_lease( + claim.run.id, + worker_id="worker-1", + fencing_token=claim.lease.fencing_token, + lease_seconds=60, + ) + assert renewed.expires_at > claim.lease.expires_at + + with pytest.raises(ConflictError, match="fencing"): + store.renew_lease( + claim.run.id, + worker_id="worker-1", + fencing_token=0, + lease_seconds=60, + ) + + +def test_expired_lease_is_requeued_with_new_fencing_token(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + store.create_run(_run()) + first = store.claim_next(worker_id="worker-1", lease_seconds=1) + assert first is not None + future = datetime.now(timezone.utc) + timedelta(seconds=2) + + recovered = store.requeue_expired_leases(now=future) + second = store.claim_next(worker_id="worker-2", lease_seconds=30, now=future) + + assert recovered == (first.run.id,) + assert second is not None + assert second.run.id == first.run.id + assert second.lease.fencing_token == 2 + with pytest.raises(ConflictError, match="fencing"): + store.assert_fencing_token( + first.run.id, + worker_id="worker-1", + fencing_token=first.lease.fencing_token, + ) + From f77e4d8cab94b16df92d9efedafe720d6680f256 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 18:05:16 +0800 Subject: [PATCH 06/22] feat(v2): execute durable workflows with checkpoint recovery --- src/rath/runtime/__init__.py | 3 + src/rath/runtime/local.py | 194 ++++++++++++++++++++++++++++ src/rath/runtime/sqlite.py | 147 +++++++++++++++++++++ src/rath/security/__init__.py | 1 - src/rath/security/audit.py | 6 +- src/rath/security/policy.py | 7 +- src/rath/security/secrets.py | 6 +- tests/runtime/test_local_runtime.py | 107 +++++++++++++++ 8 files changed, 462 insertions(+), 9 deletions(-) create mode 100644 src/rath/runtime/local.py create mode 100644 tests/runtime/test_local_runtime.py diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py index 357cf7b..baf8481 100644 --- a/src/rath/runtime/__init__.py +++ b/src/rath/runtime/__init__.py @@ -1,5 +1,6 @@ """Public durable runtime state and persistence contracts.""" +from rath.runtime.local import LocalRuntime, StepContext from rath.runtime.models import ( ApprovalDecision, ApprovalDecisionKind, @@ -28,10 +29,12 @@ "Interrupt", "InterruptKind", "InvalidRunTransition", + "LocalRuntime", "Run", "RunEvent", "RunStatus", "ResourceLease", "RunStore", "SQLiteRunStore", + "StepContext", ] diff --git a/src/rath/runtime/local.py b/src/rath/runtime/local.py new file mode 100644 index 0000000..108de56 --- /dev/null +++ b/src/rath/runtime/local.py @@ -0,0 +1,194 @@ +"""Embedded durable executor for explicit Workflow step boundaries.""" + +from __future__ import annotations + +import inspect +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Coroutine, cast +from uuid import UUID + +from rath._json import thaw_json +from rath.context import RunContext +from rath.definition import ExecutionPlan, NodeKind, WorkflowCompiler +from rath.runtime.models import Checkpoint, ClaimedRun, Run, RunStatus +from rath.runtime.sqlite import SQLiteRunStore + +__all__ = ["LocalRuntime", "StepContext"] + + +@dataclass(frozen=True, slots=True) +class StepContext: + run_id: UUID + request: RunContext + worker_id: str + fencing_token: int + + +@dataclass(frozen=True, slots=True) +class _Registration: + workflow: object + plan: ExecutionPlan + + +class LocalRuntime: + """Sync façade over the durable local worker engine.""" + + def __init__(self, store: SQLiteRunStore) -> None: + self.store = store + self._registrations: dict[UUID, _Registration] = {} + self._contexts: dict[UUID, RunContext] = {} + + def register(self, workflow: object, *, revision_id: UUID) -> ExecutionPlan: + plan = WorkflowCompiler().compile(workflow, revision_id=revision_id) + if not plan.durable: + raise ValueError( + "durable runtime requires explicit @step boundaries; " + f"issues: {plan.compatibility_issues}" + ) + self._registrations[plan.id] = _Registration(workflow=workflow, plan=plan) + return plan + + def submit( + self, + workflow: object, + *, + session_id: UUID, + context: RunContext, + state: Mapping[str, object] | None = None, + idempotency_key: str | None = None, + ) -> Run: + context.ensure_active() + plan = self.register(workflow, revision_id=context.revision_id) + run = Run.create( + plan_id=plan.id, + revision_id=plan.revision_id, + session_id=session_id, + tenant_id=context.security.tenant_id, + state=state, + next_nodes=(plan.definition.entrypoint,), + idempotency_key=idempotency_key, + ) + created = self.store.create_run(run) + self._contexts[created.id] = context + return created + + def work_once( + self, + *, + worker_id: str, + lease_seconds: float = 30.0, + max_steps: int | None = None, + now: datetime | None = None, + ) -> Run | None: + claim = self.store.claim_next( + worker_id=worker_id, + lease_seconds=lease_seconds, + now=now, + ) + if claim is None: + return None + try: + return self._execute_claim(claim, max_steps=max_steps) + except BaseException as exc: + return self.store.finish_claim( + claim.run.id, + worker_id=worker_id, + fencing_token=claim.lease.fencing_token, + expected_run_version=self.store.get_run(claim.run.id).version, + target=RunStatus.FAILED, + event_type="run.execution.failed", + event_data={ + "error_type": type(exc).__name__, + "message": str(exc), + }, + ) + + def _execute_claim( + self, + claim: ClaimedRun, + *, + max_steps: int | None, + ) -> Run: + registration = self._registrations.get(claim.run.plan_id) + if registration is None: + raise RuntimeError(f"execution plan {claim.run.plan_id} is not registered") + context = self._contexts.get(claim.run.id) + if context is None: + context = RunContext.local(revision_id=claim.run.revision_id) + run = claim.run + steps = 0 + by_id = {node.id: node for node in registration.plan.nodes} + while run.next_nodes and (max_steps is None or steps < max_steps): + node_id = run.next_nodes[0] + node = by_id[node_id] + state_value = thaw_json(run.state) + assert isinstance(state_value, dict) + handler = getattr(registration.workflow, node.id) + step_context = StepContext( + run_id=run.id, + request=context, + worker_id=claim.lease.holder_worker_id, + fencing_token=claim.lease.fencing_token, + ) + if node.kind is NodeKind.ROUTER: + result = handler(state_value) + else: + result = handler(state_value, step_context) + if inspect.isawaitable(result): + from rath._async.runtime import runtime as async_runtime + + result = async_runtime().run( + cast(Coroutine[Any, Any, object], result) + ) + + next_nodes: tuple[str, ...] + if node.kind is NodeKind.ROUTER: + if not isinstance(result, str) or result not in node.successors: + raise ValueError( + f"router {node.id!r} returned invalid successor {result!r}" + ) + next_nodes = (result,) + next_state = state_value + else: + if result is None: + next_state = state_value + elif isinstance(result, Mapping): + next_state = dict(result) + else: + raise TypeError( + f"step {node.id!r} must return a mapping or None" + ) + if len(node.successors) > 1: + raise ValueError( + f"step {node.id!r} has multiple successors; use @router" + ) + next_nodes = node.successors + + latest = self.store.latest_checkpoint(run.id) + checkpoint = Checkpoint.create( + run_id=run.id, + sequence=1 if latest is None else latest.sequence + 1, + plan_hash=registration.plan.definition_hash, + state=next_state, + next_nodes=next_nodes, + effect_watermark=0, + ) + run = self.store.commit_checkpoint( + checkpoint, + worker_id=claim.lease.holder_worker_id, + fencing_token=claim.lease.fencing_token, + expected_run_version=run.version, + ) + steps += 1 + + if not run.next_nodes: + return self.store.finish_claim( + run.id, + worker_id=claim.lease.holder_worker_id, + fencing_token=claim.lease.fencing_token, + expected_run_version=run.version, + target=RunStatus.SUCCEEDED, + ) + return run diff --git a/src/rath/runtime/sqlite.py b/src/rath/runtime/sqlite.py index 822d3a1..3fdabc8 100644 --- a/src/rath/runtime/sqlite.py +++ b/src/rath/runtime/sqlite.py @@ -400,6 +400,127 @@ def latest_checkpoint(self, run_id: UUID) -> Checkpoint | None: connection.close() return None if row is None else self._checkpoint_from_row(row) + def list_checkpoints(self, run_id: UUID) -> tuple[Checkpoint, ...]: + connection = self._connect() + try: + rows = connection.execute( + """ + SELECT * FROM checkpoints + WHERE run_id = ? ORDER BY sequence + """, + (str(run_id),), + ).fetchall() + finally: + connection.close() + return tuple(self._checkpoint_from_row(row) for row in rows) + + def commit_checkpoint( + self, + checkpoint: Checkpoint, + *, + worker_id: str, + fencing_token: int, + expected_run_version: int, + ) -> Run: + with self._transaction() as connection: + self._required_lease( + connection, + checkpoint.run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + current = self._run_from_row( + self._required_run_row(connection, checkpoint.run_id) + ) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + if current.status is not RunStatus.RUNNING: + raise ConflictError("checkpoint requires a running run") + row = connection.execute( + """ + SELECT COALESCE(MAX(sequence), 0) AS sequence + FROM checkpoints WHERE run_id = ? + """, + (str(checkpoint.run_id),), + ).fetchone() + expected_sequence = int(row["sequence"]) + 1 + if checkpoint.sequence != expected_sequence: + raise ConflictError( + f"checkpoint sequence must be {expected_sequence}, " + f"got {checkpoint.sequence}" + ) + self._insert_checkpoint(connection, checkpoint) + connection.execute( + """ + UPDATE runs + SET state_json = ?, next_nodes_json = ?, updated_at = ?, + version = version + 1 + WHERE id = ? AND version = ? + """, + ( + _dump(checkpoint.state), + _dump(checkpoint.next_nodes), + checkpoint.created_at.isoformat(), + str(checkpoint.run_id), + expected_run_version, + ), + ) + self._append_event( + connection, + checkpoint.run_id, + "run.checkpoint.created", + { + "checkpoint_id": str(checkpoint.id), + "sequence": checkpoint.sequence, + }, + ) + return self._run_from_row( + self._required_run_row(connection, checkpoint.run_id) + ) + + def finish_claim( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + expected_run_version: int, + target: RunStatus, + event_type: str = "run.execution.completed", + event_data: Mapping[str, object] | None = None, + ) -> Run: + with self._transaction() as connection: + self._required_lease( + connection, + run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + current = self._run_from_row(self._required_run_row(connection, run_id)) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + assert_transition(current.status, target) + self._update_status( + connection, + current, + target=target, + expected_version=expected_run_version, + ) + connection.execute( + """ + UPDATE run_leases SET active = 0, updated_at = ? + WHERE run_id = ? + """, + (_now().isoformat(), str(run_id)), + ) + self._append_event( + connection, + run_id, + event_type, + event_data or {"status": target.value}, + ) + return self._run_from_row(self._required_run_row(connection, run_id)) + def create_interrupt( self, interrupt: Interrupt, @@ -755,6 +876,32 @@ def _update_status( if cursor.rowcount != 1: raise ConflictError("run version conflict") + def _insert_checkpoint( + self, + connection: sqlite3.Connection, + checkpoint: Checkpoint, + ) -> None: + connection.execute( + """ + INSERT INTO checkpoints( + id, run_id, sequence, plan_hash, state_json, + next_nodes_json, pending_interrupts_json, + effect_watermark, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(checkpoint.id), + str(checkpoint.run_id), + checkpoint.sequence, + checkpoint.plan_hash, + _dump(checkpoint.state), + _dump(checkpoint.next_nodes), + _dump(tuple(str(item) for item in checkpoint.pending_interrupts)), + checkpoint.effect_watermark, + checkpoint.created_at.isoformat(), + ), + ) + def _append_event( self, connection: sqlite3.Connection, diff --git a/src/rath/security/__init__.py b/src/rath/security/__init__.py index af4efd8..aba3b45 100644 --- a/src/rath/security/__init__.py +++ b/src/rath/security/__init__.py @@ -50,4 +50,3 @@ "SecurityContext", "TrustLevel", ] - diff --git a/src/rath/security/audit.py b/src/rath/security/audit.py index b553a08..a6caf99 100644 --- a/src/rath/security/audit.py +++ b/src/rath/security/audit.py @@ -7,13 +7,15 @@ from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum -from typing import Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable from uuid import UUID, uuid4 from rath._json import JSONValue, freeze_mapping -from rath.context import RunContext from rath.security.policy import Action, PolicyDecision, ResourceRef +if TYPE_CHECKING: + from rath.context import RunContext + __all__ = [ "AuditEvent", "AuditKind", diff --git a/src/rath/security/policy.py b/src/rath/security/policy.py index 2801b29..9ffb8f0 100644 --- a/src/rath/security/policy.py +++ b/src/rath/security/policy.py @@ -5,12 +5,14 @@ from collections.abc import Mapping from dataclasses import dataclass, field from enum import Enum -from typing import Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable from rath._json import JSONValue, freeze_mapping -from rath.context import RunContext from rath.errors import ErrorCode, RathError +if TYPE_CHECKING: + from rath.context import RunContext + __all__ = [ "Action", "ApprovalRequiredError", @@ -223,4 +225,3 @@ async def authorize( if decision.effect is PolicyEffect.REQUIRE_APPROVAL: raise ApprovalRequiredError(decision) return decision - diff --git a/src/rath/security/secrets.py b/src/rath/security/secrets.py index 4f03964..5d25775 100644 --- a/src/rath/security/secrets.py +++ b/src/rath/security/secrets.py @@ -3,9 +3,10 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable -from rath.context import RunContext +if TYPE_CHECKING: + from rath.context import RunContext __all__ = [ "ResolvedSecret", @@ -61,4 +62,3 @@ async def resolve( *, context: RunContext, ) -> ResolvedSecret: ... - diff --git a/tests/runtime/test_local_runtime.py b/tests/runtime/test_local_runtime.py new file mode 100644 index 0000000..3a97f37 --- /dev/null +++ b/tests/runtime/test_local_runtime.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from pathlib import Path +from uuid import uuid4 + +from rath.context import RunContext +from rath.definition import EffectClass, router, step +from rath.flow import Workflow +from rath.runtime import LocalRuntime, RunStatus, SQLiteRunStore +from rath.session import Session + + +class _Workflow(Workflow): + @step(entry=True, successors=("route",), effects=EffectClass.READ_ONLY) + async def start(self, state, context): # type: ignore[no-untyped-def] + return {**state, "count": state.get("count", 0) + 1} + + @router(successors=("finish", "review")) + def route(self, state): # type: ignore[no-untyped-def] + return "finish" if state["count"] == 1 else "review" + + @step(effects=EffectClass.READ_ONLY) + def finish(self, state, context): # type: ignore[no-untyped-def] + return {**state, "result": "done"} + + @step(effects=EffectClass.READ_ONLY) + def review(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + +def test_local_runtime_executes_and_checkpoints_every_step(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + context = RunContext.local(revision_id=uuid4()) + submitted = runtime.submit( + _Workflow(), + session_id=uuid4(), + context=context, + state={"count": 0}, + idempotency_key="request-1", + ) + + completed = runtime.work_once(worker_id="worker-1") + + assert completed is not None + assert completed.id == submitted.id + assert completed.status is RunStatus.SUCCEEDED + assert completed.state["result"] == "done" + checkpoints = store.list_checkpoints(completed.id) + assert [checkpoint.sequence for checkpoint in checkpoints] == [1, 2, 3] + assert checkpoints[-1].next_nodes == () + + +def test_runtime_resumes_from_committed_checkpoint_after_lease_expiry( + tmp_path: Path, +) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + context = RunContext.local(revision_id=uuid4()) + submitted = runtime.submit( + _Workflow(), + session_id=uuid4(), + context=context, + state={"count": 0}, + ) + partial = runtime.work_once(worker_id="worker-1", max_steps=1, lease_seconds=1) + assert partial is not None + assert partial.status is RunStatus.RUNNING + assert partial.next_nodes == ("route",) + + future = datetime.now(timezone.utc) + timedelta(seconds=2) + assert store.requeue_expired_leases(now=future) == (submitted.id,) + resumed = runtime.work_once(worker_id="worker-2", now=future) + + assert resumed is not None + assert resumed.status is RunStatus.SUCCEEDED + assert resumed.state["count"] == 1 + assert len(store.list_checkpoints(resumed.id)) == 3 + + +def test_runtime_marks_step_exception_failed(tmp_path: Path) -> None: + class _Broken(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def broken(self, state, context): # type: ignore[no-untyped-def] + raise RuntimeError("boom") + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + runtime.submit( + _Broken(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + + failed = runtime.work_once(worker_id="worker-1") + + assert failed is not None + assert failed.status is RunStatus.FAILED + assert any(event.type == "run.execution.failed" for event in store.list_run_events(failed.id)) + From 54b66f3752fa7bd8cd80b146bf9465dce34ca982 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 18:14:40 +0800 Subject: [PATCH 07/22] feat(v2): add governed adapters observability eval and server --- pyproject.toml | 15 ++ src/rath/__init__.py | 6 + src/rath/adapters/__init__.py | 29 ++ src/rath/adapters/context.py | 32 +++ src/rath/adapters/schema.py | 59 ++++ src/rath/adapters/specs.py | 121 +++++++++ src/rath/adapters/tool.py | 92 +++++++ src/rath/client/__init__.py | 4 + src/rath/client/remote.py | 95 +++++++ src/rath/eval/__init__.py | 21 ++ src/rath/eval/models.py | 99 +++++++ src/rath/eval/runner.py | 55 ++++ src/rath/observability/__init__.py | 17 ++ src/rath/observability/core.py | 199 ++++++++++++++ src/rath/observability/redaction.py | 27 ++ src/rath/runtime/local.py | 36 ++- src/rath/server/__init__.py | 5 + src/rath/server/app.py | 254 ++++++++++++++++++ src/rath/server/auth.py | 29 ++ .../conformance/v2/test_adapter_contracts.py | 87 ++++++ tests/eval/test_runner.py | 36 +++ tests/observability/test_telemetry.py | 39 +++ tests/server/test_agent_server.py | 72 +++++ 23 files changed, 1419 insertions(+), 10 deletions(-) create mode 100644 src/rath/adapters/__init__.py create mode 100644 src/rath/adapters/context.py create mode 100644 src/rath/adapters/schema.py create mode 100644 src/rath/adapters/specs.py create mode 100644 src/rath/adapters/tool.py create mode 100644 src/rath/client/__init__.py create mode 100644 src/rath/client/remote.py create mode 100644 src/rath/eval/__init__.py create mode 100644 src/rath/eval/models.py create mode 100644 src/rath/eval/runner.py create mode 100644 src/rath/observability/__init__.py create mode 100644 src/rath/observability/core.py create mode 100644 src/rath/observability/redaction.py create mode 100644 src/rath/server/__init__.py create mode 100644 src/rath/server/app.py create mode 100644 src/rath/server/auth.py create mode 100644 tests/conformance/v2/test_adapter_contracts.py create mode 100644 tests/eval/test_runner.py create mode 100644 tests/observability/test_telemetry.py create mode 100644 tests/server/test_agent_server.py diff --git a/pyproject.toml b/pyproject.toml index 1ac6aee..c144732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,21 @@ opensandbox = [ openviking = [ "openviking>=0.4.7", ] +server = [ + "starlette>=0.47,<1", + "uvicorn>=0.35,<1", + "httpx>=0.28,<1", +] +postgres = [ + "psycopg[binary,pool]>=3.2,<4", +] +redis = [ + "redis>=6,<7", +] +otel = [ + "opentelemetry-api>=1.36,<2", + "opentelemetry-sdk>=1.36,<2", +] [tool.ruff] line-length = 88 diff --git a/src/rath/__init__.py b/src/rath/__init__.py index c4ea9c3..799768c 100644 --- a/src/rath/__init__.py +++ b/src/rath/__init__.py @@ -18,19 +18,25 @@ from typing import Any from rath import ( + adapters, backend, definition, + eval, flow, memory, + observability, runtime, security, ) __all__ = [ "backend", + "adapters", "definition", + "eval", "flow", "memory", + "observability", "runtime", "security", "session", diff --git a/src/rath/adapters/__init__.py b/src/rath/adapters/__init__.py new file mode 100644 index 0000000..7894540 --- /dev/null +++ b/src/rath/adapters/__init__.py @@ -0,0 +1,29 @@ +"""Shared v2 adapter contracts.""" + +from rath.adapters.context import AdapterRequestContext +from rath.adapters.schema import SchemaValidationError, validate_json +from rath.adapters.specs import ( + MemoryNamespace, + ProviderCapability, + ProviderSpec, + SandboxIsolation, + SandboxSpec, + ToolSpec, +) +from rath.adapters.tool import ToolExecutor, ToolHandler, ToolOutputTooLarge + +__all__ = [ + "AdapterRequestContext", + "MemoryNamespace", + "ProviderCapability", + "ProviderSpec", + "SandboxIsolation", + "SandboxSpec", + "SchemaValidationError", + "ToolExecutor", + "ToolHandler", + "ToolOutputTooLarge", + "ToolSpec", + "validate_json", +] + diff --git a/src/rath/adapters/context.py b/src/rath/adapters/context.py new file mode 100644 index 0000000..2346f73 --- /dev/null +++ b/src/rath/adapters/context.py @@ -0,0 +1,32 @@ +"""Uniform request context propagated to every external adapter.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from uuid import UUID + +from rath.context import TraceContext +from rath.security import PolicyConstraints + +__all__ = ["AdapterRequestContext"] + + +@dataclass(frozen=True, slots=True) +class AdapterRequestContext: + run_id: UUID + node_id: str + tenant_id: str + deadline: datetime | None + trace_context: TraceContext + idempotency_key: str | None + policy_constraints: PolicyConstraints + + def __post_init__(self) -> None: + if not self.node_id.strip(): + raise ValueError("adapter node_id must not be empty") + if not self.tenant_id.strip(): + raise ValueError("adapter tenant_id must not be empty") + if self.deadline is not None and self.deadline.tzinfo is None: + raise ValueError("adapter deadline must be timezone-aware") + diff --git a/src/rath/adapters/schema.py b/src/rath/adapters/schema.py new file mode 100644 index 0000000..820cb31 --- /dev/null +++ b/src/rath/adapters/schema.py @@ -0,0 +1,59 @@ +"""Small fail-closed JSON Schema subset used for Tool runtime validation.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +__all__ = ["SchemaValidationError", "validate_json"] + + +class SchemaValidationError(ValueError): + pass + + +def validate_json(value: object, schema: Mapping[str, object], *, path: str = "$") -> None: + expected = schema.get("type") + if expected == "object": + if not isinstance(value, Mapping): + raise SchemaValidationError(f"{path} must be an object") + required = schema.get("required", ()) + if isinstance(required, Sequence) and not isinstance(required, str): + for key in required: + if str(key) not in value: + raise SchemaValidationError(f"{path}.{key} is required") + properties = schema.get("properties", {}) + if isinstance(properties, Mapping): + for key, item in value.items(): + child_schema = properties.get(key) + if isinstance(child_schema, Mapping): + validate_json(item, child_schema, path=f"{path}.{key}") + elif schema.get("additionalProperties") is False: + raise SchemaValidationError(f"{path}.{key} is not allowed") + elif expected == "array": + if not isinstance(value, Sequence) or isinstance(value, (str, bytes)): + raise SchemaValidationError(f"{path} must be an array") + items = schema.get("items") + if isinstance(items, Mapping): + for index, item in enumerate(value): + validate_json(item, items, path=f"{path}[{index}]") + elif expected == "string": + if not isinstance(value, str): + raise SchemaValidationError(f"{path} must be a string") + max_length = schema.get("maxLength") + if isinstance(max_length, int) and len(value) > max_length: + raise SchemaValidationError(f"{path} exceeds maxLength") + elif expected == "integer": + if not isinstance(value, int) or isinstance(value, bool): + raise SchemaValidationError(f"{path} must be an integer") + elif expected == "number": + if not isinstance(value, (int, float)) or isinstance(value, bool): + raise SchemaValidationError(f"{path} must be a number") + elif expected == "boolean" and not isinstance(value, bool): + raise SchemaValidationError(f"{path} must be a boolean") + elif expected == "null" and value is not None: + raise SchemaValidationError(f"{path} must be null") + elif expected not in (None, "object", "array", "string", "integer", "number", "boolean", "null"): + raise SchemaValidationError(f"{path} uses unsupported schema type {expected!r}") + enum = schema.get("enum") + if isinstance(enum, Sequence) and value not in enum: + raise SchemaValidationError(f"{path} is not an allowed enum value") diff --git a/src/rath/adapters/specs.py b/src/rath/adapters/specs.py new file mode 100644 index 0000000..7ec13d6 --- /dev/null +++ b/src/rath/adapters/specs.py @@ -0,0 +1,121 @@ +"""Versioned external adapter specifications without resolved credentials.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Literal + +from rath.definition import EffectClass +from rath.security import SecretRef, TrustLevel + +__all__ = [ + "MemoryNamespace", + "ProviderCapability", + "ProviderSpec", + "SandboxIsolation", + "SandboxSpec", + "ToolSpec", +] + + +class ProviderCapability(str, Enum): + CHAT = "chat" + STREAM = "stream" + TOOLS = "tools" + STRUCTURED_OUTPUT = "structured_output" + EMBEDDING = "embedding" + VISION = "vision" + + +@dataclass(frozen=True, slots=True) +class ProviderSpec: + id: str + kind: str + model: str + credential: SecretRef | None = None + capabilities: frozenset[ProviderCapability] = field(default_factory=frozenset) + connect_timeout_seconds: float = 10.0 + read_timeout_seconds: float = 60.0 + total_timeout_seconds: float = 120.0 + max_concurrency: int = 16 + + def __post_init__(self) -> None: + if not self.id or not self.kind or not self.model: + raise ValueError("provider id, kind, and model are required") + if min( + self.connect_timeout_seconds, + self.read_timeout_seconds, + self.total_timeout_seconds, + ) <= 0: + raise ValueError("provider timeouts must be positive") + if self.max_concurrency < 1: + raise ValueError("provider max_concurrency must be positive") + + +@dataclass(frozen=True, slots=True) +class ToolSpec: + name: str + version: str + input_schema: dict[str, object] + output_schema: dict[str, object] | None = None + effects: EffectClass = EffectClass.NON_IDEMPOTENT + risk: Literal["low", "medium", "high", "critical"] = "high" + timeout_seconds: float = 30.0 + max_output_bytes: int = 1024 * 1024 + requires_approval: bool = False + + def __post_init__(self) -> None: + if not self.name or not self.version: + raise ValueError("tool name and version are required") + if self.timeout_seconds <= 0 or self.max_output_bytes <= 0: + raise ValueError("tool budgets must be positive") + if self.effects is EffectClass.NON_IDEMPOTENT and self.risk in { + "high", + "critical", + }: + object.__setattr__(self, "requires_approval", True) + + +class SandboxIsolation(str, Enum): + TRUSTED_HOST = "trusted_host" + LOCAL_CONTAINER = "local_container" + REMOTE_CONTAINER = "remote_container" + + +@dataclass(frozen=True, slots=True) +class SandboxSpec: + id: str + isolation: SandboxIsolation + image_digest: str | None = None + cpu_limit: float | None = None + memory_bytes: int | None = None + disk_bytes: int | None = None + process_limit: int | None = None + network: Literal["deny", "allowlist", "unrestricted"] = "deny" + allowed_hosts: frozenset[str] = field(default_factory=frozenset) + ttl_seconds: int = 900 + + def __post_init__(self) -> None: + if not self.id: + raise ValueError("sandbox id is required") + if self.ttl_seconds <= 0: + raise ValueError("sandbox ttl_seconds must be positive") + if self.isolation is not SandboxIsolation.TRUSTED_HOST and not self.image_digest: + raise ValueError("container sandboxes require an immutable image_digest") + if self.network == "allowlist" and not self.allowed_hosts: + raise ValueError("network allowlist requires at least one host") + + +@dataclass(frozen=True, slots=True) +class MemoryNamespace: + tenant_id: str + user_id: str | None = None + agent_id: str | None = None + session_id: str | None = None + trust: TrustLevel = TrustLevel.UNTRUSTED + + def __post_init__(self) -> None: + if not self.tenant_id: + raise ValueError("memory namespace tenant_id is required") + diff --git a/src/rath/adapters/tool.py b/src/rath/adapters/tool.py new file mode 100644 index 0000000..4f9313f --- /dev/null +++ b/src/rath/adapters/tool.py @@ -0,0 +1,92 @@ +"""Policy-governed Tool v2 execution boundary.""" + +from __future__ import annotations + +import inspect +import json +from collections.abc import Awaitable, Mapping +from typing import Protocol, cast + +from rath.adapters.context import AdapterRequestContext +from rath.adapters.schema import validate_json +from rath.adapters.specs import ToolSpec +from rath.context import RunContext +from rath.security import ( + Action, + ApprovalRequiredError, + PolicyDecision, + PolicyEffect, + PolicyEngine, + ResourceRef, + authorize, +) + +__all__ = ["ToolExecutor", "ToolHandler", "ToolOutputTooLarge"] + + +class ToolOutputTooLarge(RuntimeError): + pass + + +class ToolHandler(Protocol): + def __call__( + self, + arguments: Mapping[str, object], + context: AdapterRequestContext, + ) -> object | Awaitable[object]: ... + + +class ToolExecutor: + def __init__(self, policy: PolicyEngine) -> None: + self.policy = policy + + async def execute( + self, + spec: ToolSpec, + handler: ToolHandler, + arguments: Mapping[str, object], + *, + adapter_context: AdapterRequestContext, + run_context: RunContext, + approved: bool = False, + ) -> object: + validate_json(arguments, spec.input_schema) + try: + await authorize( + self.policy, + action=Action("tool.execute"), + resource=ResourceRef( + kind="tool", + id=f"{spec.name}@{spec.version}", + tenant_id=adapter_context.tenant_id, + attributes={"risk": spec.risk, "effects": spec.effects.value}, + ), + context=run_context, + ) + except ApprovalRequiredError: + if not approved: + raise + if spec.requires_approval and not approved: + raise ApprovalRequiredError( + PolicyDecision( + effect=PolicyEffect.REQUIRE_APPROVAL, + reason=f"tool {spec.name} requires approval", + policy_id="tool-spec", + ) + ) + result = handler(arguments, adapter_context) + if inspect.isawaitable(result): + result = await cast(Awaitable[object], result) + if spec.output_schema is not None: + validate_json(result, spec.output_schema) + encoded = json.dumps(result, ensure_ascii=False, default=str).encode("utf-8") + limit = min( + spec.max_output_bytes, + adapter_context.policy_constraints.max_output_bytes + or spec.max_output_bytes, + ) + if len(encoded) > limit: + raise ToolOutputTooLarge( + f"tool output is {len(encoded)} bytes; maximum is {limit}" + ) + return result diff --git a/src/rath/client/__init__.py b/src/rath/client/__init__.py new file mode 100644 index 0000000..b781ad8 --- /dev/null +++ b/src/rath/client/__init__.py @@ -0,0 +1,4 @@ +from rath.client.remote import AsyncRemoteClient, RemoteClient + +__all__ = ["AsyncRemoteClient", "RemoteClient"] + diff --git a/src/rath/client/remote.py b/src/rath/client/remote.py new file mode 100644 index 0000000..c88c8c4 --- /dev/null +++ b/src/rath/client/remote.py @@ -0,0 +1,95 @@ +"""Sync and native-async clients for the Agent Server resource API.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from typing import Any, cast + +import httpx + +__all__ = ["AsyncRemoteClient", "RemoteClient"] + + +class RemoteClient: + def __init__(self, base_url: str, *, token: str, timeout: float = 30.0) -> None: + self._client = httpx.Client( + base_url=base_url, + headers={"Authorization": f"Bearer {token}"}, + timeout=timeout, + ) + + def create_run( + self, + *, + assistant_id: str, + session_id: str, + state: dict[str, object] | None = None, + idempotency_key: str | None = None, + ) -> dict[str, Any]: + headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {} + response = self._client.post( + "/v1/runs", + json={ + "assistant_id": assistant_id, + "session_id": session_id, + "state": state or {}, + }, + headers=headers, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def get_run(self, run_id: str) -> dict[str, Any]: + response = self._client.get(f"/v1/runs/{run_id}") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def close(self) -> None: + self._client.close() + + +class AsyncRemoteClient: + def __init__(self, base_url: str, *, token: str, timeout: float = 30.0) -> None: + self._client = httpx.AsyncClient( + base_url=base_url, + headers={"Authorization": f"Bearer {token}"}, + timeout=timeout, + ) + + async def create_run( + self, + *, + assistant_id: str, + session_id: str, + state: dict[str, object] | None = None, + idempotency_key: str | None = None, + ) -> dict[str, Any]: + headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {} + response = await self._client.post( + "/v1/runs", + json={ + "assistant_id": assistant_id, + "session_id": session_id, + "state": state or {}, + }, + headers=headers, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def get_run(self, run_id: str) -> dict[str, Any]: + response = await self._client.get(f"/v1/runs/{run_id}") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def events(self, run_id: str, *, after: int = 0) -> AsyncIterator[dict[str, Any]]: + response = await self._client.get( + f"/v1/runs/{run_id}/events", + params={"after": after}, + ) + response.raise_for_status() + for item in response.json()["items"]: + yield item + + async def aclose(self) -> None: + await self._client.aclose() diff --git a/src/rath/eval/__init__.py b/src/rath/eval/__init__.py new file mode 100644 index 0000000..c4853d3 --- /dev/null +++ b/src/rath/eval/__init__.py @@ -0,0 +1,21 @@ +from rath.eval.models import ( + Dataset, + EvaluationResult, + Evaluator, + Example, + Experiment, + GateDecision, +) +from rath.eval.runner import EvaluationRunner, regression_gate + +__all__ = [ + "Dataset", + "EvaluationResult", + "EvaluationRunner", + "Evaluator", + "Example", + "Experiment", + "GateDecision", + "regression_gate", +] + diff --git a/src/rath/eval/models.py b/src/rath/eval/models.py new file mode 100644 index 0000000..7aa076e --- /dev/null +++ b/src/rath/eval/models.py @@ -0,0 +1,99 @@ +"""Versioned evaluation datasets, results, and experiment summaries.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from enum import Enum +from typing import Protocol, runtime_checkable +from uuid import UUID, uuid4 + +from rath._json import JSONValue, freeze_mapping +from rath.runtime import Run + +__all__ = [ + "Dataset", + "EvaluationResult", + "Evaluator", + "Example", + "Experiment", + "GateDecision", +] + + +@dataclass(frozen=True, slots=True) +class Example: + id: UUID + inputs: Mapping[str, JSONValue] + expected: Mapping[str, JSONValue] + + def __post_init__(self) -> None: + object.__setattr__(self, "inputs", freeze_mapping(self.inputs, field="inputs")) + object.__setattr__( + self, "expected", freeze_mapping(self.expected, field="expected") + ) + + @classmethod + def create( + cls, + inputs: Mapping[str, object], + expected: Mapping[str, object], + ) -> "Example": + return cls( + id=uuid4(), + inputs=freeze_mapping(inputs, field="inputs"), + expected=freeze_mapping(expected, field="expected"), + ) + + +@dataclass(frozen=True, slots=True) +class Dataset: + id: UUID + name: str + version: str + examples: tuple[Example, ...] + + +@dataclass(frozen=True, slots=True) +class EvaluationResult: + evaluator: str + score: float + passed: bool + reason: str + metadata: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not 0 <= self.score <= 1: + raise ValueError("evaluation score must be between 0 and 1") + object.__setattr__( + self, + "metadata", + freeze_mapping(self.metadata, field="evaluation.metadata"), + ) + + +@runtime_checkable +class Evaluator(Protocol): + name: str + + async def evaluate(self, example: Example, run: Run) -> EvaluationResult: ... + + +@dataclass(frozen=True, slots=True) +class Experiment: + id: UUID + dataset_id: UUID + revision_id: UUID + results: tuple[EvaluationResult, ...] + + @property + def mean_score(self) -> float: + if not self.results: + return 0.0 + return sum(result.score for result in self.results) / len(self.results) + + +class GateDecision(str, Enum): + PASS = "pass" + FAIL = "fail" + diff --git a/src/rath/eval/runner.py b/src/rath/eval/runner.py new file mode 100644 index 0000000..46b67bd --- /dev/null +++ b/src/rath/eval/runner.py @@ -0,0 +1,55 @@ +"""Offline evaluation runner and baseline regression gate.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Sequence +from uuid import UUID, uuid4 + +from rath.eval.models import ( + Dataset, + EvaluationResult, + Evaluator, + Example, + Experiment, + GateDecision, +) +from rath.runtime import Run + +__all__ = ["EvaluationRunner", "regression_gate"] + + +class EvaluationRunner: + async def run( + self, + dataset: Dataset, + *, + revision_id: UUID, + execute: Callable[[Example], Awaitable[Run]], + evaluators: Sequence[Evaluator], + ) -> Experiment: + results: list[EvaluationResult] = [] + for example in dataset.examples: + run = await execute(example) + for evaluator in evaluators: + results.append(await evaluator.evaluate(example, run)) + return Experiment( + id=uuid4(), + dataset_id=dataset.id, + revision_id=revision_id, + results=tuple(results), + ) + + +def regression_gate( + candidate: Experiment, + *, + baseline: Experiment, + maximum_regression: float = 0.02, + minimum_score: float = 0.8, +) -> GateDecision: + if candidate.mean_score < minimum_score: + return GateDecision.FAIL + if candidate.mean_score < baseline.mean_score - maximum_regression: + return GateDecision.FAIL + return GateDecision.PASS + diff --git a/src/rath/observability/__init__.py b/src/rath/observability/__init__.py new file mode 100644 index 0000000..8f95d66 --- /dev/null +++ b/src/rath/observability/__init__.py @@ -0,0 +1,17 @@ +from rath.observability.core import ( + GuardedTelemetry, + InMemoryTelemetry, + NoOpTelemetry, + SpanRecord, + Telemetry, +) +from rath.observability.redaction import redact + +__all__ = [ + "InMemoryTelemetry", + "GuardedTelemetry", + "NoOpTelemetry", + "SpanRecord", + "Telemetry", + "redact", +] diff --git a/src/rath/observability/core.py b/src/rath/observability/core.py new file mode 100644 index 0000000..78b0509 --- /dev/null +++ b/src/rath/observability/core.py @@ -0,0 +1,199 @@ +"""Dependency-light OpenTelemetry-compatible tracing and metric hooks.""" + +from __future__ import annotations + +import threading +import time +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Protocol, runtime_checkable + +from rath._json import JSONValue, freeze_mapping +from rath.context import TraceContext + +__all__ = [ + "InMemoryTelemetry", + "NoOpTelemetry", + "GuardedTelemetry", + "SpanRecord", + "Telemetry", +] + + +@dataclass(frozen=True, slots=True) +class SpanRecord: + name: str + trace_id: str + span_id: str + parent_span_id: str | None + started_at: datetime + ended_at: datetime + duration_ms: float + status: str + attributes: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "attributes", + freeze_mapping(self.attributes, field="span.attributes"), + ) + + +@runtime_checkable +class Telemetry(Protocol): + @contextmanager + def span( + self, + name: str, + *, + context: TraceContext, + attributes: Mapping[str, object] | None = None, + ) -> Iterator[None]: ... + + def increment( + self, + name: str, + value: int = 1, + *, + attributes: Mapping[str, str] | None = None, + ) -> None: ... + + +class NoOpTelemetry: + @contextmanager + def span( + self, + name: str, + *, + context: TraceContext, + attributes: Mapping[str, object] | None = None, + ) -> Iterator[None]: + yield + + def increment( + self, + name: str, + value: int = 1, + *, + attributes: Mapping[str, str] | None = None, + ) -> None: + return None + + +class InMemoryTelemetry: + """Reference exporter used by tests and embedded diagnostics.""" + + def __init__(self) -> None: + self._spans: list[SpanRecord] = [] + self._counters: dict[tuple[str, tuple[tuple[str, str], ...]], int] = {} + self._lock = threading.Lock() + + @property + def spans(self) -> tuple[SpanRecord, ...]: + with self._lock: + return tuple(self._spans) + + @property + def counters(self) -> Mapping[tuple[str, tuple[tuple[str, str], ...]], int]: + with self._lock: + return dict(self._counters) + + @contextmanager + def span( + self, + name: str, + *, + context: TraceContext, + attributes: Mapping[str, object] | None = None, + ) -> Iterator[None]: + started_at = datetime.now(timezone.utc) + started = time.perf_counter() + status = "ok" + try: + yield + except BaseException: + status = "error" + raise + finally: + ended_at = datetime.now(timezone.utc) + record = SpanRecord( + name=name, + trace_id=context.trace_id, + span_id=context.span_id, + parent_span_id=None, + started_at=started_at, + ended_at=ended_at, + duration_ms=(time.perf_counter() - started) * 1000.0, + status=status, + attributes=freeze_mapping(attributes, field="span.attributes"), + ) + with self._lock: + self._spans.append(record) + + def increment( + self, + name: str, + value: int = 1, + *, + attributes: Mapping[str, str] | None = None, + ) -> None: + labels = tuple(sorted((attributes or {}).items())) + with self._lock: + key = (name, labels) + self._counters[key] = self._counters.get(key, 0) + value + + +class GuardedTelemetry: + """Failure-isolating wrapper: exporter faults never change application results.""" + + def __init__(self, delegate: Telemetry) -> None: + self.delegate = delegate + + @contextmanager + def span( + self, + name: str, + *, + context: TraceContext, + attributes: Mapping[str, object] | None = None, + ) -> Iterator[None]: + manager = None + try: + manager = self.delegate.span( + name, + context=context, + attributes=attributes, + ) + manager.__enter__() + except Exception: + manager = None + try: + yield + except BaseException as exc: + if manager is not None: + try: + manager.__exit__(type(exc), exc, exc.__traceback__) + except Exception: + pass + raise + else: + if manager is not None: + try: + manager.__exit__(None, None, None) + except Exception: + pass + + def increment( + self, + name: str, + value: int = 1, + *, + attributes: Mapping[str, str] | None = None, + ) -> None: + try: + self.delegate.increment(name, value, attributes=attributes) + except Exception: + pass diff --git a/src/rath/observability/redaction.py b/src/rath/observability/redaction.py new file mode 100644 index 0000000..21fc973 --- /dev/null +++ b/src/rath/observability/redaction.py @@ -0,0 +1,27 @@ +"""Recursive telemetry redaction with safe defaults.""" + +from __future__ import annotations + +from collections.abc import Mapping + +__all__ = ["redact"] + +_SENSITIVE = frozenset( + {"api_key", "authorization", "cookie", "password", "secret", "token"} +) + + +def redact(value: object) -> object: + if isinstance(value, Mapping): + return { + str(key): ( + "" + if any(part in str(key).lower() for part in _SENSITIVE) + else redact(item) + ) + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return [redact(item) for item in value] + return value + diff --git a/src/rath/runtime/local.py b/src/rath/runtime/local.py index 108de56..294a0ac 100644 --- a/src/rath/runtime/local.py +++ b/src/rath/runtime/local.py @@ -12,6 +12,7 @@ from rath._json import thaw_json from rath.context import RunContext from rath.definition import ExecutionPlan, NodeKind, WorkflowCompiler +from rath.observability import GuardedTelemetry, NoOpTelemetry, Telemetry from rath.runtime.models import Checkpoint, ClaimedRun, Run, RunStatus from rath.runtime.sqlite import SQLiteRunStore @@ -35,8 +36,14 @@ class _Registration: class LocalRuntime: """Sync façade over the durable local worker engine.""" - def __init__(self, store: SQLiteRunStore) -> None: + def __init__( + self, + store: SQLiteRunStore, + *, + telemetry: Telemetry | None = None, + ) -> None: self.store = store + self.telemetry = GuardedTelemetry(telemetry or NoOpTelemetry()) self._registrations: dict[UUID, _Registration] = {} self._contexts: dict[UUID, RunContext] = {} @@ -132,16 +139,21 @@ def _execute_claim( worker_id=claim.lease.holder_worker_id, fencing_token=claim.lease.fencing_token, ) - if node.kind is NodeKind.ROUTER: - result = handler(state_value) - else: - result = handler(state_value, step_context) - if inspect.isawaitable(result): - from rath._async.runtime import runtime as async_runtime + with self.telemetry.span( + "openrath.node", + context=context.trace_context, + attributes={"run_id": str(run.id), "node_id": node.id}, + ): + if node.kind is NodeKind.ROUTER: + result = handler(state_value) + else: + result = handler(state_value, step_context) + if inspect.isawaitable(result): + from rath._async.runtime import runtime as async_runtime - result = async_runtime().run( - cast(Coroutine[Any, Any, object], result) - ) + result = async_runtime().run( + cast(Coroutine[Any, Any, object], result) + ) next_nodes: tuple[str, ...] if node.kind is NodeKind.ROUTER: @@ -182,6 +194,10 @@ def _execute_claim( expected_run_version=run.version, ) steps += 1 + self.telemetry.increment( + "openrath.node.completed", + attributes={"kind": node.kind.value}, + ) if not run.next_nodes: return self.store.finish_claim( diff --git a/src/rath/server/__init__.py b/src/rath/server/__init__.py new file mode 100644 index 0000000..a59a984 --- /dev/null +++ b/src/rath/server/__init__.py @@ -0,0 +1,5 @@ +from rath.server.app import AgentServer, create_app +from rath.server.auth import AuthProvider, StaticTokenAuth + +__all__ = ["AgentServer", "AuthProvider", "StaticTokenAuth", "create_app"] + diff --git a/src/rath/server/app.py b/src/rath/server/app.py new file mode 100644 index 0000000..f8c4f7d --- /dev/null +++ b/src/rath/server/app.py @@ -0,0 +1,254 @@ +"""Starlette Agent Server exposing durable Run resources and SSE replay.""" + +from __future__ import annotations + +import json +from collections.abc import AsyncIterator +from dataclasses import dataclass +from uuid import UUID + +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import JSONResponse, Response, StreamingResponse +from starlette.routing import Route + +from rath._json import thaw_json +from rath.context import RunContext +from rath.errors import RathError +from rath.runtime import LocalRuntime, Run, RunStatus, SQLiteRunStore +from rath.security import SecurityContext +from rath.server.auth import AuthProvider + +__all__ = ["AgentServer", "create_app"] + + +def _run_json(run: Run) -> dict[str, object]: + return { + "id": str(run.id), + "plan_id": str(run.plan_id), + "revision_id": str(run.revision_id), + "session_id": str(run.session_id), + "tenant_id": run.tenant_id, + "status": run.status.value, + "state": thaw_json(run.state), + "next_nodes": list(run.next_nodes), + "version": run.version, + "created_at": run.created_at.isoformat(), + "updated_at": run.updated_at.isoformat(), + } + + +@dataclass(frozen=True, slots=True) +class _Assistant: + id: str + workflow: object + revision_id: UUID + + +class AgentServer: + def __init__( + self, + store: SQLiteRunStore, + runtime: LocalRuntime, + *, + auth: AuthProvider, + ) -> None: + self.store = store + self.runtime = runtime + self.auth = auth + self.assistants: dict[str, _Assistant] = {} + self.app = create_app(self) + + def register_assistant( + self, + assistant_id: str, + workflow: object, + *, + revision_id: UUID, + ) -> None: + if not assistant_id: + raise ValueError("assistant_id is required") + self.runtime.register(workflow, revision_id=revision_id) + self.assistants[assistant_id] = _Assistant( + id=assistant_id, + workflow=workflow, + revision_id=revision_id, + ) + + +def create_app(server: AgentServer) -> Starlette: + async def authenticate( + request: Request, + ) -> tuple[SecurityContext | None, JSONResponse | None]: + context = await server.auth.authenticate(request.headers.get("authorization")) + if context is None: + return None, JSONResponse( + {"error": {"code": "security.unauthenticated", "message": "unauthenticated"}}, + status_code=401, + ) + return context, None + + async def live(request: Request) -> Response: + return JSONResponse({"status": "ok"}) + + async def ready(request: Request) -> Response: + try: + server.store.list_runs(tenant_id="__readiness__") + except Exception: + return JSONResponse({"status": "not_ready"}, status_code=503) + return JSONResponse({"status": "ready"}) + + async def info(request: Request) -> Response: + return JSONResponse( + { + "name": "openrath-agent-server", + "api_version": "v1", + "capabilities": ["runs", "events", "sse", "interrupts"], + } + ) + + async def list_assistants(request: Request) -> Response: + context, error = await authenticate(request) + if error: + return error + assert context is not None + return JSONResponse( + { + "items": [ + {"id": item.id, "revision_id": str(item.revision_id)} + for item in server.assistants.values() + ] + } + ) + + async def create_run(request: Request) -> Response: + context, error = await authenticate(request) + if error: + return error + assert context is not None + try: + body = await request.json() + assistant = server.assistants[str(body["assistant_id"])] + session_id = UUID(str(body["session_id"])) + run_context = RunContext( + security=context, + revision_id=assistant.revision_id, + ) + run = server.runtime.submit( + assistant.workflow, + session_id=session_id, + context=run_context, + state=body.get("state") or {}, + idempotency_key=request.headers.get("idempotency-key"), + ) + return JSONResponse(_run_json(run), status_code=201) + except KeyError as exc: + return JSONResponse( + {"error": {"code": "request.invalid_argument", "message": str(exc)}}, + status_code=400, + ) + except (ValueError, TypeError) as exc: + return JSONResponse( + {"error": {"code": "request.invalid_argument", "message": str(exc)}}, + status_code=400, + ) + except RathError as exc: + return JSONResponse({"error": exc.to_dict()}, status_code=409) + + async def get_run(request: Request) -> Response: + context, error = await authenticate(request) + if error: + return error + assert context is not None + try: + run = server.store.get_run(UUID(request.path_params["run_id"])) + except (KeyError, ValueError): + return JSONResponse( + {"error": {"code": "resource.not_found", "message": "run not found"}}, + status_code=404, + ) + if run.tenant_id != context.tenant_id: + return JSONResponse( + {"error": {"code": "resource.not_found", "message": "run not found"}}, + status_code=404, + ) + return JSONResponse(_run_json(run)) + + async def cancel_run(request: Request) -> Response: + context, error = await authenticate(request) + if error: + return error + assert context is not None + try: + run = server.store.get_run(UUID(request.path_params["run_id"])) + if run.tenant_id != context.tenant_id: + raise KeyError + cancelled = server.store.transition_run( + run.id, + expected_version=run.version, + target=RunStatus.CANCELLED, + ) + return JSONResponse(_run_json(cancelled)) + except (KeyError, ValueError): + return JSONResponse( + {"error": {"code": "resource.not_found", "message": "run not found"}}, + status_code=404, + ) + except RathError as exc: + return JSONResponse({"error": exc.to_dict()}, status_code=409) + + async def events(request: Request) -> Response: + context, error = await authenticate(request) + if error: + return error + assert context is not None + try: + run_id = UUID(request.path_params["run_id"]) + run = server.store.get_run(run_id) + if run.tenant_id != context.tenant_id: + raise KeyError + except (KeyError, ValueError): + return JSONResponse( + {"error": {"code": "resource.not_found", "message": "run not found"}}, + status_code=404, + ) + after = int(request.query_params.get("after", "0")) + items = [ + { + "id": str(event.sequence), + "run_id": str(event.run_id), + "sequence": event.sequence, + "type": event.type, + "time": event.created_at.isoformat(), + "data": thaw_json(event.data), + } + for event in server.store.list_run_events(run_id) + if event.sequence > after + ] + return JSONResponse({"items": items}) + + async def stream(request: Request) -> Response: + response = await events(request) + if response.status_code != 200: + return response + payload = json.loads(bytes(response.body)) + + async def generate() -> AsyncIterator[str]: + for item in payload["items"]: + yield f"id: {item['sequence']}\nevent: {item['type']}\ndata: {json.dumps(item, separators=(',', ':'))}\n\n" + + return StreamingResponse(generate(), media_type="text/event-stream") + + return Starlette( + routes=[ + Route("/health/live", live), + Route("/health/ready", ready), + Route("/info", info), + Route("/v1/assistants", list_assistants), + Route("/v1/runs", create_run, methods=["POST"]), + Route("/v1/runs/{run_id}", get_run), + Route("/v1/runs/{run_id}/cancel", cancel_run, methods=["POST"]), + Route("/v1/runs/{run_id}/events", events), + Route("/v1/runs/{run_id}/stream", stream), + ] + ) diff --git a/src/rath/server/auth.py b/src/rath/server/auth.py new file mode 100644 index 0000000..3db8d1a --- /dev/null +++ b/src/rath/server/auth.py @@ -0,0 +1,29 @@ +"""Pluggable Agent Server authentication.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from rath.security import SecurityContext + +__all__ = ["AuthProvider", "StaticTokenAuth"] + + +@runtime_checkable +class AuthProvider(Protocol): + async def authenticate(self, authorization: str | None) -> SecurityContext | None: ... + + +class StaticTokenAuth: + """Reference bearer-token provider for self-hosted deployments and tests.""" + + def __init__(self, tokens: dict[str, SecurityContext]) -> None: + if not tokens: + raise ValueError("at least one authentication token is required") + self._tokens = dict(tokens) + + async def authenticate(self, authorization: str | None) -> SecurityContext | None: + if not authorization or not authorization.startswith("Bearer "): + return None + return self._tokens.get(authorization.removeprefix("Bearer ").strip()) + diff --git a/tests/conformance/v2/test_adapter_contracts.py b/tests/conformance/v2/test_adapter_contracts.py new file mode 100644 index 0000000..12a107b --- /dev/null +++ b/tests/conformance/v2/test_adapter_contracts.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import asyncio +from uuid import uuid4 + +import pytest + +from rath.adapters import ( + AdapterRequestContext, + SchemaValidationError, + ToolExecutor, + ToolOutputTooLarge, + ToolSpec, +) +from rath.context import RunContext +from rath.definition import EffectClass +from rath.security import LocalTrustedPolicy, PolicyConstraints + + +def _contexts(): # type: ignore[no-untyped-def] + run = RunContext.local(revision_id=uuid4()) + adapter = AdapterRequestContext( + run_id=uuid4(), + node_id="tool", + tenant_id="local", + deadline=None, + trace_context=run.trace_context, + idempotency_key="key", + policy_constraints=PolicyConstraints(max_output_bytes=32), + ) + return run, adapter + + +def test_tool_schema_is_validated_before_handler() -> None: + called = False + + def handler(arguments, context): # type: ignore[no-untyped-def] + nonlocal called + called = True + return {"ok": True} + + run, adapter = _contexts() + spec = ToolSpec( + name="search", + version="1", + input_schema={ + "type": "object", + "required": ["query"], + "properties": {"query": {"type": "string"}}, + "additionalProperties": False, + }, + effects=EffectClass.READ_ONLY, + risk="low", + ) + with pytest.raises(SchemaValidationError): + asyncio.run( + ToolExecutor(LocalTrustedPolicy()).execute( + spec, + handler, + {"unknown": True}, + adapter_context=adapter, + run_context=run, + ) + ) + assert called is False + + +def test_tool_output_budget_is_enforced() -> None: + run, adapter = _contexts() + spec = ToolSpec( + name="large", + version="1", + input_schema={"type": "object"}, + effects=EffectClass.READ_ONLY, + risk="low", + ) + with pytest.raises(ToolOutputTooLarge): + asyncio.run( + ToolExecutor(LocalTrustedPolicy()).execute( + spec, + lambda arguments, context: {"data": "x" * 100}, + {}, + adapter_context=adapter, + run_context=run, + ) + ) + diff --git a/tests/eval/test_runner.py b/tests/eval/test_runner.py new file mode 100644 index 0000000..cd0da6f --- /dev/null +++ b/tests/eval/test_runner.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from uuid import uuid4 + +from rath.eval import EvaluationResult, Experiment, GateDecision, regression_gate + + +def _experiment(score: float) -> Experiment: + return Experiment( + id=uuid4(), + dataset_id=uuid4(), + revision_id=uuid4(), + results=( + EvaluationResult( + evaluator="exact", + score=score, + passed=score >= 0.8, + reason="test", + ), + ), + ) + + +def test_regression_gate_blocks_absolute_and_relative_regression() -> None: + baseline = _experiment(0.9) + assert ( + regression_gate(_experiment(0.89), baseline=baseline) is GateDecision.PASS + ) + assert ( + regression_gate(_experiment(0.85), baseline=baseline) is GateDecision.FAIL + ) + assert ( + regression_gate(_experiment(0.79), baseline=_experiment(0.7)) + is GateDecision.FAIL + ) + diff --git a/tests/observability/test_telemetry.py b/tests/observability/test_telemetry.py new file mode 100644 index 0000000..7ef4045 --- /dev/null +++ b/tests/observability/test_telemetry.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from contextlib import contextmanager + +from rath.context import TraceContext +from rath.observability import GuardedTelemetry, InMemoryTelemetry, redact + + +def test_span_records_status_and_correlation() -> None: + telemetry = InMemoryTelemetry() + trace = TraceContext.new() + with telemetry.span("run", context=trace, attributes={"run.status": "running"}): + pass + assert telemetry.spans[0].trace_id == trace.trace_id + assert telemetry.spans[0].status == "ok" + + +def test_redaction_is_recursive() -> None: + assert redact({"api_key": "x", "nested": {"password": "y"}}) == { + "api_key": "", + "nested": {"password": ""}, + } + + +def test_guarded_telemetry_swallows_exporter_failure() -> None: + class Broken: + @contextmanager + def span(self, name, *, context, attributes=None): # type: ignore[no-untyped-def] + yield + raise RuntimeError("export failed") + + def increment(self, name, value=1, *, attributes=None): # type: ignore[no-untyped-def] + raise RuntimeError("export failed") + + guarded = GuardedTelemetry(Broken()) + with guarded.span("run", context=TraceContext.new()): + value = 42 + guarded.increment("counter") + assert value == 42 diff --git a/tests/server/test_agent_server.py b/tests/server/test_agent_server.py new file mode 100644 index 0000000..e4c2d64 --- /dev/null +++ b/tests/server/test_agent_server.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from pathlib import Path +from uuid import uuid4 + +import httpx + +from rath.definition import EffectClass, step +from rath.flow import Workflow +from rath.runtime import LocalRuntime, SQLiteRunStore +from rath.security import Principal, PrincipalKind, SecurityContext +from rath.server import AgentServer, StaticTokenAuth +from rath.session import Session + + +class _Echo(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def echo(self, state, context): # type: ignore[no-untyped-def] + return {**state, "done": True} + + def forward(self, session: Session) -> Session: + return session + + +def test_server_auth_tenant_idempotency_and_sse_sync(tmp_path: Path) -> None: + import asyncio + + async def exercise() -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + tenant = SecurityContext( + principal=Principal(id="user-1", kind=PrincipalKind.USER), + tenant_id="tenant-1", + ) + server = AgentServer( + store, + runtime, + auth=StaticTokenAuth({"token": tenant}), + ) + server.register_assistant("echo", _Echo(), revision_id=uuid4()) + transport = httpx.ASGITransport(app=server.app) + async with httpx.AsyncClient( + transport=transport, + base_url="http://test", + ) as client: + assert (await client.get("/v1/assistants")).status_code == 401 + headers = { + "Authorization": "Bearer token", + "Idempotency-Key": "req-1", + } + body = { + "assistant_id": "echo", + "session_id": str(uuid4()), + "state": {"value": 1}, + } + first = await client.post("/v1/runs", headers=headers, json=body) + second = await client.post("/v1/runs", headers=headers, json=body) + assert first.status_code == 201 + assert second.json()["id"] == first.json()["id"] + runtime.work_once(worker_id="worker") + run_id = first.json()["id"] + fetched = await client.get(f"/v1/runs/{run_id}", headers=headers) + assert fetched.json()["status"] == "succeeded" + stream = await client.get( + f"/v1/runs/{run_id}/stream?after=1", + headers=headers, + ) + assert stream.status_code == 200 + assert "run.checkpoint.created" in stream.text + assert (await client.get("/health/ready")).status_code == 200 + + asyncio.run(exercise()) From e1b21de68d1f3ec2299f42e4d8a56b9f87f2c6e2 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 18:20:46 +0800 Subject: [PATCH 08/22] feat(runtime): add production postgres run store --- pyproject.toml | 2 +- src/rath/runtime/__init__.py | 2 + src/rath/runtime/local.py | 4 +- .../migrations/postgres/0001_initial.sql | 76 ++ src/rath/runtime/postgres.py | 882 ++++++++++++++++++ src/rath/runtime/store.py | 23 + tests/integration/test_postgres_run_store.py | 155 +++ uv.lock | 114 ++- 8 files changed, 1251 insertions(+), 7 deletions(-) create mode 100644 src/rath/runtime/migrations/postgres/0001_initial.sql create mode 100644 src/rath/runtime/postgres.py create mode 100644 tests/integration/test_postgres_run_store.py diff --git a/pyproject.toml b/pyproject.toml index c144732..89b6973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ openviking = [ "openviking>=0.4.7", ] server = [ - "starlette>=0.47,<1", + "starlette>=1.3.1,<2", "uvicorn>=0.35,<1", "httpx>=0.28,<1", ] diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py index baf8481..35d2123 100644 --- a/src/rath/runtime/__init__.py +++ b/src/rath/runtime/__init__.py @@ -16,6 +16,7 @@ RunStatus, assert_transition, ) +from rath.runtime.postgres import PostgresRunStore from rath.runtime.sqlite import SQLiteRunStore from rath.runtime.store import RunStore @@ -30,6 +31,7 @@ "InterruptKind", "InvalidRunTransition", "LocalRuntime", + "PostgresRunStore", "Run", "RunEvent", "RunStatus", diff --git a/src/rath/runtime/local.py b/src/rath/runtime/local.py index 294a0ac..c68a3ef 100644 --- a/src/rath/runtime/local.py +++ b/src/rath/runtime/local.py @@ -14,7 +14,7 @@ from rath.definition import ExecutionPlan, NodeKind, WorkflowCompiler from rath.observability import GuardedTelemetry, NoOpTelemetry, Telemetry from rath.runtime.models import Checkpoint, ClaimedRun, Run, RunStatus -from rath.runtime.sqlite import SQLiteRunStore +from rath.runtime.store import RunStore __all__ = ["LocalRuntime", "StepContext"] @@ -38,7 +38,7 @@ class LocalRuntime: def __init__( self, - store: SQLiteRunStore, + store: RunStore, *, telemetry: Telemetry | None = None, ) -> None: diff --git a/src/rath/runtime/migrations/postgres/0001_initial.sql b/src/rath/runtime/migrations/postgres/0001_initial.sql new file mode 100644 index 0000000..db0162c --- /dev/null +++ b/src/rath/runtime/migrations/postgres/0001_initial.sql @@ -0,0 +1,76 @@ +CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE IF NOT EXISTS runs ( + id UUID PRIMARY KEY, + plan_id UUID NOT NULL, + revision_id UUID NOT NULL, + session_id UUID NOT NULL, + tenant_id TEXT NOT NULL, + status TEXT NOT NULL, + state_json JSONB NOT NULL, + next_nodes_json JSONB NOT NULL, + idempotency_key TEXT, + request_fingerprint TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + version BIGINT NOT NULL, + UNIQUE (tenant_id, idempotency_key) +); + +CREATE INDEX IF NOT EXISTS runs_tenant_status_idx + ON runs (tenant_id, status, created_at); + +CREATE TABLE IF NOT EXISTS run_events ( + run_id UUID NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + sequence BIGINT NOT NULL, + type TEXT NOT NULL, + data_json JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (run_id, sequence) +); + +CREATE TABLE IF NOT EXISTS checkpoints ( + id UUID PRIMARY KEY, + run_id UUID NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + sequence BIGINT NOT NULL, + plan_hash TEXT NOT NULL, + state_json JSONB NOT NULL, + next_nodes_json JSONB NOT NULL, + pending_interrupts_json JSONB NOT NULL, + effect_watermark BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + UNIQUE (run_id, sequence) +); + +CREATE TABLE IF NOT EXISTS interrupts ( + id UUID PRIMARY KEY, + run_id UUID NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + kind TEXT NOT NULL, + request_json JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + decision_kind TEXT, + decision_actor_id TEXT, + decision_reason TEXT, + decision_payload_json JSONB, + decided_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS interrupts_run_pending_idx + ON interrupts (run_id, decided_at); + +CREATE TABLE IF NOT EXISTS run_leases ( + id UUID PRIMARY KEY, + run_id UUID NOT NULL UNIQUE REFERENCES runs(id) ON DELETE CASCADE, + holder_worker_id TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + fencing_token BIGINT NOT NULL, + active BOOLEAN NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS run_leases_expiry_idx + ON run_leases (active, expires_at); diff --git a/src/rath/runtime/postgres.py b/src/rath/runtime/postgres.py new file mode 100644 index 0000000..a990c5e --- /dev/null +++ b/src/rath/runtime/postgres.py @@ -0,0 +1,882 @@ +"""PostgreSQL source-of-truth Run store for multi-worker production mode.""" + +from __future__ import annotations + +import hashlib +import re +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from datetime import datetime, timedelta, timezone +from importlib.resources import files +from typing import Any, cast +from uuid import UUID + +from rath._json import freeze_json, thaw_json +from rath.runtime.models import ( + ApprovalDecision, + ApprovalDecisionKind, + Checkpoint, + ClaimedRun, + ConflictError, + Interrupt, + InterruptKind, + ResourceLease, + Run, + RunEvent, + RunStatus, + assert_transition, +) + +__all__ = ["PostgresRunStore"] + +_SCHEMA_NAME = re.compile(r"^[a-z_][a-z0-9_]{0,62}$") + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _json(value: object) -> object: + from psycopg.types.json import Jsonb + + return Jsonb(thaw_json(freeze_json(value, field="persistence value"))) + + +class PostgresRunStore: + """Transactional Postgres store using row locks and fencing tokens.""" + + def __init__(self, dsn: str, *, schema: str = "openrath") -> None: + if not dsn.strip(): + raise ValueError("dsn must not be empty") + if not _SCHEMA_NAME.fullmatch(schema): + raise ValueError("schema must be a safe lowercase PostgreSQL identifier") + self.dsn = dsn + self.schema = schema + self._closed = False + self._migrate() + + def _connect(self) -> Any: + if self._closed: + raise RuntimeError("PostgresRunStore is closed") + try: + import psycopg + from psycopg import sql + from psycopg.rows import dict_row + except ImportError as exc: + raise RuntimeError( + "Postgres support requires `pip install openrath[postgres]`" + ) from exc + connection = psycopg.connect(self.dsn, row_factory=dict_row) + connection.execute( + sql.SQL("SET search_path TO {}").format(sql.Identifier(self.schema)) + ) + return connection + + def _migrate(self) -> None: + try: + import psycopg + from psycopg import sql + except ImportError as exc: + raise RuntimeError( + "Postgres support requires `pip install openrath[postgres]`" + ) from exc + migration = ( + files("rath.runtime") + .joinpath("migrations/postgres/0001_initial.sql") + .read_text(encoding="utf-8") + ) + with psycopg.connect(self.dsn) as connection: + connection.execute( + sql.SQL("CREATE SCHEMA IF NOT EXISTS {}").format( + sql.Identifier(self.schema) + ) + ) + connection.execute( + sql.SQL("SET search_path TO {}").format(sql.Identifier(self.schema)) + ) + connection.execute(migration) + connection.execute( + """ + INSERT INTO schema_migrations(version, applied_at) + VALUES (1, %s) ON CONFLICT (version) DO NOTHING + """, + (_now(),), + ) + + @contextmanager + def _transaction(self) -> Iterator[Any]: + connection = self._connect() + try: + yield connection + connection.commit() + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + def close(self) -> None: + self._closed = True + + def create_run(self, run: Run) -> Run: + fingerprint = self._fingerprint(run) + with self._transaction() as connection: + if run.idempotency_key is not None: + existing = connection.execute( + """ + SELECT * FROM runs + WHERE tenant_id = %s AND idempotency_key = %s + FOR UPDATE + """, + (run.tenant_id, run.idempotency_key), + ).fetchone() + if existing is not None: + if existing["request_fingerprint"] != fingerprint: + raise ConflictError( + "idempotency key was already used for a different request" + ) + return self._run_from_row(existing) + inserted = connection.execute( + """ + INSERT INTO runs( + id, plan_id, revision_id, session_id, tenant_id, status, + state_json, next_nodes_json, idempotency_key, + request_fingerprint, created_at, updated_at, version + ) VALUES ( + %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s + ) + ON CONFLICT DO NOTHING + RETURNING id + """, + ( + run.id, + run.plan_id, + run.revision_id, + run.session_id, + run.tenant_id, + run.status.value, + _json(run.state), + _json(run.next_nodes), + run.idempotency_key, + fingerprint, + run.created_at, + run.updated_at, + run.version, + ), + ).fetchone() + if inserted is None: + if run.idempotency_key is not None: + existing = connection.execute( + """ + SELECT * FROM runs + WHERE tenant_id = %s AND idempotency_key = %s + """, + (run.tenant_id, run.idempotency_key), + ).fetchone() + if ( + existing is not None + and existing["request_fingerprint"] == fingerprint + ): + return self._run_from_row(existing) + raise ConflictError("run already exists") + self._append_event( + connection, run.id, "run.created", {"status": run.status.value} + ) + return run + + def get_run(self, run_id: UUID) -> Run: + with self._transaction() as connection: + row = connection.execute( + "SELECT * FROM runs WHERE id = %s", (run_id,) + ).fetchone() + if row is None: + raise KeyError(str(run_id)) + return self._run_from_row(row) + + def list_runs(self, *, tenant_id: str) -> tuple[Run, ...]: + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT * FROM runs WHERE tenant_id = %s + ORDER BY created_at, id + """, + (tenant_id,), + ).fetchall() + return tuple(self._run_from_row(row) for row in rows) + + def transition_run( + self, + run_id: UUID, + *, + expected_version: int, + target: RunStatus, + state: Mapping[str, object] | None = None, + next_nodes: tuple[str, ...] | None = None, + ) -> Run: + with self._transaction() as connection: + current = self._run_from_row(self._required_run_row(connection, run_id)) + if current.version != expected_version: + raise ConflictError("run version conflict") + assert_transition(current.status, target) + row = connection.execute( + """ + UPDATE runs SET status = %s, state_json = %s, + next_nodes_json = %s, updated_at = %s, version = version + 1 + WHERE id = %s AND version = %s RETURNING * + """, + ( + target.value, + _json(state if state is not None else current.state), + _json(next_nodes if next_nodes is not None else current.next_nodes), + _now(), + run_id, + expected_version, + ), + ).fetchone() + if row is None: + raise ConflictError("run version conflict") + self._append_event( + connection, + run_id, + "run.state.changed", + {"from": current.status.value, "to": target.value}, + ) + return self._run_from_row(row) + + def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT * FROM run_events + WHERE run_id = %s ORDER BY sequence + """, + (run_id,), + ).fetchall() + return tuple( + RunEvent( + run_id=row["run_id"], + sequence=int(row["sequence"]), + type=row["type"], + data=row["data_json"], + created_at=row["created_at"], + ) + for row in rows + ) + + def append_checkpoint(self, checkpoint: Checkpoint) -> None: + with self._transaction() as connection: + self._required_run_row(connection, checkpoint.run_id, lock=True) + self._validate_checkpoint_sequence(connection, checkpoint) + self._insert_checkpoint(connection, checkpoint) + self._append_event( + connection, + checkpoint.run_id, + "run.checkpoint.created", + {"checkpoint_id": str(checkpoint.id), "sequence": checkpoint.sequence}, + ) + + def latest_checkpoint(self, run_id: UUID) -> Checkpoint | None: + with self._transaction() as connection: + row = connection.execute( + """ + SELECT * FROM checkpoints + WHERE run_id = %s ORDER BY sequence DESC LIMIT 1 + """, + (run_id,), + ).fetchone() + return None if row is None else self._checkpoint_from_row(row) + + def list_checkpoints(self, run_id: UUID) -> tuple[Checkpoint, ...]: + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT * FROM checkpoints + WHERE run_id = %s ORDER BY sequence + """, + (run_id,), + ).fetchall() + return tuple(self._checkpoint_from_row(row) for row in rows) + + def commit_checkpoint( + self, + checkpoint: Checkpoint, + *, + worker_id: str, + fencing_token: int, + expected_run_version: int, + ) -> Run: + with self._transaction() as connection: + self._required_lease( + connection, + checkpoint.run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + current = self._run_from_row( + self._required_run_row(connection, checkpoint.run_id, lock=True) + ) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + if current.status is not RunStatus.RUNNING: + raise ConflictError("checkpoint requires a running run") + self._validate_checkpoint_sequence(connection, checkpoint) + self._insert_checkpoint(connection, checkpoint) + row = connection.execute( + """ + UPDATE runs SET state_json = %s, next_nodes_json = %s, + updated_at = %s, version = version + 1 + WHERE id = %s AND version = %s RETURNING * + """, + ( + _json(checkpoint.state), + _json(checkpoint.next_nodes), + checkpoint.created_at, + checkpoint.run_id, + expected_run_version, + ), + ).fetchone() + if row is None: + raise ConflictError("run version conflict") + self._append_event( + connection, + checkpoint.run_id, + "run.checkpoint.created", + {"checkpoint_id": str(checkpoint.id), "sequence": checkpoint.sequence}, + ) + return self._run_from_row(row) + + def finish_claim( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + expected_run_version: int, + target: RunStatus, + event_type: str = "run.execution.completed", + event_data: Mapping[str, object] | None = None, + ) -> Run: + with self._transaction() as connection: + self._required_lease( + connection, + run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + current = self._run_from_row( + self._required_run_row(connection, run_id, lock=True) + ) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + assert_transition(current.status, target) + row = connection.execute( + """ + UPDATE runs SET status = %s, updated_at = %s, version = version + 1 + WHERE id = %s AND version = %s RETURNING * + """, + (target.value, _now(), run_id, expected_run_version), + ).fetchone() + if row is None: + raise ConflictError("run version conflict") + connection.execute( + """ + UPDATE run_leases SET active = FALSE, updated_at = %s + WHERE run_id = %s + """, + (_now(), run_id), + ) + self._append_event( + connection, + run_id, + event_type, + event_data or {"status": target.value}, + ) + return self._run_from_row(row) + + def create_interrupt( + self, interrupt: Interrupt, *, expected_run_version: int + ) -> Run: + with self._transaction() as connection: + current = self._run_from_row( + self._required_run_row(connection, interrupt.run_id, lock=True) + ) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + assert_transition(current.status, RunStatus.WAITING) + connection.execute( + """ + INSERT INTO interrupts(id, run_id, kind, request_json, created_at) + VALUES (%s, %s, %s, %s, %s) + """, + ( + interrupt.id, + interrupt.run_id, + interrupt.kind.value, + _json(interrupt.request), + interrupt.created_at, + ), + ) + row = self._update_status( + connection, + current, + target=RunStatus.WAITING, + expected_version=expected_run_version, + ) + self._append_event( + connection, + interrupt.run_id, + "run.interrupt.created", + {"interrupt_id": str(interrupt.id), "kind": interrupt.kind.value}, + ) + return self._run_from_row(row) + + def get_interrupt(self, interrupt_id: UUID) -> Interrupt: + with self._transaction() as connection: + row = connection.execute( + "SELECT * FROM interrupts WHERE id = %s", (interrupt_id,) + ).fetchone() + if row is None: + raise KeyError(str(interrupt_id)) + return self._interrupt_from_row(row) + + def decide_interrupt( + self, + interrupt_id: UUID, + *, + decision: ApprovalDecision, + expected_run_version: int, + ) -> Run: + with self._transaction() as connection: + row = connection.execute( + "SELECT * FROM interrupts WHERE id = %s FOR UPDATE", + (interrupt_id,), + ).fetchone() + if row is None: + raise KeyError(str(interrupt_id)) + if row["decision_kind"] is not None: + raise ConflictError("interrupt was already decided") + current = self._run_from_row( + self._required_run_row(connection, row["run_id"], lock=True) + ) + if current.version != expected_run_version: + raise ConflictError("run version conflict") + assert_transition(current.status, RunStatus.QUEUED) + connection.execute( + """ + UPDATE interrupts SET decision_kind = %s, + decision_actor_id = %s, decision_reason = %s, + decision_payload_json = %s, decided_at = %s + WHERE id = %s AND decision_kind IS NULL + """, + ( + decision.kind.value, + decision.actor_id, + decision.reason, + _json(decision.payload), + _now(), + interrupt_id, + ), + ) + updated = self._update_status( + connection, + current, + target=RunStatus.QUEUED, + expected_version=expected_run_version, + ) + self._append_event( + connection, + current.id, + "run.interrupt.decided", + { + "interrupt_id": str(interrupt_id), + "decision": decision.kind.value, + "actor_id": decision.actor_id, + }, + ) + return self._run_from_row(updated) + + def claim_next( + self, + *, + worker_id: str, + lease_seconds: float, + now: datetime | None = None, + ) -> ClaimedRun | None: + if not worker_id.strip(): + raise ValueError("worker_id must not be empty") + if lease_seconds <= 0: + raise ValueError("lease_seconds must be greater than zero") + claimed_at = now or _now() + if claimed_at.tzinfo is None: + raise ValueError("now must be timezone-aware") + with self._transaction() as connection: + row = connection.execute( + """ + SELECT * FROM runs WHERE status = %s + ORDER BY created_at, id FOR UPDATE SKIP LOCKED LIMIT 1 + """, + (RunStatus.QUEUED.value,), + ).fetchone() + if row is None: + return None + current = self._run_from_row(row) + previous = connection.execute( + "SELECT * FROM run_leases WHERE run_id = %s FOR UPDATE", + (current.id,), + ).fetchone() + lease_id = ( + previous["id"] + if previous is not None + else UUID( + bytes=hashlib.sha256(str(current.id).encode()).digest()[:16] + ) + ) + token = int(previous["fencing_token"]) + 1 if previous else 1 + created_at = previous["created_at"] if previous else claimed_at + expires_at = claimed_at + timedelta(seconds=lease_seconds) + updated = connection.execute( + """ + UPDATE runs SET status = %s, updated_at = %s, version = version + 1 + WHERE id = %s AND version = %s AND status = %s RETURNING * + """, + ( + RunStatus.RUNNING.value, + claimed_at, + current.id, + current.version, + RunStatus.QUEUED.value, + ), + ).fetchone() + if updated is None: + raise ConflictError("run claim conflict") + connection.execute( + """ + INSERT INTO run_leases( + id, run_id, holder_worker_id, expires_at, fencing_token, + active, created_at, updated_at + ) VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s) + ON CONFLICT(run_id) DO UPDATE SET + holder_worker_id = excluded.holder_worker_id, + expires_at = excluded.expires_at, + fencing_token = excluded.fencing_token, + active = TRUE, updated_at = excluded.updated_at + """, + ( + lease_id, + current.id, + worker_id, + expires_at, + token, + created_at, + claimed_at, + ), + ) + self._append_event( + connection, + current.id, + "run.claimed", + {"worker_id": worker_id, "fencing_token": token}, + ) + return ClaimedRun( + run=self._run_from_row(updated), + lease=ResourceLease( + id=lease_id, + resource_type="run", + resource_id=str(current.id), + owner_run_id=current.id, + holder_worker_id=worker_id, + expires_at=expires_at, + fencing_token=token, + created_at=created_at, + updated_at=claimed_at, + ), + ) + + def renew_lease( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + lease_seconds: float, + now: datetime | None = None, + ) -> ResourceLease: + if lease_seconds <= 0: + raise ValueError("lease_seconds must be greater than zero") + renewed_at = now or _now() + expires_at = renewed_at + timedelta(seconds=lease_seconds) + with self._transaction() as connection: + row = self._required_lease( + connection, + run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + connection.execute( + """ + UPDATE run_leases SET expires_at = %s, updated_at = %s + WHERE run_id = %s + """, + (expires_at, renewed_at, run_id), + ) + return self._lease_from_row( + {**row, "expires_at": expires_at, "updated_at": renewed_at} + ) + + def assert_fencing_token( + self, run_id: UUID, *, worker_id: str, fencing_token: int + ) -> None: + with self._transaction() as connection: + self._required_lease( + connection, + run_id, + worker_id=worker_id, + fencing_token=fencing_token, + ) + + def requeue_expired_leases( + self, *, now: datetime | None = None + ) -> tuple[UUID, ...]: + recovered_at = now or _now() + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT l.*, r.status, r.version FROM run_leases l + JOIN runs r ON r.id = l.run_id + WHERE l.active = TRUE AND l.expires_at <= %s + ORDER BY l.expires_at, l.run_id FOR UPDATE OF l, r SKIP LOCKED + """, + (recovered_at,), + ).fetchall() + recovered: list[UUID] = [] + for row in rows: + run_id = row["run_id"] + if RunStatus(row["status"]) is RunStatus.RUNNING: + connection.execute( + """ + UPDATE runs SET status = %s, updated_at = %s, + version = version + 1 + WHERE id = %s AND version = %s + """, + ( + RunStatus.QUEUED.value, + recovered_at, + run_id, + row["version"], + ), + ) + self._append_event( + connection, + run_id, + "run.lease.expired", + {"fencing_token": int(row["fencing_token"])}, + ) + recovered.append(run_id) + connection.execute( + """ + UPDATE run_leases SET active = FALSE, updated_at = %s + WHERE run_id = %s + """, + (recovered_at, run_id), + ) + return tuple(recovered) + + def _required_run_row( + self, connection: Any, run_id: UUID, *, lock: bool = False + ) -> Mapping[str, Any]: + suffix = " FOR UPDATE" if lock else "" + row = connection.execute( + f"SELECT * FROM runs WHERE id = %s{suffix}", (run_id,) + ).fetchone() + if row is None: + raise KeyError(str(run_id)) + return cast(Mapping[str, Any], row) + + def _required_lease( + self, + connection: Any, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + ) -> Mapping[str, Any]: + row = connection.execute( + "SELECT * FROM run_leases WHERE run_id = %s FOR UPDATE", (run_id,) + ).fetchone() + if ( + row is None + or not row["active"] + or row["holder_worker_id"] != worker_id + or int(row["fencing_token"]) != fencing_token + ): + raise ConflictError("lease fencing token is stale or not owned") + return cast(Mapping[str, Any], row) + + def _update_status( + self, + connection: Any, + current: Run, + *, + target: RunStatus, + expected_version: int, + ) -> Mapping[str, Any]: + row = connection.execute( + """ + UPDATE runs SET status = %s, updated_at = %s, version = version + 1 + WHERE id = %s AND version = %s RETURNING * + """, + (target.value, _now(), current.id, expected_version), + ).fetchone() + if row is None: + raise ConflictError("run version conflict") + return cast(Mapping[str, Any], row) + + def _validate_checkpoint_sequence( + self, connection: Any, checkpoint: Checkpoint + ) -> None: + row = connection.execute( + """ + SELECT COALESCE(MAX(sequence), 0) AS sequence + FROM checkpoints WHERE run_id = %s + """, + (checkpoint.run_id,), + ).fetchone() + expected = int(row["sequence"]) + 1 + if checkpoint.sequence != expected: + raise ConflictError( + f"checkpoint sequence must be {expected}, got {checkpoint.sequence}" + ) + + def _insert_checkpoint(self, connection: Any, checkpoint: Checkpoint) -> None: + connection.execute( + """ + INSERT INTO checkpoints( + id, run_id, sequence, plan_hash, state_json, + next_nodes_json, pending_interrupts_json, + effect_watermark, created_at + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + checkpoint.id, + checkpoint.run_id, + checkpoint.sequence, + checkpoint.plan_hash, + _json(checkpoint.state), + _json(checkpoint.next_nodes), + _json(tuple(str(item) for item in checkpoint.pending_interrupts)), + checkpoint.effect_watermark, + checkpoint.created_at, + ), + ) + + def _append_event( + self, + connection: Any, + run_id: UUID, + type: str, + data: Mapping[str, object], + ) -> None: + row = connection.execute( + """ + SELECT COALESCE(MAX(sequence), 0) AS sequence + FROM run_events WHERE run_id = %s + """, + (run_id,), + ).fetchone() + connection.execute( + """ + INSERT INTO run_events(run_id, sequence, type, data_json, created_at) + VALUES (%s, %s, %s, %s, %s) + """, + (run_id, int(row["sequence"]) + 1, type, _json(data), _now()), + ) + + def _fingerprint(self, run: Run) -> str: + frozen = freeze_json( + { + "plan_id": str(run.plan_id), + "revision_id": str(run.revision_id), + "session_id": str(run.session_id), + "tenant_id": run.tenant_id, + "state": run.state, + "next_nodes": run.next_nodes, + }, + field="run fingerprint", + ) + import json + + payload = json.dumps( + thaw_json(frozen), + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + return hashlib.sha256(payload.encode()).hexdigest() + + @staticmethod + def _run_from_row(row: Mapping[str, Any]) -> Run: + return Run( + id=row["id"], + plan_id=row["plan_id"], + revision_id=row["revision_id"], + session_id=row["session_id"], + tenant_id=row["tenant_id"], + status=RunStatus(row["status"]), + state=row["state_json"], + next_nodes=tuple(row["next_nodes_json"]), + idempotency_key=row["idempotency_key"], + created_at=row["created_at"], + updated_at=row["updated_at"], + version=int(row["version"]), + ) + + @staticmethod + def _checkpoint_from_row(row: Mapping[str, Any]) -> Checkpoint: + return Checkpoint( + id=row["id"], + run_id=row["run_id"], + sequence=int(row["sequence"]), + plan_hash=row["plan_hash"], + state=row["state_json"], + next_nodes=tuple(row["next_nodes_json"]), + pending_interrupts=tuple( + UUID(item) for item in row["pending_interrupts_json"] + ), + effect_watermark=int(row["effect_watermark"]), + created_at=row["created_at"], + ) + + @staticmethod + def _interrupt_from_row(row: Mapping[str, Any]) -> Interrupt: + decision = None + if row["decision_kind"] is not None: + decision = ApprovalDecision( + kind=ApprovalDecisionKind(row["decision_kind"]), + actor_id=row["decision_actor_id"], + reason=row["decision_reason"], + payload=row["decision_payload_json"], + ) + return Interrupt( + id=row["id"], + run_id=row["run_id"], + kind=InterruptKind(row["kind"]), + request=row["request_json"], + created_at=row["created_at"], + decision=decision, + decided_at=row["decided_at"], + ) + + @staticmethod + def _lease_from_row(row: Mapping[str, Any]) -> ResourceLease: + run_id = row["run_id"] + return ResourceLease( + id=row["id"], + resource_type="run", + resource_id=str(run_id), + owner_run_id=run_id, + holder_worker_id=row["holder_worker_id"], + expires_at=row["expires_at"], + fencing_token=int(row["fencing_token"]), + created_at=row["created_at"], + updated_at=row["updated_at"], + ) diff --git a/src/rath/runtime/store.py b/src/rath/runtime/store.py index 48e793d..b095dca 100644 --- a/src/rath/runtime/store.py +++ b/src/rath/runtime/store.py @@ -45,6 +45,17 @@ def append_checkpoint(self, checkpoint: Checkpoint) -> None: ... def latest_checkpoint(self, run_id: UUID) -> Checkpoint | None: ... + def list_checkpoints(self, run_id: UUID) -> tuple[Checkpoint, ...]: ... + + def commit_checkpoint( + self, + checkpoint: Checkpoint, + *, + worker_id: str, + fencing_token: int, + expected_run_version: int, + ) -> Run: ... + def create_interrupt( self, interrupt: Interrupt, @@ -94,4 +105,16 @@ def requeue_expired_leases( now: datetime | None = None, ) -> tuple[UUID, ...]: ... + def finish_claim( + self, + run_id: UUID, + *, + worker_id: str, + fencing_token: int, + expected_run_version: int, + target: RunStatus, + event_type: str = "run.execution.completed", + event_data: Mapping[str, object] | None = None, + ) -> Run: ... + def close(self) -> None: ... diff --git a/tests/integration/test_postgres_run_store.py b/tests/integration/test_postgres_run_store.py new file mode 100644 index 0000000..b5edcb8 --- /dev/null +++ b/tests/integration/test_postgres_run_store.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import os +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta, timezone +from uuid import uuid4 + +import pytest + +from rath.runtime import ( + ApprovalDecision, + ApprovalDecisionKind, + Checkpoint, + ConflictError, + Interrupt, + InterruptKind, + PostgresRunStore, + Run, + RunStatus, +) + +pytestmark = pytest.mark.skipif( + not os.getenv("OPENRATH_TEST_POSTGRES_DSN"), + reason="OPENRATH_TEST_POSTGRES_DSN is not configured", +) + + +def _run(*, key: str | None = None) -> Run: + return Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="postgres-test", + state={"count": 0}, + next_nodes=("start",), + idempotency_key=key, + ) + + +@pytest.fixture +def store() -> PostgresRunStore: + dsn = os.environ["OPENRATH_TEST_POSTGRES_DSN"] + schema = f"test_{uuid4().hex}" + value = PostgresRunStore(dsn, schema=schema) + yield value + value.close() + import psycopg + from psycopg import sql + + with psycopg.connect(dsn) as connection: + connection.execute( + sql.SQL("DROP SCHEMA {} CASCADE").format(sql.Identifier(schema)) + ) + + +def test_postgres_lifecycle_and_interrupt(store: PostgresRunStore) -> None: + queued = store.create_run(_run(key="lifecycle")) + assert store.create_run(queued) == queued + running = store.transition_run( + queued.id, + expected_version=queued.version, + target=RunStatus.RUNNING, + ) + checkpoint = Checkpoint.create( + run_id=running.id, + sequence=1, + plan_hash="a" * 64, + state={"count": 1}, + next_nodes=("approve",), + effect_watermark=0, + ) + store.append_checkpoint(checkpoint) + assert store.latest_checkpoint(running.id) == checkpoint + interrupt = Interrupt.create( + run_id=running.id, + kind=InterruptKind.APPROVAL, + request={"operation": "email.send"}, + ) + waiting = store.create_interrupt( + interrupt, expected_run_version=running.version + ) + resumed = store.decide_interrupt( + interrupt.id, + decision=ApprovalDecision( + kind=ApprovalDecisionKind.APPROVE, + actor_id="reviewer", + reason="expected operation", + ), + expected_run_version=waiting.version, + ) + + assert resumed.status is RunStatus.QUEUED + assert store.get_interrupt(interrupt.id).decision is not None + assert [event.sequence for event in store.list_run_events(queued.id)] == list( + range(1, len(store.list_run_events(queued.id)) + 1) + ) + + +def test_postgres_idempotency_and_claim_are_concurrency_safe( + store: PostgresRunStore, +) -> None: + candidate = _run(key="same") + with ThreadPoolExecutor(max_workers=8) as pool: + ids = list(pool.map(lambda _: store.create_run(candidate).id, range(16))) + assert set(ids) == {candidate.id} + + with ThreadPoolExecutor(max_workers=8) as pool: + claims = list( + pool.map( + lambda index: store.claim_next( + worker_id=f"worker-{index}", lease_seconds=30 + ), + range(8), + ) + ) + winners = [claim for claim in claims if claim is not None] + assert len(winners) == 1 + assert winners[0].run.status is RunStatus.RUNNING + + +def test_postgres_checkpoint_fencing_and_orphan_recovery( + store: PostgresRunStore, +) -> None: + queued = store.create_run(_run()) + first = store.claim_next(worker_id="worker-1", lease_seconds=1) + assert first is not None + checkpoint = Checkpoint.create( + run_id=queued.id, + sequence=1, + plan_hash="b" * 64, + state={"count": 1}, + next_nodes=(), + effect_watermark=0, + ) + updated = store.commit_checkpoint( + checkpoint, + worker_id="worker-1", + fencing_token=first.lease.fencing_token, + expected_run_version=first.run.version, + ) + future = datetime.now(timezone.utc) + timedelta(seconds=2) + assert store.requeue_expired_leases(now=future) == (queued.id,) + second = store.claim_next( + worker_id="worker-2", lease_seconds=30, now=future + ) + assert second is not None + assert second.lease.fencing_token == 2 + with pytest.raises(ConflictError, match="fencing"): + store.finish_claim( + updated.id, + worker_id="worker-1", + fencing_token=first.lease.fencing_token, + expected_run_version=updated.version, + target=RunStatus.SUCCEEDED, + ) diff --git a/uv.lock b/uv.lock index 9b5accb..3af6d09 100644 --- a/uv.lock +++ b/uv.lock @@ -1845,6 +1845,21 @@ opensandbox = [ openviking = [ { name = "openviking" }, ] +otel = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +postgres = [ + { name = "psycopg", extra = ["binary", "pool"] }, +] +redis = [ + { name = "redis" }, +] +server = [ + { name = "httpx" }, + { name = "starlette" }, + { name = "uvicorn" }, +] [package.dev-dependencies] dev = [ @@ -1868,16 +1883,23 @@ docs = [ [package.metadata] requires-dist = [ { name = "anthropic", specifier = ">=0.40.0" }, + { name = "httpx", marker = "extra == 'server'", specifier = ">=0.28,<1" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.80,<1.88" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "openai", specifier = ">=1.0.0" }, { name = "opensandbox", marker = "extra == 'opensandbox'", specifier = ">=0.1.13" }, { name = "opensandbox-code-interpreter", marker = "extra == 'opensandbox'", specifier = ">=0.1.2" }, { name = "opensandbox-server", marker = "extra == 'opensandbox'", specifier = ">=0.2.1" }, + { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.36,<2" }, + { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.36,<2" }, { name = "openviking", marker = "extra == 'openviking'", specifier = ">=0.4.7" }, + { name = "psycopg", extras = ["binary", "pool"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=6,<7" }, + { name = "starlette", marker = "extra == 'server'", specifier = ">=1.3.1,<2" }, + { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.35,<1" }, ] -provides-extras = ["litellm", "opensandbox", "openviking"] +provides-extras = ["litellm", "opensandbox", "openviking", "server", "postgres", "redis", "otel"] [package.metadata.requires-dev] dev = [ @@ -2395,6 +2417,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/bf/70d8a60488f9955cbbcd538beae44d56bb2f1d19e673b72788f2d343ff55/psycopg_binary-3.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b7bfff1ca23732b488cbca3076fc11bc98d520ee122514fdb17a8e20d3338f5a", size = 4609750, upload-time = "2026-05-01T23:24:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/b0/29e98ba210c9dbc75a6dc91e3f99b9e06ea901a62ca95804e02a1ae13e6b/psycopg_binary-3.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32a6fbf8481e3a370d0d72b860d35948a693cb01281da217f7b2f307636e591a", size = 4676700, upload-time = "2026-05-01T23:25:21.727Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ab/3df087b3c12bf74e47c08204172b2fabb5a144679110d5c7ad12d9201323/psycopg_binary-3.3.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bdef84570ebbce1d42b4e7ea952d21c414c5f118ad02fee00c5625f35e134429", size = 5496319, upload-time = "2026-05-01T23:25:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/f088207b4cd6772f9e0d8a91807e79fa2458d4eb9eb1ae406c68415f2bec/psycopg_binary-3.3.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765", size = 5171906, upload-time = "2026-05-01T23:25:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/4523a857f253871d75c22e1c2e79fd47e599e736bcba1bad58d83e24be02/psycopg_binary-3.3.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf7f73a4a792bc5db58a4b385d8a1467e8d468f7548702fb0ed1e9b7501b1c13", size = 6762621, upload-time = "2026-05-01T23:25:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d1/925bf776503345bef428e6c45fb017d0139ddbe0e211814b585c4253dca8/psycopg_binary-3.3.4-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7b4d40c153fa352ab3cca530f3a0baedf7621b2ebcbd7f084009522c21788fc", size = 5006319, upload-time = "2026-05-01T23:25:51.419Z" }, + { url = "https://files.pythonhosted.org/packages/6f/aa/99727337206fbba357ca084bf4ea8b29dc986f61842a2685859af61416db/psycopg_binary-3.3.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9b1c2533af01cd7648378599f82b0b8ae32f293296e6eec5753a625bc97ef28", size = 4535388, upload-time = "2026-05-01T23:25:57.957Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a4/567ba2c37d19d8c2f63d836385dfd2495aa5897bbee6cfab104d9ee58624/psycopg_binary-3.3.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad3bc94054876155549fdaedf4a46d1ec69d39a5bcee377148afe498e84c4b8e", size = 4224544, upload-time = "2026-05-01T23:26:03.832Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/86457f5a82731685d7701de7bfaa5eb783dd1fecbf875321897d9d9ce33a/psycopg_binary-3.3.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb4eed2079c01a4850bf467deacfab56d356d4225040170af03dc9958321242d", size = 3956282, upload-time = "2026-05-01T23:26:09.983Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d8/249456df16d47de082abd9b73bce8ccdeb0293eb12e590f9150c7cbdb788/psycopg_binary-3.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f80e3f2b5331dbbf0901bcb658056c03eeb2c1ef31d774afb0d61598b242e744", size = 4261736, upload-time = "2026-05-01T23:26:16.798Z" }, + { url = "https://files.pythonhosted.org/packages/15/6b/c4abe228acafd8a385c1fb615d4f1e3c9b8ad7a4e4f0e84118ba3ffeed9c/psycopg_binary-3.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:574ea21a9651958f1535c5a1c649c7409e9168bcbffa29a3f2f961f58b322949", size = 3570620, upload-time = "2026-05-01T23:26:22.655Z" }, + { url = "https://files.pythonhosted.org/packages/b6/82/df3312c0ca083d5b43b352f27d4dd8b1e614bd334473074715d9e0000da4/psycopg_binary-3.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da", size = 4609813, upload-time = "2026-05-01T23:26:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/d74d542458d3e8ac0571d8a88f57ca369999b9a82f4fa528052d0d7d3e4c/psycopg_binary-3.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e", size = 4676799, upload-time = "2026-05-01T23:26:38.475Z" }, + { url = "https://files.pythonhosted.org/packages/09/67/06bab9c60671999f4c6ceff1b334f3ac1f9fc5789eb467c714623ea21de9/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992", size = 5497050, upload-time = "2026-05-01T23:26:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/72/9b/023433e2b20f970de1e22d29132a95281277646da0b2e2879dd4ee94b8c1/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4", size = 5172428, upload-time = "2026-05-01T23:26:56.708Z" }, + { url = "https://files.pythonhosted.org/packages/08/cd/ae16da8fde228a38b2fe9269bbc13cf89e0186173f2265600f02d6a71e64/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e", size = 6762746, upload-time = "2026-05-01T23:27:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/4f/81/0ba09fa5f5f88779093a2541a8e02489825721f258ab88058b11d68b3eb5/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097", size = 5006033, upload-time = "2026-05-01T23:27:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/73/6a/629136040cc3497adb442a305710b5913f2a754d4630fc3d3717c4c0df65/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95", size = 4534175, upload-time = "2026-05-01T23:27:18.248Z" }, + { url = "https://files.pythonhosted.org/packages/7c/32/1027f843c6dc2d5d51960ee62cc0c2cf755a4c39455aff1371173edbef7d/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839", size = 4224203, upload-time = "2026-05-01T23:27:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/380a724d9093c74adb14d4fce920ea8327838abb61f760b1448586b14a8e/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007", size = 3954509, upload-time = "2026-05-01T23:27:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/db/cd/895893ae575a09c97ccfd5def070d88993d955ef34df45a881fd5ff506d6/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c", size = 4259551, upload-time = "2026-05-01T23:27:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/2330a20794e37a3ec609ef2fd8522919ec7a4395a1abf979a8e2d1775cd5/psycopg_binary-3.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d", size = 3572054, upload-time = "2026-05-01T23:27:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" }, +] + [[package]] name = "py" version = "1.11.0" @@ -2866,14 +2972,14 @@ wheels = [ [[package]] name = "redis" -version = "7.4.0" +version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, ] [[package]] From 03c24c27939f7fed5bdaa82735f0bf5ac99a5641 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 18:26:05 +0800 Subject: [PATCH 09/22] feat(runtime): add effect ledger and artifact stores --- pyproject.toml | 3 + src/rath/adapters/tool.py | 56 +- src/rath/artifacts/__init__.py | 17 + src/rath/artifacts/store.py | 355 +++++++++++ src/rath/runtime/__init__.py | 18 + src/rath/runtime/effects.py | 590 ++++++++++++++++++ .../migrations/postgres/0001_initial.sql | 18 + src/rath/runtime/sqlite.py | 18 + tests/artifacts/test_artifact_store.py | 49 ++ tests/integration/test_postgres_run_store.py | 27 + tests/integration/test_s3_artifact_store.py | 51 ++ tests/runtime/test_effect_ledger.py | 108 ++++ uv.lock | 55 +- 13 files changed, 1360 insertions(+), 5 deletions(-) create mode 100644 src/rath/artifacts/__init__.py create mode 100644 src/rath/artifacts/store.py create mode 100644 src/rath/runtime/effects.py create mode 100644 tests/artifacts/test_artifact_store.py create mode 100644 tests/integration/test_s3_artifact_store.py create mode 100644 tests/runtime/test_effect_ledger.py diff --git a/pyproject.toml b/pyproject.toml index 89b6973..2c0b598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,9 @@ otel = [ "opentelemetry-api>=1.36,<2", "opentelemetry-sdk>=1.36,<2", ] +s3 = [ + "boto3>=1.40,<2", +] [tool.ruff] line-length = 88 diff --git a/src/rath/adapters/tool.py b/src/rath/adapters/tool.py index 4f9313f..6e7e514 100644 --- a/src/rath/adapters/tool.py +++ b/src/rath/adapters/tool.py @@ -6,11 +6,17 @@ import json from collections.abc import Awaitable, Mapping from typing import Protocol, cast +from uuid import UUID from rath.adapters.context import AdapterRequestContext from rath.adapters.schema import validate_json from rath.adapters.specs import ToolSpec from rath.context import RunContext +from rath.runtime.effects import ( + EffectLedger, + InvocationStatus, + arguments_digest, +) from rath.security import ( Action, ApprovalRequiredError, @@ -37,8 +43,14 @@ def __call__( class ToolExecutor: - def __init__(self, policy: PolicyEngine) -> None: + def __init__( + self, + policy: PolicyEngine, + *, + effect_ledger: EffectLedger | None = None, + ) -> None: self.policy = policy + self.effect_ledger = effect_ledger async def execute( self, @@ -49,6 +61,8 @@ async def execute( adapter_context: AdapterRequestContext, run_context: RunContext, approved: bool = False, + run_id: UUID | None = None, + idempotency_key: str | None = None, ) -> object: validate_json(arguments, spec.input_schema) try: @@ -74,9 +88,36 @@ async def execute( policy_id="tool-spec", ) ) - result = handler(arguments, adapter_context) - if inspect.isawaitable(result): - result = await cast(Awaitable[object], result) + invocation = None + ledger = self.effect_ledger + if ledger is not None: + if run_id is None: + raise ValueError("run_id is required when effect ledger is enabled") + invocation = ledger.prepare( + run_id=run_id, + tool_name=f"{spec.name}@{spec.version}", + effect_class=spec.effects, + arguments_digest=arguments_digest(arguments), + idempotency_key=idempotency_key, + ) + if invocation.status is InvocationStatus.SUCCEEDED: + return invocation.result + if invocation.status in { + InvocationStatus.AMBIGUOUS, + InvocationStatus.DISPATCHED, + }: + raise RuntimeError( + "tool invocation outcome is ambiguous and requires reconciliation" + ) + invocation = ledger.mark_dispatched(invocation.id) + try: + result = handler(arguments, adapter_context) + if inspect.isawaitable(result): + result = await cast(Awaitable[object], result) + except BaseException as exc: + if invocation is not None and ledger is not None: + ledger.fail(invocation.id, f"{type(exc).__name__}: {exc}") + raise if spec.output_schema is not None: validate_json(result, spec.output_schema) encoded = json.dumps(result, ensure_ascii=False, default=str).encode("utf-8") @@ -86,7 +127,14 @@ async def execute( or spec.max_output_bytes, ) if len(encoded) > limit: + if invocation is not None and ledger is not None: + ledger.fail( + invocation.id, + f"tool output exceeded {limit} bytes", + ) raise ToolOutputTooLarge( f"tool output is {len(encoded)} bytes; maximum is {limit}" ) + if invocation is not None and ledger is not None: + ledger.complete(invocation.id, result) return result diff --git a/src/rath/artifacts/__init__.py b/src/rath/artifacts/__init__.py new file mode 100644 index 0000000..937dc56 --- /dev/null +++ b/src/rath/artifacts/__init__.py @@ -0,0 +1,17 @@ +"""Content-addressed artifact storage.""" + +from rath.artifacts.store import ( + Artifact, + ArtifactNotFound, + ArtifactStore, + LocalArtifactStore, + S3ArtifactStore, +) + +__all__ = [ + "Artifact", + "ArtifactNotFound", + "ArtifactStore", + "LocalArtifactStore", + "S3ArtifactStore", +] diff --git a/src/rath/artifacts/store.py b/src/rath/artifacts/store.py new file mode 100644 index 0000000..65da189 --- /dev/null +++ b/src/rath/artifacts/store.py @@ -0,0 +1,355 @@ +"""Tenant-scoped, content-addressed artifact stores.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import tempfile +from collections.abc import Iterator, Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, BinaryIO, Protocol, cast, runtime_checkable + +from rath._json import JSONValue, freeze_mapping, thaw_json + +__all__ = [ + "Artifact", + "ArtifactNotFound", + "ArtifactStore", + "LocalArtifactStore", + "S3ArtifactStore", +] + +_SCOPE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$") +_SHA256 = re.compile(r"^[0-9a-f]{64}$") + + +class ArtifactNotFound(KeyError): + """Raised when an artifact does not exist in the requested tenant.""" + + +@dataclass(frozen=True, slots=True) +class Artifact: + tenant_id: str + digest: str + size: int + media_type: str + created_at: datetime + metadata: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + _validate_scope(self.tenant_id, field_name="tenant_id") + if not _SHA256.fullmatch(self.digest): + raise ValueError("digest must be a lowercase SHA-256 digest") + if self.size < 0: + raise ValueError("size must not be negative") + if not self.media_type.strip(): + raise ValueError("media_type must not be empty") + if self.created_at.tzinfo is None: + raise ValueError("created_at must be timezone-aware") + object.__setattr__( + self, "metadata", freeze_mapping(self.metadata, field="artifact.metadata") + ) + + @property + def uri(self) -> str: + return f"artifact://{self.tenant_id}/{self.digest}" + + +@runtime_checkable +class ArtifactStore(Protocol): + def put( + self, + tenant_id: str, + content: bytes | BinaryIO, + *, + media_type: str = "application/octet-stream", + metadata: Mapping[str, object] | None = None, + ) -> Artifact: ... + + def get(self, tenant_id: str, digest: str) -> bytes: ... + + def stat(self, tenant_id: str, digest: str) -> Artifact: ... + + def delete(self, tenant_id: str, digest: str) -> bool: ... + + +class _S3Client(Protocol): + def put_object(self, **kwargs: object) -> object: ... + + def get_object(self, **kwargs: object) -> Mapping[str, Any]: ... + + def delete_objects(self, **kwargs: object) -> object: ... + + +def _validate_scope(value: str, *, field_name: str) -> None: + if not _SCOPE.fullmatch(value): + raise ValueError(f"{field_name} contains unsafe characters") + + +def _validate_digest(digest: str) -> None: + if not _SHA256.fullmatch(digest): + raise ValueError("digest must be a lowercase SHA-256 digest") + + +def _chunks(content: bytes | BinaryIO, size: int = 1024 * 1024) -> Iterator[bytes]: + if isinstance(content, bytes): + yield content + return + while chunk := content.read(size): + yield chunk + + +def _manifest(artifact: Artifact) -> bytes: + value = { + "tenant_id": artifact.tenant_id, + "digest": artifact.digest, + "size": artifact.size, + "media_type": artifact.media_type, + "created_at": artifact.created_at.isoformat(), + "metadata": thaw_json(artifact.metadata), + } + return json.dumps(value, sort_keys=True, ensure_ascii=False).encode() + + +def _parse_manifest(value: bytes) -> Artifact: + data = json.loads(value) + return Artifact( + tenant_id=data["tenant_id"], + digest=data["digest"], + size=data["size"], + media_type=data["media_type"], + created_at=datetime.fromisoformat(data["created_at"]), + metadata=data["metadata"], + ) + + +class LocalArtifactStore: + """Atomic filesystem store intended for embedded and single-node operation.""" + + def __init__(self, root: str | Path, *, max_bytes: int = 128 * 1024 * 1024): + if max_bytes < 1: + raise ValueError("max_bytes must be positive") + self.root = Path(root).expanduser().resolve(strict=False) + self.root.mkdir(parents=True, exist_ok=True) + self.max_bytes = max_bytes + + def _paths(self, tenant_id: str, digest: str) -> tuple[Path, Path]: + _validate_scope(tenant_id, field_name="tenant_id") + _validate_digest(digest) + directory = self.root / tenant_id / digest[:2] + payload = directory / digest + manifest = directory / f"{digest}.json" + for path in (directory, payload, manifest): + if not path.resolve(strict=False).is_relative_to(self.root): + raise ValueError("artifact path escapes the configured root") + return payload, manifest + + def put( + self, + tenant_id: str, + content: bytes | BinaryIO, + *, + media_type: str = "application/octet-stream", + metadata: Mapping[str, object] | None = None, + ) -> Artifact: + _validate_scope(tenant_id, field_name="tenant_id") + digest = hashlib.sha256() + total = 0 + temporary: Path | None = None + try: + with tempfile.NamedTemporaryFile( + dir=self.root, prefix=".upload-", delete=False + ) as target: + temporary = Path(target.name) + for chunk in _chunks(content): + total += len(chunk) + if total > self.max_bytes: + raise ValueError("artifact exceeds configured size limit") + digest.update(chunk) + target.write(chunk) + target.flush() + os.fsync(target.fileno()) + artifact = Artifact( + tenant_id=tenant_id, + digest=digest.hexdigest(), + size=total, + media_type=media_type, + created_at=datetime.now(timezone.utc), + metadata=freeze_mapping(metadata, field="artifact.metadata"), + ) + payload, manifest = self._paths(tenant_id, artifact.digest) + payload.parent.mkdir(parents=True, exist_ok=True) + if payload.exists(): + temporary.unlink(missing_ok=True) + else: + temporary.replace(payload) + temporary = None + self._atomic_write(manifest, _manifest(artifact)) + return self.stat(tenant_id, artifact.digest) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + def get(self, tenant_id: str, digest: str) -> bytes: + payload, _ = self._paths(tenant_id, digest) + try: + value = payload.read_bytes() + except FileNotFoundError as exc: + raise ArtifactNotFound(digest) from exc + if not hashlib.sha256(value).hexdigest() == digest: + raise IOError("artifact digest verification failed") + return value + + def stat(self, tenant_id: str, digest: str) -> Artifact: + _, manifest = self._paths(tenant_id, digest) + try: + artifact = _parse_manifest(manifest.read_bytes()) + except FileNotFoundError as exc: + raise ArtifactNotFound(digest) from exc + if artifact.tenant_id != tenant_id or artifact.digest != digest: + raise IOError("artifact manifest identity mismatch") + return artifact + + def delete(self, tenant_id: str, digest: str) -> bool: + payload, manifest = self._paths(tenant_id, digest) + existed = payload.exists() or manifest.exists() + payload.unlink(missing_ok=True) + manifest.unlink(missing_ok=True) + return existed + + @staticmethod + def _atomic_write(path: Path, value: bytes) -> None: + descriptor, name = tempfile.mkstemp(dir=path.parent, prefix=".manifest-") + temporary = Path(name) + try: + with os.fdopen(descriptor, "wb") as target: + target.write(value) + target.flush() + os.fsync(target.fileno()) + temporary.replace(path) + finally: + temporary.unlink(missing_ok=True) + + +class S3ArtifactStore: + """S3-compatible store; durable identity is the SHA-256 object key.""" + + def __init__( + self, + bucket: str, + *, + prefix: str = "openrath", + client: object | None = None, + max_bytes: int = 128 * 1024 * 1024, + **client_options: object, + ) -> None: + _validate_scope(bucket, field_name="bucket") + if not prefix or prefix.startswith("/") or ".." in prefix.split("/"): + raise ValueError("prefix must be a safe relative object prefix") + if max_bytes < 1: + raise ValueError("max_bytes must be positive") + if client is None: + try: + import boto3 # type: ignore[import-not-found] + except ImportError as exc: + raise RuntimeError( + "S3 support requires `pip install openrath[s3]`" + ) from exc + client = boto3.client("s3", **client_options) + self.bucket = bucket + self.prefix = prefix.rstrip("/") + self.client = cast(_S3Client, client) + self.max_bytes = max_bytes + + def _keys(self, tenant_id: str, digest: str) -> tuple[str, str]: + _validate_scope(tenant_id, field_name="tenant_id") + _validate_digest(digest) + base = f"{self.prefix}/{tenant_id}/{digest[:2]}/{digest}" + return base, f"{base}.json" + + def put( + self, + tenant_id: str, + content: bytes | BinaryIO, + *, + media_type: str = "application/octet-stream", + metadata: Mapping[str, object] | None = None, + ) -> Artifact: + data = b"".join(_chunks(content)) + if len(data) > self.max_bytes: + raise ValueError("artifact exceeds configured size limit") + artifact = Artifact( + tenant_id=tenant_id, + digest=hashlib.sha256(data).hexdigest(), + size=len(data), + media_type=media_type, + created_at=datetime.now(timezone.utc), + metadata=freeze_mapping(metadata, field="artifact.metadata"), + ) + payload_key, manifest_key = self._keys(tenant_id, artifact.digest) + self.client.put_object( + Bucket=self.bucket, + Key=payload_key, + Body=data, + ContentType=media_type, + Metadata={"sha256": artifact.digest}, + ) + self.client.put_object( + Bucket=self.bucket, + Key=manifest_key, + Body=_manifest(artifact), + ContentType="application/json", + ) + return artifact + + def get(self, tenant_id: str, digest: str) -> bytes: + payload_key, _ = self._keys(tenant_id, digest) + try: + response = self.client.get_object( + Bucket=self.bucket, Key=payload_key + ) + except Exception as exc: + if _not_found(exc): + raise ArtifactNotFound(digest) from exc + raise + value = cast(bytes, response["Body"].read()) + if hashlib.sha256(value).hexdigest() != digest: + raise IOError("artifact digest verification failed") + return value + + def stat(self, tenant_id: str, digest: str) -> Artifact: + _, manifest_key = self._keys(tenant_id, digest) + try: + response = self.client.get_object( + Bucket=self.bucket, Key=manifest_key + ) + except Exception as exc: + if _not_found(exc): + raise ArtifactNotFound(digest) from exc + raise + artifact = _parse_manifest(response["Body"].read()) + if artifact.tenant_id != tenant_id or artifact.digest != digest: + raise IOError("artifact manifest identity mismatch") + return artifact + + def delete(self, tenant_id: str, digest: str) -> bool: + try: + self.stat(tenant_id, digest) + except ArtifactNotFound: + return False + payload_key, manifest_key = self._keys(tenant_id, digest) + self.client.delete_objects( + Bucket=self.bucket, + Delete={"Objects": [{"Key": payload_key}, {"Key": manifest_key}]}, + ) + return True + + +def _not_found(exc: Exception) -> bool: + response = getattr(exc, "response", {}) + code = response.get("Error", {}).get("Code") if isinstance(response, dict) else None + return str(code) in {"404", "NoSuchKey", "NotFound"} diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py index 35d2123..9ebf6ed 100644 --- a/src/rath/runtime/__init__.py +++ b/src/rath/runtime/__init__.py @@ -1,5 +1,15 @@ """Public durable runtime state and persistence contracts.""" +from rath.runtime.effects import ( + EffectLedger, + InvocationStatus, + PostgresEffectLedger, + Reconciliation, + SQLiteEffectLedger, + ToolInvocation, + arguments_digest, + reconcile_stale_effects, +) from rath.runtime.local import LocalRuntime, StepContext from rath.runtime.models import ( ApprovalDecision, @@ -27,16 +37,24 @@ "Checkpoint", "ClaimedRun", "ConflictError", + "EffectLedger", "Interrupt", "InterruptKind", + "InvocationStatus", "InvalidRunTransition", "LocalRuntime", "PostgresRunStore", + "PostgresEffectLedger", + "Reconciliation", "Run", "RunEvent", "RunStatus", "ResourceLease", "RunStore", "SQLiteRunStore", + "SQLiteEffectLedger", "StepContext", + "ToolInvocation", + "arguments_digest", + "reconcile_stale_effects", ] diff --git a/src/rath/runtime/effects.py b/src/rath/runtime/effects.py new file mode 100644 index 0000000..461c6c7 --- /dev/null +++ b/src/rath/runtime/effects.py @@ -0,0 +1,590 @@ +"""Durable side-effect ledger and crash ambiguity reconciliation.""" + +from __future__ import annotations + +import hashlib +import json +import sqlite3 +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import Any, Protocol, cast, runtime_checkable +from uuid import UUID, uuid4 + +from rath._json import JSONValue, freeze_json, thaw_json +from rath.definition import EffectClass +from rath.runtime.models import ConflictError, RunStatus +from rath.runtime.store import RunStore + +__all__ = [ + "EffectLedger", + "InvocationStatus", + "PostgresEffectLedger", + "Reconciliation", + "SQLiteEffectLedger", + "ToolInvocation", + "arguments_digest", + "reconcile_stale_effects", +] + + +class InvocationStatus(str, Enum): + PREPARED = "prepared" + DISPATCHED = "dispatched" + SUCCEEDED = "succeeded" + FAILED = "failed" + AMBIGUOUS = "ambiguous" + + +@dataclass(frozen=True, slots=True) +class ToolInvocation: + id: UUID + run_id: UUID + tool_name: str + effect_class: EffectClass + arguments_digest: str + status: InvocationStatus + created_at: datetime + updated_at: datetime + idempotency_key: str | None = None + result: JSONValue | None = None + error: str | None = None + + def __post_init__(self) -> None: + if not self.tool_name: + raise ValueError("tool_name must not be empty") + if len(self.arguments_digest) != 64: + raise ValueError("arguments_digest must be a SHA-256 digest") + if self.created_at.tzinfo is None or self.updated_at.tzinfo is None: + raise ValueError("invocation timestamps must be timezone-aware") + + +@dataclass(frozen=True, slots=True) +class Reconciliation: + retryable: tuple[UUID, ...] + needs_review: tuple[UUID, ...] + + +@runtime_checkable +class EffectLedger(Protocol): + def prepare( + self, + *, + run_id: UUID, + tool_name: str, + effect_class: EffectClass, + arguments_digest: str, + idempotency_key: str | None, + ) -> ToolInvocation: ... + + def get(self, invocation_id: UUID) -> ToolInvocation: ... + + def mark_dispatched(self, invocation_id: UUID) -> ToolInvocation: ... + + def complete(self, invocation_id: UUID, result: object) -> ToolInvocation: ... + + def fail(self, invocation_id: UUID, error: str) -> ToolInvocation: ... + + def reconcile_stale( + self, *, older_than: datetime + ) -> tuple[ToolInvocation, ...]: ... + + +def arguments_digest(arguments: Mapping[str, object]) -> str: + frozen = freeze_json(arguments, field="tool arguments") + encoded = json.dumps( + thaw_json(frozen), + sort_keys=True, + ensure_ascii=False, + separators=(",", ":"), + ).encode() + return hashlib.sha256(encoded).hexdigest() + + +def reconcile_stale_effects( + ledger: EffectLedger, + run_store: RunStore, + *, + grace_seconds: float = 30.0, + now: datetime | None = None, +) -> Reconciliation: + """Classify dispatched work after a worker crash. + + Idempotent calls may be retried under their stable key. Non-idempotent calls + are never replayed automatically and move their Run to NEEDS_REVIEW. + """ + + if grace_seconds < 0: + raise ValueError("grace_seconds must not be negative") + current = now or datetime.now(timezone.utc) + stale = ledger.reconcile_stale( + older_than=current - timedelta(seconds=grace_seconds) + ) + retryable: list[UUID] = [] + needs_review: list[UUID] = [] + for invocation in stale: + if invocation.status is InvocationStatus.PREPARED: + retryable.append(invocation.id) + continue + needs_review.append(invocation.id) + run = run_store.get_run(invocation.run_id) + if run.status is RunStatus.RUNNING: + try: + run_store.transition_run( + run.id, + expected_version=run.version, + target=RunStatus.NEEDS_REVIEW, + ) + except ConflictError: + pass + return Reconciliation(tuple(retryable), tuple(needs_review)) + + +class SQLiteEffectLedger: + """Effect ledger sharing the embedded runtime SQLite database.""" + + def __init__(self, path: str) -> None: + self.path = path + + def _connect(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.path, isolation_level=None) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + return connection + + def prepare( + self, + *, + run_id: UUID, + tool_name: str, + effect_class: EffectClass, + arguments_digest: str, + idempotency_key: str | None, + ) -> ToolInvocation: + now = datetime.now(timezone.utc) + invocation = ToolInvocation( + id=uuid4(), + run_id=run_id, + tool_name=tool_name, + effect_class=effect_class, + arguments_digest=arguments_digest, + idempotency_key=idempotency_key, + status=InvocationStatus.PREPARED, + created_at=now, + updated_at=now, + ) + connection = self._connect() + try: + connection.execute("BEGIN IMMEDIATE") + if idempotency_key is not None: + row = connection.execute( + """ + SELECT * FROM tool_invocations + WHERE run_id = ? AND idempotency_key = ? + """, + (str(run_id), idempotency_key), + ).fetchone() + if row is not None: + existing = self._from_row(row) + if ( + existing.arguments_digest != arguments_digest + or existing.tool_name != tool_name + ): + raise ConflictError( + "effect idempotency key was reused with different input" + ) + connection.commit() + return existing + connection.execute( + """ + INSERT INTO tool_invocations( + id, run_id, tool_name, effect_class, idempotency_key, + arguments_digest, status, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(invocation.id), + str(run_id), + tool_name, + effect_class.value, + idempotency_key, + arguments_digest, + invocation.status.value, + now.isoformat(), + now.isoformat(), + ), + ) + connection.commit() + return invocation + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + def get(self, invocation_id: UUID) -> ToolInvocation: + connection = self._connect() + try: + row = connection.execute( + "SELECT * FROM tool_invocations WHERE id = ?", + (str(invocation_id),), + ).fetchone() + finally: + connection.close() + if row is None: + raise KeyError(str(invocation_id)) + return self._from_row(row) + + def mark_dispatched(self, invocation_id: UUID) -> ToolInvocation: + return self._transition( + invocation_id, + expected=(InvocationStatus.PREPARED,), + target=InvocationStatus.DISPATCHED, + ) + + def complete(self, invocation_id: UUID, result: object) -> ToolInvocation: + return self._transition( + invocation_id, + expected=(InvocationStatus.PREPARED, InvocationStatus.DISPATCHED), + target=InvocationStatus.SUCCEEDED, + result=result, + ) + + def fail(self, invocation_id: UUID, error: str) -> ToolInvocation: + return self._transition( + invocation_id, + expected=(InvocationStatus.PREPARED, InvocationStatus.DISPATCHED), + target=InvocationStatus.FAILED, + error=error, + ) + + def reconcile_stale( + self, *, older_than: datetime + ) -> tuple[ToolInvocation, ...]: + connection = self._connect() + try: + connection.execute("BEGIN IMMEDIATE") + rows = connection.execute( + """ + SELECT * FROM tool_invocations + WHERE status = ? AND updated_at <= ? + ORDER BY created_at, id + """, + (InvocationStatus.DISPATCHED.value, older_than.isoformat()), + ).fetchall() + output: list[ToolInvocation] = [] + now = datetime.now(timezone.utc).isoformat() + for row in rows: + effect = EffectClass(row["effect_class"]) + target = ( + InvocationStatus.PREPARED + if effect + in {EffectClass.NONE, EffectClass.READ_ONLY, EffectClass.IDEMPOTENT} + else InvocationStatus.AMBIGUOUS + ) + connection.execute( + """ + UPDATE tool_invocations SET status = ?, updated_at = ? + WHERE id = ? AND status = ? + """, + ( + target.value, + now, + row["id"], + InvocationStatus.DISPATCHED.value, + ), + ) + updated = dict(row) + updated["status"] = target.value + updated["updated_at"] = now + output.append(self._from_row(updated)) + connection.commit() + return tuple(output) + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + def _transition( + self, + invocation_id: UUID, + *, + expected: tuple[InvocationStatus, ...], + target: InvocationStatus, + result: object | None = None, + error: str | None = None, + ) -> ToolInvocation: + connection = self._connect() + try: + connection.execute("BEGIN IMMEDIATE") + row = connection.execute( + "SELECT * FROM tool_invocations WHERE id = ?", + (str(invocation_id),), + ).fetchone() + if row is None: + raise KeyError(str(invocation_id)) + if InvocationStatus(row["status"]) not in expected: + raise ConflictError("invalid effect invocation transition") + now = datetime.now(timezone.utc).isoformat() + result_json = ( + json.dumps( + thaw_json(freeze_json(result, field="tool result")), + ensure_ascii=False, + sort_keys=True, + ) + if result is not None + else None + ) + connection.execute( + """ + UPDATE tool_invocations SET status = ?, result_json = ?, + error = ?, updated_at = ? WHERE id = ? + """, + (target.value, result_json, error, now, str(invocation_id)), + ) + connection.commit() + return self.get(invocation_id) + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + @staticmethod + def _from_row(row: Mapping[str, Any]) -> ToolInvocation: + result = row["result_json"] + return ToolInvocation( + id=UUID(row["id"]), + run_id=UUID(row["run_id"]), + tool_name=row["tool_name"], + effect_class=EffectClass(row["effect_class"]), + idempotency_key=row["idempotency_key"], + arguments_digest=row["arguments_digest"], + status=InvocationStatus(row["status"]), + result=json.loads(result) if isinstance(result, str) else result, + error=row["error"], + created_at=datetime.fromisoformat(row["created_at"]), + updated_at=datetime.fromisoformat(row["updated_at"]), + ) + + +class PostgresEffectLedger: + """Effect ledger sharing a production Postgres Run schema.""" + + def __init__(self, dsn: str, *, schema: str = "openrath") -> None: + from rath.runtime.postgres import PostgresRunStore + + bootstrap = PostgresRunStore(dsn, schema=schema) + bootstrap.close() + self.dsn = dsn + self.schema = schema + + def _connect(self) -> Any: + import psycopg + from psycopg import sql + from psycopg.rows import dict_row + + connection = psycopg.connect(self.dsn, row_factory=dict_row) + connection.execute( + sql.SQL("SET search_path TO {}").format(sql.Identifier(self.schema)) + ) + return connection + + def prepare( + self, + *, + run_id: UUID, + tool_name: str, + effect_class: EffectClass, + arguments_digest: str, + idempotency_key: str | None, + ) -> ToolInvocation: + now = datetime.now(timezone.utc) + invocation_id = uuid4() + connection = self._connect() + try: + row = connection.execute( + """ + INSERT INTO tool_invocations( + id, run_id, tool_name, effect_class, idempotency_key, + arguments_digest, status, created_at, updated_at + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (run_id, idempotency_key) DO NOTHING RETURNING * + """, + ( + invocation_id, + run_id, + tool_name, + effect_class.value, + idempotency_key, + arguments_digest, + InvocationStatus.PREPARED.value, + now, + now, + ), + ).fetchone() + if row is None: + row = connection.execute( + """ + SELECT * FROM tool_invocations + WHERE run_id = %s AND idempotency_key = %s FOR UPDATE + """, + (run_id, idempotency_key), + ).fetchone() + if row is None: + raise ConflictError("effect invocation already exists") + if ( + row["arguments_digest"] != arguments_digest + or row["tool_name"] != tool_name + ): + raise ConflictError( + "effect idempotency key was reused with different input" + ) + connection.commit() + return self._from_row(row) + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + def get(self, invocation_id: UUID) -> ToolInvocation: + connection = self._connect() + try: + row = connection.execute( + "SELECT * FROM tool_invocations WHERE id = %s", (invocation_id,) + ).fetchone() + connection.commit() + finally: + connection.close() + if row is None: + raise KeyError(str(invocation_id)) + return self._from_row(row) + + def mark_dispatched(self, invocation_id: UUID) -> ToolInvocation: + return self._transition( + invocation_id, + expected=(InvocationStatus.PREPARED,), + target=InvocationStatus.DISPATCHED, + ) + + def complete(self, invocation_id: UUID, result: object) -> ToolInvocation: + return self._transition( + invocation_id, + expected=(InvocationStatus.PREPARED, InvocationStatus.DISPATCHED), + target=InvocationStatus.SUCCEEDED, + result=result, + ) + + def fail(self, invocation_id: UUID, error: str) -> ToolInvocation: + return self._transition( + invocation_id, + expected=(InvocationStatus.PREPARED, InvocationStatus.DISPATCHED), + target=InvocationStatus.FAILED, + error=error, + ) + + def reconcile_stale( + self, *, older_than: datetime + ) -> tuple[ToolInvocation, ...]: + connection = self._connect() + try: + rows = connection.execute( + """ + SELECT * FROM tool_invocations + WHERE status = %s AND updated_at <= %s + ORDER BY created_at, id FOR UPDATE SKIP LOCKED + """, + (InvocationStatus.DISPATCHED.value, older_than), + ).fetchall() + output: list[ToolInvocation] = [] + for row in rows: + effect = EffectClass(row["effect_class"]) + target = ( + InvocationStatus.PREPARED + if effect + in {EffectClass.NONE, EffectClass.READ_ONLY, EffectClass.IDEMPOTENT} + else InvocationStatus.AMBIGUOUS + ) + updated = connection.execute( + """ + UPDATE tool_invocations SET status = %s, updated_at = %s + WHERE id = %s AND status = %s RETURNING * + """, + ( + target.value, + datetime.now(timezone.utc), + row["id"], + InvocationStatus.DISPATCHED.value, + ), + ).fetchone() + if updated is not None: + output.append(self._from_row(updated)) + connection.commit() + return tuple(output) + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + def _transition( + self, + invocation_id: UUID, + *, + expected: tuple[InvocationStatus, ...], + target: InvocationStatus, + result: object | None = None, + error: str | None = None, + ) -> ToolInvocation: + from psycopg.types.json import Jsonb + + result_value = ( + thaw_json(freeze_json(result, field="tool result")) + if result is not None + else None + ) + connection = self._connect() + try: + row = connection.execute( + """ + UPDATE tool_invocations SET status = %s, result_json = %s, + error = %s, updated_at = %s + WHERE id = %s AND status = ANY(%s) RETURNING * + """, + ( + target.value, + Jsonb(result_value) if result_value is not None else None, + error, + datetime.now(timezone.utc), + invocation_id, + [item.value for item in expected], + ), + ).fetchone() + if row is None: + raise ConflictError("invalid effect invocation transition") + connection.commit() + return self._from_row(row) + except BaseException: + connection.rollback() + raise + finally: + connection.close() + + @staticmethod + def _from_row(row: Mapping[str, Any]) -> ToolInvocation: + return ToolInvocation( + id=row["id"], + run_id=row["run_id"], + tool_name=row["tool_name"], + effect_class=EffectClass(row["effect_class"]), + idempotency_key=row["idempotency_key"], + arguments_digest=row["arguments_digest"], + status=InvocationStatus(row["status"]), + result=cast(JSONValue | None, row["result_json"]), + error=row["error"], + created_at=row["created_at"], + updated_at=row["updated_at"], + ) diff --git a/src/rath/runtime/migrations/postgres/0001_initial.sql b/src/rath/runtime/migrations/postgres/0001_initial.sql index db0162c..37366a8 100644 --- a/src/rath/runtime/migrations/postgres/0001_initial.sql +++ b/src/rath/runtime/migrations/postgres/0001_initial.sql @@ -74,3 +74,21 @@ CREATE TABLE IF NOT EXISTS run_leases ( CREATE INDEX IF NOT EXISTS run_leases_expiry_idx ON run_leases (active, expires_at); + +CREATE TABLE IF NOT EXISTS tool_invocations ( + id UUID PRIMARY KEY, + run_id UUID NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + tool_name TEXT NOT NULL, + effect_class TEXT NOT NULL, + idempotency_key TEXT, + arguments_digest TEXT NOT NULL, + status TEXT NOT NULL, + result_json JSONB, + error TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE (run_id, idempotency_key) +); + +CREATE INDEX IF NOT EXISTS tool_invocations_reconcile_idx + ON tool_invocations (status, effect_class, updated_at); diff --git a/src/rath/runtime/sqlite.py b/src/rath/runtime/sqlite.py index 3fdabc8..fcf89f4 100644 --- a/src/rath/runtime/sqlite.py +++ b/src/rath/runtime/sqlite.py @@ -108,6 +108,24 @@ CREATE INDEX IF NOT EXISTS run_leases_expiry_idx ON run_leases (active, expires_at); + +CREATE TABLE IF NOT EXISTS tool_invocations ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + tool_name TEXT NOT NULL, + effect_class TEXT NOT NULL, + idempotency_key TEXT, + arguments_digest TEXT NOT NULL, + status TEXT NOT NULL, + result_json TEXT, + error TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE (run_id, idempotency_key) +); + +CREATE INDEX IF NOT EXISTS tool_invocations_reconcile_idx + ON tool_invocations (status, effect_class, updated_at); """ diff --git a/tests/artifacts/test_artifact_store.py b/tests/artifacts/test_artifact_store.py new file mode 100644 index 0000000..3f530f2 --- /dev/null +++ b/tests/artifacts/test_artifact_store.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import io +from pathlib import Path + +import pytest + +from rath.artifacts import ArtifactNotFound, LocalArtifactStore + + +def test_local_artifact_is_content_addressed_and_tenant_scoped( + tmp_path: Path, +) -> None: + store = LocalArtifactStore(tmp_path / "artifacts") + first = store.put( + "tenant-a", + io.BytesIO(b"durable result"), + media_type="text/plain", + metadata={"run_id": "run-1"}, + ) + duplicate = store.put("tenant-a", b"durable result", media_type="text/plain") + + assert first.digest == duplicate.digest + assert first.uri.startswith("artifact://tenant-a/") + assert store.get("tenant-a", first.digest) == b"durable result" + with pytest.raises(ArtifactNotFound): + store.get("tenant-b", first.digest) + + +def test_local_artifact_enforces_size_identity_and_deletion(tmp_path: Path) -> None: + store = LocalArtifactStore(tmp_path / "artifacts", max_bytes=3) + with pytest.raises(ValueError, match="size"): + store.put("tenant", b"four") + with pytest.raises(ValueError, match="unsafe"): + store.put("../tenant", b"x") + + artifact = store.put("tenant", b"one") + payload = ( + tmp_path + / "artifacts" + / "tenant" + / artifact.digest[:2] + / artifact.digest + ) + payload.write_bytes(b"corrupt") + with pytest.raises(OSError, match="verification"): + store.get("tenant", artifact.digest) + assert store.delete("tenant", artifact.digest) + assert not store.delete("tenant", artifact.digest) diff --git a/tests/integration/test_postgres_run_store.py b/tests/integration/test_postgres_run_store.py index b5edcb8..96a34df 100644 --- a/tests/integration/test_postgres_run_store.py +++ b/tests/integration/test_postgres_run_store.py @@ -7,6 +7,7 @@ import pytest +from rath.definition import EffectClass from rath.runtime import ( ApprovalDecision, ApprovalDecisionKind, @@ -14,9 +15,11 @@ ConflictError, Interrupt, InterruptKind, + PostgresEffectLedger, PostgresRunStore, Run, RunStatus, + arguments_digest, ) pytestmark = pytest.mark.skipif( @@ -153,3 +156,27 @@ def test_postgres_checkpoint_fencing_and_orphan_recovery( expected_run_version=updated.version, target=RunStatus.SUCCEEDED, ) + + +def test_postgres_effect_ledger_persists_ambiguous_dispatch( + store: PostgresRunStore, +) -> None: + run = store.create_run(_run()) + running = store.transition_run( + run.id, expected_version=0, target=RunStatus.RUNNING + ) + ledger = PostgresEffectLedger(store.dsn, schema=store.schema) + invocation = ledger.prepare( + run_id=running.id, + tool_name="payment.charge@1", + effect_class=EffectClass.NON_IDEMPOTENT, + arguments_digest=arguments_digest({"amount": 42}), + idempotency_key="charge-42", + ) + dispatched = ledger.mark_dispatched(invocation.id) + + assert dispatched.status.value == "dispatched" + stale = ledger.reconcile_stale( + older_than=datetime.now(timezone.utc) + timedelta(seconds=1) + ) + assert stale[0].status.value == "ambiguous" diff --git a/tests/integration/test_s3_artifact_store.py b/tests/integration/test_s3_artifact_store.py new file mode 100644 index 0000000..990d17c --- /dev/null +++ b/tests/integration/test_s3_artifact_store.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import os +from uuid import uuid4 + +import pytest + +from rath.artifacts import ArtifactNotFound, S3ArtifactStore + +pytestmark = pytest.mark.skipif( + not os.getenv("OPENRATH_TEST_S3_ENDPOINT"), + reason="OPENRATH_TEST_S3_ENDPOINT is not configured", +) + + +def test_s3_artifact_real_lifecycle() -> None: + import boto3 + + endpoint = os.environ["OPENRATH_TEST_S3_ENDPOINT"] + bucket = f"openrath-{uuid4().hex}" + client = boto3.client( + "s3", + endpoint_url=endpoint, + region_name="us-east-1", + aws_access_key_id=os.environ["OPENRATH_TEST_S3_ACCESS_KEY"], + aws_secret_access_key=os.environ["OPENRATH_TEST_S3_SECRET_KEY"], + ) + client.create_bucket(Bucket=bucket) + try: + store = S3ArtifactStore(bucket, client=client) + artifact = store.put( + "tenant-a", + b"object-store-result", + media_type="text/plain", + metadata={"source": "integration"}, + ) + + assert store.stat("tenant-a", artifact.digest) == artifact + assert store.get("tenant-a", artifact.digest) == b"object-store-result" + with pytest.raises(ArtifactNotFound): + store.get("tenant-b", artifact.digest) + assert store.delete("tenant-a", artifact.digest) + assert not store.delete("tenant-a", artifact.digest) + finally: + objects = client.list_objects_v2(Bucket=bucket).get("Contents", []) + if objects: + client.delete_objects( + Bucket=bucket, + Delete={"Objects": [{"Key": item["Key"]} for item in objects]}, + ) + client.delete_bucket(Bucket=bucket) diff --git a/tests/runtime/test_effect_ledger.py b/tests/runtime/test_effect_ledger.py new file mode 100644 index 0000000..95e7c6b --- /dev/null +++ b/tests/runtime/test_effect_ledger.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from pathlib import Path +from uuid import uuid4 + +from rath.definition import EffectClass +from rath.runtime import ( + InvocationStatus, + Run, + RunStatus, + SQLiteEffectLedger, + SQLiteRunStore, + arguments_digest, + reconcile_stale_effects, +) + + +def _running(store: SQLiteRunStore) -> Run: + queued = store.create_run( + Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant", + ) + ) + return store.transition_run( + queued.id, expected_version=0, target=RunStatus.RUNNING + ) + + +def test_completed_effect_is_deduplicated(tmp_path: Path) -> None: + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + run = _running(store) + ledger = SQLiteEffectLedger(str(path)) + digest = arguments_digest({"to": "user@example.test"}) + first = ledger.prepare( + run_id=run.id, + tool_name="email.send@1", + effect_class=EffectClass.NON_IDEMPOTENT, + arguments_digest=digest, + idempotency_key="email-1", + ) + ledger.mark_dispatched(first.id) + completed = ledger.complete(first.id, {"message_id": "m-1"}) + replay = ledger.prepare( + run_id=run.id, + tool_name="email.send@1", + effect_class=EffectClass.NON_IDEMPOTENT, + arguments_digest=digest, + idempotency_key="email-1", + ) + + assert completed.status is InvocationStatus.SUCCEEDED + assert replay == completed + + +def test_crashed_non_idempotent_effect_requires_review(tmp_path: Path) -> None: + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + run = _running(store) + ledger = SQLiteEffectLedger(str(path)) + invocation = ledger.prepare( + run_id=run.id, + tool_name="payment.charge@1", + effect_class=EffectClass.NON_IDEMPOTENT, + arguments_digest=arguments_digest({"amount": 10}), + idempotency_key="charge-1", + ) + ledger.mark_dispatched(invocation.id) + + result = reconcile_stale_effects( + ledger, + store, + grace_seconds=0, + now=datetime.now(timezone.utc) + timedelta(seconds=1), + ) + + assert result.needs_review == (invocation.id,) + assert ledger.get(invocation.id).status is InvocationStatus.AMBIGUOUS + assert store.get_run(run.id).status is RunStatus.NEEDS_REVIEW + + +def test_crashed_idempotent_effect_is_retryable(tmp_path: Path) -> None: + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + run = _running(store) + ledger = SQLiteEffectLedger(str(path)) + invocation = ledger.prepare( + run_id=run.id, + tool_name="object.put@1", + effect_class=EffectClass.IDEMPOTENT, + arguments_digest=arguments_digest({"key": "a"}), + idempotency_key="put-a", + ) + ledger.mark_dispatched(invocation.id) + result = reconcile_stale_effects( + ledger, + store, + grace_seconds=0, + now=datetime.now(timezone.utc) + timedelta(seconds=1), + ) + + assert result.retryable == (invocation.id,) + assert ledger.get(invocation.id).status is InvocationStatus.PREPARED + assert store.get_run(run.id).status is RunStatus.RUNNING diff --git a/uv.lock b/uv.lock index 3af6d09..e117418 100644 --- a/uv.lock +++ b/uv.lock @@ -302,6 +302,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "boto3" +version = "1.43.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/05/23e1aa8c9e4b0399a61e7fd65c4f9cc0625121f24760e37471f776404abb/boto3-1.43.56.tar.gz", hash = "sha256:57c90df9fb026f2e6ae22530861198130203733c5c9ec4e5cca3a4037f5a8db4", size = 112673, upload-time = "2026-07-24T19:31:48.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/57/3a960c9f581c00f2a591901b46e035ff79ab3956d16607f12306b3b8d483/boto3-1.43.56-py3-none-any.whl", hash = "sha256:feb699d4ab241ef5c1b80bb58277be2aaad365cd4b672d7817e0bc59ee45131b", size = 140026, upload-time = "2026-07-24T19:31:47.155Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/cc/7f84a5d3071fe878380e9f610ab36ca87b8cbbc4aa81ba2727f90e1f3ea3/botocore-1.43.56.tar.gz", hash = "sha256:6c01f85f0ff9863076f4c761e74ee3aa96c5ccc1ad09fc1efd62ef8f2d22bf57", size = 15733117, upload-time = "2026-07-24T19:31:38.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/cd/86fe9e659e9699f62f8dd5ecd8c6725474334b23cab8aa71d82b5f56f1a4/botocore-1.43.56-py3-none-any.whl", hash = "sha256:aafc741f1b10f6fd63253eaf6ea029680c1ff436d87e1b8969d62aefa0c76976", size = 15418773, upload-time = "2026-07-24T19:31:34.758Z" }, +] + [[package]] name = "certifi" version = "2026.5.20" @@ -1170,6 +1198,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "json-repair" version = "0.59.10" @@ -1855,6 +1892,9 @@ postgres = [ redis = [ { name = "redis" }, ] +s3 = [ + { name = "boto3" }, +] server = [ { name = "httpx" }, { name = "starlette" }, @@ -1883,6 +1923,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "anthropic", specifier = ">=0.40.0" }, + { name = "boto3", marker = "extra == 's3'", specifier = ">=1.40,<2" }, { name = "httpx", marker = "extra == 'server'", specifier = ">=0.28,<1" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.80,<1.88" }, { name = "mcp", specifier = ">=1.0.0" }, @@ -1899,7 +1940,7 @@ requires-dist = [ { name = "starlette", marker = "extra == 'server'", specifier = ">=1.3.1,<2" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.35,<1" }, ] -provides-extras = ["litellm", "opensandbox", "openviking", "server", "postgres", "redis", "otel"] +provides-extras = ["litellm", "opensandbox", "openviking", "server", "postgres", "redis", "otel", "s3"] [package.metadata.requires-dev] dev = [ @@ -3279,6 +3320,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, +] + [[package]] name = "sgmllib3k" version = "1.0.0" From 7dfd560ce3186bdf2d3716ea62176a95a1548fd6 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 19:33:12 +0800 Subject: [PATCH 10/22] feat(v2): complete production runtime and operations --- .dockerignore | 13 + .github/workflows/ci-v2-production.yml | 86 ++ benchmarks/v2_runtime.py | 85 ++ deploy/compose/compose.yaml | 125 +++ deploy/docs/api-governance-v2.md | 58 ++ deploy/docs/migration-v2.md | 45 ++ deploy/docs/operations-v2.md | 50 ++ deploy/kubernetes/openrath.yaml | 229 ++++++ deploy/kubernetes/secret.example.yaml | 9 + docker/Dockerfile | 31 + examples/v2_server_app.py | 72 ++ pyproject.toml | 7 +- scripts/capacity_v2.py | 56 ++ scripts/migrate_v1_to_v2.py | 143 ++++ scripts/soak_v2.py | 106 +++ src/rath/__init__.py | 4 + src/rath/_async/runtime.py | 4 + src/rath/adapters/__init__.py | 10 +- src/rath/adapters/memory.py | 73 ++ src/rath/adapters/provider.py | 73 ++ src/rath/adapters/sandbox.py | 69 ++ src/rath/adapters/tool.py | 22 + src/rath/client/remote.py | 225 ++++++ src/rath/config/store.py | 4 + src/rath/deployment/__init__.py | 19 + src/rath/deployment/revisions.py | 198 +++++ src/rath/eval/__init__.py | 9 +- src/rath/eval/store.py | 220 +++++ src/rath/observability/__init__.py | 4 + src/rath/observability/logging.py | 54 ++ src/rath/observability/otel.py | 88 ++ src/rath/runtime/__init__.py | 14 + src/rath/runtime/local.py | 386 ++++++++- .../migrations/postgres/0001_initial.sql | 76 ++ src/rath/runtime/models.py | 29 +- src/rath/runtime/postgres.py | 174 +++- src/rath/runtime/signals.py | 137 ++++ src/rath/runtime/sqlite.py | 227 +++++- src/rath/runtime/store.py | 20 + src/rath/server/__init__.py | 24 +- src/rath/server/app.py | 763 +++++++++++++++++- src/rath/server/auth.py | 16 +- src/rath/server/cli.py | 133 +++ src/rath/server/resources.py | 495 ++++++++++++ tests/chaos/test_runtime_failures.py | 87 ++ tests/config/test_credentials_split.py | 20 + .../conformance/v2/test_adapter_contracts.py | 81 +- tests/deployment/test_revisions.py | 26 + tests/eval/test_store.py | 42 + tests/integration/test_postgres_run_store.py | 96 +++ tests/integration/test_redis_signals.py | 27 + .../integration/test_v1_migration_postgres.py | 73 ++ tests/migration/test_v1_to_v2.py | 49 ++ tests/observability/test_telemetry.py | 51 +- tests/runtime/test_local_runtime.py | 176 +++- tests/runtime/test_scheduler_leases.py | 29 +- tests/runtime/test_signals.py | 44 + tests/runtime/test_sqlite_run_store.py | 36 +- tests/server/test_agent_server.py | 186 ++++- uv.lock | 8 +- 60 files changed, 5616 insertions(+), 100 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci-v2-production.yml create mode 100644 benchmarks/v2_runtime.py create mode 100644 deploy/compose/compose.yaml create mode 100644 deploy/docs/api-governance-v2.md create mode 100644 deploy/docs/migration-v2.md create mode 100644 deploy/docs/operations-v2.md create mode 100644 deploy/kubernetes/openrath.yaml create mode 100644 deploy/kubernetes/secret.example.yaml create mode 100644 docker/Dockerfile create mode 100644 examples/v2_server_app.py create mode 100644 scripts/capacity_v2.py create mode 100644 scripts/migrate_v1_to_v2.py create mode 100644 scripts/soak_v2.py create mode 100644 src/rath/adapters/memory.py create mode 100644 src/rath/adapters/provider.py create mode 100644 src/rath/adapters/sandbox.py create mode 100644 src/rath/deployment/__init__.py create mode 100644 src/rath/deployment/revisions.py create mode 100644 src/rath/eval/store.py create mode 100644 src/rath/observability/logging.py create mode 100644 src/rath/observability/otel.py create mode 100644 src/rath/runtime/signals.py create mode 100644 src/rath/server/cli.py create mode 100644 src/rath/server/resources.py create mode 100644 tests/chaos/test_runtime_failures.py create mode 100644 tests/deployment/test_revisions.py create mode 100644 tests/eval/test_store.py create mode 100644 tests/integration/test_redis_signals.py create mode 100644 tests/integration/test_v1_migration_postgres.py create mode 100644 tests/migration/test_v1_to_v2.py create mode 100644 tests/runtime/test_signals.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1defedf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.mypy_cache +.pytest_cache +.ruff_cache +.venv +.workspace +build +dist +docs +tests +**/__pycache__ +*.pyc diff --git a/.github/workflows/ci-v2-production.yml b/.github/workflows/ci-v2-production.yml new file mode 100644 index 0000000..8fcecaa --- /dev/null +++ b/.github/workflows/ci-v2-production.yml @@ -0,0 +1,86 @@ +name: v2 production gates + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + integration: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 55432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 3s + --health-retries 12 + redis: + image: redis:8-alpine + ports: + - 56379:6379 + env: + OPENRATH_TEST_POSTGRES_DSN: postgresql://postgres@127.0.0.1:55432/postgres + OPENRATH_TEST_REDIS_URL: redis://127.0.0.1:56379/0 + OPENRATH_TEST_S3_ENDPOINT: http://127.0.0.1:59000 + OPENRATH_TEST_S3_ACCESS_KEY: openrathtest + OPENRATH_TEST_S3_SECRET_KEY: openrath-test-secret + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - name: Start S3-compatible object store + run: >- + docker run -d --name minio -p 59000:9000 + -e MINIO_ROOT_USER=openrathtest + -e MINIO_ROOT_PASSWORD=openrath-test-secret + minio/minio:RELEASE.2025-09-07T16-13-09Z server /data + - run: uv sync --extra postgres --extra server --extra s3 --extra redis --extra otel + - run: uv lock --check + - run: uv run ruff check src tests scripts examples + - run: uv run mypy src/rath + - run: uv run pytest -q -n auto -m "not live_llm and not opensandbox and not openviking" + - run: uv run python scripts/soak_v2.py --duration-seconds 10 --max-runs 500 + - run: uv build + - name: Validate reference deployments + env: + POSTGRES_PASSWORD: review-only-password + OPENRATH_TOKEN: review-only-token + MINIO_ROOT_PASSWORD: review-only-minio-password + run: | + docker compose -f deploy/compose/compose.yaml config --quiet + docker run --rm \ + -v "$PWD/deploy/kubernetes:/manifests:ro" \ + ghcr.io/yannh/kubeconform:v0.7.0 \ + -strict -summary /manifests + + container: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + push: false + load: true + tags: openrath:review + - uses: aquasecurity/trivy-action@0.32.0 + with: + image-ref: openrath:review + severity: CRITICAL,HIGH + exit-code: "1" + ignore-unfixed: true + - uses: aquasecurity/trivy-action@0.32.0 + with: + image-ref: openrath:review + format: cyclonedx + output: openrath-v2-sbom.cdx.json diff --git a/benchmarks/v2_runtime.py b/benchmarks/v2_runtime.py new file mode 100644 index 0000000..7441bee --- /dev/null +++ b/benchmarks/v2_runtime.py @@ -0,0 +1,85 @@ +"""Reproducible local v2 runtime throughput/latency profile.""" + +from __future__ import annotations + +import argparse +import json +import platform +import statistics +import tempfile +import time +from pathlib import Path +from uuid import uuid4 + +from rath.context import RunContext +from rath.definition import EffectClass, step +from rath.flow import Workflow +from rath.runtime import LocalRuntime, SQLiteRunStore +from rath.session import Session + + +class OneStep(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def execute(self, state, context): # type: ignore[no-untyped-def] + return {"value": state["value"] + 1} + + def forward(self, session: Session) -> Session: + return session + + +def percentile(values: list[float], fraction: float) -> float: + ordered = sorted(values) + return ordered[min(len(ordered) - 1, int(len(ordered) * fraction))] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--runs", type=int, default=500) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.runs < 10: + parser.error("--runs must be at least 10") + latencies: list[float] = [] + started = time.perf_counter() + with tempfile.TemporaryDirectory(prefix="openrath-benchmark-") as directory: + store = SQLiteRunStore(Path(directory) / "runtime.db") + runtime = LocalRuntime(store) + context = RunContext.local(revision_id=uuid4()) + workflow = OneStep() + for index in range(args.runs): + before = time.perf_counter() + runtime.submit( + workflow, + session_id=uuid4(), + context=context, + state={"value": index}, + ) + runtime.work_once(worker_id="benchmark") + latencies.append((time.perf_counter() - before) * 1000) + duration = time.perf_counter() - started + report = { + "schema": "openrath.v2.benchmark/1", + "profile": "sqlite-single-worker-one-step", + "runs": args.runs, + "throughput_runs_per_second": args.runs / duration, + "latency_ms": { + "mean": statistics.mean(latencies), + "p50": percentile(latencies, 0.50), + "p95": percentile(latencies, 0.95), + "p99": percentile(latencies, 0.99), + }, + "environment": { + "python": platform.python_version(), + "platform": platform.platform(), + "processor": platform.processor(), + }, + } + value = json.dumps(report, indent=2) + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(value, encoding="utf-8") + print(value) + + +if __name__ == "__main__": + main() diff --git a/deploy/compose/compose.yaml b/deploy/compose/compose.yaml new file mode 100644 index 0000000..594a4ba --- /dev/null +++ b/deploy/compose/compose.yaml @@ -0,0 +1,125 @@ +name: openrath-v2 + +services: + migrate: + build: + context: ../.. + dockerfile: docker/Dockerfile + image: openrath:2.0.0-review + entrypoint: ["openrath-migrate"] + environment: + OPENRATH_POSTGRES_DSN: postgresql://openrath:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/openrath + depends_on: + postgres: + condition: service_healthy + restart: "no" + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: [ALL] + + api: + build: + context: ../.. + dockerfile: docker/Dockerfile + image: openrath:2.0.0-review + environment: + OPENRATH_APP: examples.v2_server_app:app + OPENRATH_POSTGRES_DSN: postgresql://openrath:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/openrath + OPENRATH_TOKEN: ${OPENRATH_TOKEN:?set OPENRATH_TOKEN} + OPENRATH_TENANT_ID: ${OPENRATH_TENANT_ID:-default} + OPENRATH_EMBEDDED_WORKER: "false" + ports: + - "${OPENRATH_PORT:-8000}:8000" + depends_on: + migrate: + condition: service_completed_successfully + healthcheck: + test: + - CMD + - python + - -c + - "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)" + interval: 10s + timeout: 3s + retries: 6 + start_period: 10s + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=128m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: [ALL] + + worker: + build: + context: ../.. + dockerfile: docker/Dockerfile + image: openrath:2.0.0-review + entrypoint: ["openrath-worker"] + command: ["--app", "examples.v2_server_app:server"] + environment: + OPENRATH_POSTGRES_DSN: postgresql://openrath:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/openrath + OPENRATH_TOKEN: ${OPENRATH_TOKEN:?set OPENRATH_TOKEN} + OPENRATH_TENANT_ID: ${OPENRATH_TENANT_ID:-default} + OPENRATH_EMBEDDED_WORKER: "false" + depends_on: + migrate: + condition: service_completed_successfully + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=128m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: [ALL] + + postgres: + image: postgres:17-alpine + environment: + POSTGRES_DB: openrath + POSTGRES_USER: openrath + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U openrath -d openrath"] + interval: 5s + timeout: 3s + retries: 12 + restart: unless-stopped + + redis: + image: redis:8-alpine + command: ["redis-server", "--save", "", "--appendonly", "no"] + profiles: ["signals"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 12 + restart: unless-stopped + read_only: true + tmpfs: + - /data:size=64m + security_opt: + - no-new-privileges:true + cap_drop: [ALL] + + minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + command: ["server", "/data", "--console-address", ":9001"] + profiles: ["artifacts"] + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-openrath-local} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD} + volumes: + - minio-data:/data + restart: unless-stopped + +volumes: + postgres-data: + minio-data: diff --git a/deploy/docs/api-governance-v2.md b/deploy/docs/api-governance-v2.md new file mode 100644 index 0000000..c7fbd84 --- /dev/null +++ b/deploy/docs/api-governance-v2.md @@ -0,0 +1,58 @@ +# OpenRath v2 API and maintenance policy + +This policy is part of the v2.0.0 review candidate and becomes effective only +when the repository owner approves the release. + +## Stability levels + +- **Stable**: documented public Python symbols, the `/v1` HTTP resource model, + persisted v2 schemas, stable error codes, and migration CLI. Breaking changes + require a new major version. +- **Beta**: explicitly labelled evaluation, deployment-helper, or adapter SDK + surfaces. A breaking change requires release notes, a migration path, and at + least one minor release of notice. +- **Experimental**: explicitly labelled research integrations and extension + hooks. They may change in a minor release and must not be required for the + durable Run, security, or storage contracts. +- Unlabelled public APIs are treated as Stable. Internal modules and names + beginning with `_` are not public contracts. + +## SemVer and deprecation + +- Patch releases contain compatible fixes, security updates, and documentation. +- Minor releases may add compatible functionality. +- Breaking Stable API or persisted-schema changes require a new major release. +- Stable APIs are deprecated before removal. The default notice is two minor + releases and at least six months, unless retaining the API would preserve an + actively exploitable vulnerability. +- Database migrations are forward-only and additive during the rollback + window. Application rollback precedes removal of old columns or tables. + +## v1 compatibility + +The v1 Python facade remains available through the owner-approved maintenance +window. v1 JSONL Sessions import as non-resumable historical evidence because +they do not contain a durable program counter or effect outcome. The exact end +date for v1 security and critical-fix support must be approved in +`review/v2.0.0/release-approval.md`; the implementation does not invent that +organizational commitment. + +## Security reporting + +Report suspected vulnerabilities privately through the repository's GitHub +Security Advisory flow. Do not open a public issue with exploit details, +credentials, tenant data, or unredacted traces. Maintainers should acknowledge, +triage severity, coordinate a fix and advisory, and publish remediation and +affected-version information. Secrets found in reports must be rotated rather +than copied into tests or logs. + +## Release evidence + +Every release candidate must link: + +- compatibility, migration, and rollback evidence; +- unit, conformance, real-backend, chaos, and tenant/security tests; +- a reproducible image digest and dependency lock; +- a CycloneDX SBOM and zero-unaccepted HIGH/CRITICAL scan; +- hardware-bound benchmark and soak profiles; +- explicit owner approval for tag, push, image publication, and deployment. diff --git a/deploy/docs/migration-v2.md b/deploy/docs/migration-v2.md new file mode 100644 index 0000000..4e065fd --- /dev/null +++ b/deploy/docs/migration-v2.md @@ -0,0 +1,45 @@ +# OpenRath v1 → v2 migration + +OpenRath v2 uses durable Runs, checkpoints, effect ledgers, immutable +revisions, and tenant-scoped resources. A v1 JSONL Session contains a +transcript but no reliable program counter or external-side-effect outcome. +It is therefore imported as historical evidence and is never resumed as an +active v2 Run. + +## Safe procedure + +1. Stop v1 writers or take a filesystem snapshot. Keep the original data + read-only throughout the migration. +2. Back up PostgreSQL and the artifact root. +3. Run an inventory (no writes): + + ```bash + python scripts/migrate_v1_to_v2.py \ + --source /data/v1/sessions \ + --report migration-inventory.json \ + --tenant TENANT_ID + ``` + +4. Review every `invalid` or partial Session. Partial Sessions import as + `NEEDS_REVIEW`; closed Sessions import as historical `SUCCEEDED` Runs. +5. Apply with explicit storage targets: + + ```bash + python scripts/migrate_v1_to_v2.py \ + --source /data/v1/sessions \ + --report migration-result.json \ + --tenant TENANT_ID \ + --apply \ + --postgres-dsn "$OPENRATH_POSTGRES_DSN" \ + --artifact-root /data/openrath-artifacts + ``` + +The import is idempotent per legacy Session ID. Imported content carries +`provenance=legacy-import`, `trust=untrusted`, and `resumable=false`. +Remote sandbox identities are not reattached. Credentials are not copied. + +## Rollback + +The migration does not mutate v1 files. Roll back application traffic to v1 +and retain the v2 database and artifacts for investigation. Do not down-migrate +v2 Runs into v1 JSONL because checkpoint and effect semantics would be lost. diff --git a/deploy/docs/operations-v2.md b/deploy/docs/operations-v2.md new file mode 100644 index 0000000..91ea3e1 --- /dev/null +++ b/deploy/docs/operations-v2.md @@ -0,0 +1,50 @@ +# OpenRath v2 operations + +## Release and upgrade + +- Build immutable images by Git commit digest; never deploy a mutable `latest`. +- Run `openrath-migrate --check` before traffic and `openrath-migrate` as a + single pre-deploy Job. +- Database changes are additive in v2.0.0. Roll application pods back first; + retain added columns/tables until the rollback window closes. +- Take and restore-test PostgreSQL and artifact backups before an upgrade. + +## Incident runbooks + +### PostgreSQL unavailable + +Stop accepting new Runs (`/health/ready` returns 503), keep existing pods from +restart loops, restore database connectivity, verify schema and lease expiry, +then let workers requeue expired leases. Never substitute Redis as state. + +### Redis unavailable + +Runs remain durable. Alert on signal failures and increased queue latency, +restore Redis, and allow polling to continue. Do not reconstruct Run state +from Redis. + +### Worker terminated or stuck + +Confirm the worker lease has expired, call the orphan reconciliation loop, and +verify the fencing token increased. A stale worker must fail its next commit. +Inspect dispatched non-idempotent ToolInvocations; they must enter +`NEEDS_REVIEW`, not automatic retry. + +### Queue backlog + +Measure queued age, database lock time, provider saturation, and artifact +latency. Scale replicas only after confirming PostgreSQL connection capacity. +Rate-limit tenants producing disproportionate load. + +### Artifact store unavailable + +Keep the Run/checkpoint durable, fail or pause the affected step, and do not +inline payloads beyond the configured limit. Restore object storage and verify +SHA-256 before resuming. + +## Backup/restore exercise + +Quarterly, restore PostgreSQL and artifacts into an isolated environment, +run `openrath-migrate --check`, fetch historical Runs and artifacts, requeue +an expired lease, and verify a non-idempotent ambiguous invocation remains +blocked for review. diff --git a/deploy/kubernetes/openrath.yaml b/deploy/kubernetes/openrath.yaml new file mode 100644 index 0000000..bc01c40 --- /dev/null +++ b/deploy/kubernetes/openrath.yaml @@ -0,0 +1,229 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: openrath +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: openrath +data: + OPENRATH_APP: examples.v2_server_app:app + OPENRATH_EMBEDDED_WORKER: "false" + OPENRATH_DB_SCHEMA: openrath + OPENRATH_TENANT_ID: default +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: openrath-migrate +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: openrath + restartPolicy: OnFailure + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: migrate + image: ghcr.io/rath-team/openrath:2.0.0-review + imagePullPolicy: IfNotPresent + command: ["openrath-migrate"] + envFrom: + - configMapRef: + name: openrath + - secretRef: + name: openrath-runtime + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openrath +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: openrath + template: + metadata: + labels: + app.kubernetes.io/name: openrath + spec: + serviceAccountName: openrath + terminationGracePeriodSeconds: 45 + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: openrath + image: ghcr.io/rath-team/openrath:2.0.0-review + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: openrath + - secretRef: + name: openrath-runtime + ports: + - name: http + containerPort: 8000 + readinessProbe: + httpGet: + path: /health/ready + port: http + periodSeconds: 5 + timeoutSeconds: 2 + livenessProbe: + httpGet: + path: /health/live + port: http + periodSeconds: 10 + timeoutSeconds: 2 + startupProbe: + httpGet: + path: /health/live + port: http + failureThreshold: 30 + periodSeconds: 2 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "2" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: + sizeLimit: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: openrath +spec: + selector: + app.kubernetes.io/name: openrath + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openrath-worker +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: openrath-worker + template: + metadata: + labels: + app.kubernetes.io/name: openrath-worker + spec: + serviceAccountName: openrath + terminationGracePeriodSeconds: 45 + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: worker + image: ghcr.io/rath-team/openrath:2.0.0-review + command: ["openrath-worker", "--app", "examples.v2_server_app:server"] + envFrom: + - configMapRef: + name: openrath + - secretRef: + name: openrath-runtime + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "2" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: + sizeLimit: 128Mi +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: openrath +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: openrath +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: openrath +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: openrath + minReplicas: 2 + maxReplicas: 10 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: openrath-default-deny +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: openrath + policyTypes: ["Ingress", "Egress"] + ingress: + - ports: + - protocol: TCP + port: 8000 + egress: + - to: [] + ports: + - protocol: TCP + port: 5432 + - protocol: TCP + port: 443 diff --git a/deploy/kubernetes/secret.example.yaml b/deploy/kubernetes/secret.example.yaml new file mode 100644 index 0000000..25da76a --- /dev/null +++ b/deploy/kubernetes/secret.example.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openrath-runtime +type: Opaque +stringData: + # Replace these placeholders using your secret manager. Do not commit values. + OPENRATH_POSTGRES_DSN: postgresql://USER:PASSWORD@HOST:5432/DATABASE + OPENRATH_TOKEN: REPLACE_ME diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..72ac635 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1.7 +FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS builder + +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + UV_PROJECT_ENVIRONMENT=/opt/venv \ + UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 +WORKDIR /build +COPY pyproject.toml uv.lock README.md README_zh.md LICENSE ./ +COPY src ./src +RUN pip install uv==0.7.18 \ + && uv sync --frozen --no-dev --no-editable \ + --extra server --extra postgres --extra s3 --extra redis --extra otel + +FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS runtime + +ENV PATH="/opt/venv/bin:${PATH}" \ + PYTHONPATH="/app" \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + OPENRATH_HOST=0.0.0.0 \ + OPENRATH_PORT=8000 +RUN groupadd --system --gid 10001 openrath \ + && useradd --system --uid 10001 --gid openrath --home /app openrath +WORKDIR /app +COPY --from=builder /opt/venv /opt/venv +COPY examples ./examples +USER 10001:10001 +EXPOSE 8000 +ENTRYPOINT ["openrath-server"] diff --git a/examples/v2_server_app.py b/examples/v2_server_app.py new file mode 100644 index 0000000..282bc07 --- /dev/null +++ b/examples/v2_server_app.py @@ -0,0 +1,72 @@ +"""Minimal standalone OpenRath v2 reference application.""" + +from __future__ import annotations + +import asyncio +import os +from uuid import UUID + +from rath.definition import EffectClass, step +from rath.flow import Workflow +from rath.runtime import LocalRuntime, PostgresRunStore +from rath.security import Principal, PrincipalKind, SecurityContext +from rath.server import AgentServer, StaticTokenAuth +from rath.session import Session + + +class EchoWorkflow(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def echo(self, state, context): # type: ignore[no-untyped-def] + return {**state, "completed": True} + + def forward(self, session: Session) -> Session: + return session + + +class SlowWorkflow(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY, timeout_seconds=60) + async def wait(self, state, context): # type: ignore[no-untyped-def] + delay = min(max(float(state.get("delay", 1)), 0), 30) + await asyncio.sleep(delay) + return {**state, "completed": True} + + def forward(self, session: Session) -> Session: + return session + + +dsn = os.environ["OPENRATH_POSTGRES_DSN"] +token = os.environ["OPENRATH_TOKEN"] +tenant_id = os.getenv("OPENRATH_TENANT_ID", "default") +store = PostgresRunStore(dsn, schema=os.getenv("OPENRATH_DB_SCHEMA", "openrath")) +runtime = LocalRuntime(store) +server = AgentServer( + store, + runtime, + auth=StaticTokenAuth( + { + token: SecurityContext( + principal=Principal(id="reference-user", kind=PrincipalKind.SERVICE), + tenant_id=tenant_id, + ) + } + ), + embedded_worker=os.getenv("OPENRATH_EMBEDDED_WORKER", "true").lower() == "true", + worker_id=os.getenv("HOSTNAME", "standalone-worker"), + worker_lease_seconds=float(os.getenv("OPENRATH_WORKER_LEASE_SECONDS", "30")), +) +server.register_assistant( + "echo", + EchoWorkflow(), + revision_id=UUID(os.getenv("OPENRATH_REVISION_ID", "00000000-0000-4000-8000-000000000001")), +) +server.register_assistant( + "slow", + SlowWorkflow(), + revision_id=UUID( + os.getenv( + "OPENRATH_SLOW_REVISION_ID", + "00000000-0000-4000-8000-000000000002", + ) + ), +) +app = server.app diff --git a/pyproject.toml b/pyproject.toml index 2c0b598..7290c28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "openai>=1.0.0", "anthropic>=0.40.0", "pydantic>=2.0.0,<3", - "mcp>=1.0.0", + "mcp>=1.28.1,<2", ] [project.urls] @@ -29,6 +29,11 @@ Homepage = "https://github.com/Rath-Team/OpenRath" Repository = "https://github.com/Rath-Team/OpenRath" "Bug Tracker" = "https://github.com/Rath-Team/OpenRath/issues" +[project.scripts] +openrath-server = "rath.server.cli:server_main" +openrath-migrate = "rath.server.cli:migrate_main" +openrath-worker = "rath.server.cli:worker_main" + [project.optional-dependencies] litellm = [ "litellm>=1.80,<1.88", diff --git a/scripts/capacity_v2.py b/scripts/capacity_v2.py new file mode 100644 index 0000000..ec47a9c --- /dev/null +++ b/scripts/capacity_v2.py @@ -0,0 +1,56 @@ +"""Conservative capacity worksheet for an OpenRath v2 deployment.""" + +from __future__ import annotations + +import argparse +import json +import math + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--peak-runs-per-second", type=float, required=True) + parser.add_argument("--mean-run-seconds", type=float, required=True) + parser.add_argument("--events-per-run", type=float, default=10) + parser.add_argument("--event-kib", type=float, default=2) + parser.add_argument("--retention-days", type=int, default=30) + parser.add_argument("--worker-concurrency", type=int, default=16) + parser.add_argument("--headroom", type=float, default=1.5) + args = parser.parse_args() + if min( + args.peak_runs_per_second, + args.mean_run_seconds, + args.events_per_run, + args.event_kib, + args.worker_concurrency, + args.headroom, + ) <= 0: + parser.error("capacity inputs must be positive") + concurrent = ( + args.peak_runs_per_second * args.mean_run_seconds * args.headroom + ) + workers = math.ceil(concurrent / args.worker_concurrency) + events_per_day = args.peak_runs_per_second * 86400 * args.events_per_run + storage_gib = ( + events_per_day + * args.retention_days + * args.event_kib + * args.headroom + / 1024 + / 1024 + ) + report = { + "estimated_concurrent_runs": math.ceil(concurrent), + "minimum_worker_replicas": max(2, workers), + "suggested_postgres_connections": max(20, workers * 4 + 10), + "event_storage_gib_before_indexes": round(storage_gib, 2), + "warning": ( + "Worksheet only; validate with the actual workflow/provider mix " + "and database benchmark before production." + ), + } + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/migrate_v1_to_v2.py b/scripts/migrate_v1_to_v2.py new file mode 100644 index 0000000..a0a8934 --- /dev/null +++ b/scripts/migrate_v1_to_v2.py @@ -0,0 +1,143 @@ +"""Inventory and import v1 JSONL Sessions as immutable v2 legacy Runs.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from uuid import NAMESPACE_URL, UUID, uuid5 + +from rath.artifacts import LocalArtifactStore +from rath.runtime import PostgresRunStore, Run, RunStatus +from rath.server import PostgresResourceStore, SessionRecord +from rath.session.persistence.loader import load_session + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--tenant", required=True) + parser.add_argument("--apply", action="store_true") + parser.add_argument("--postgres-dsn") + parser.add_argument("--schema", default="openrath") + parser.add_argument("--artifact-root", type=Path) + args = parser.parse_args() + source = args.source.expanduser().resolve() + if not source.is_dir(): + parser.error("--source must be an existing v1 sessions directory") + if args.apply and (not args.postgres_dsn or not args.artifact_root): + parser.error("--apply requires --postgres-dsn and --artifact-root") + + candidates = sorted( + [ + *source.glob("*.jsonl"), + *source.glob("*.jsonl.__partial__"), + ] + ) + rows: list[dict[str, object]] = [] + runtime = ( + PostgresRunStore(args.postgres_dsn, schema=args.schema) + if args.apply + else None + ) + resources = PostgresResourceStore(runtime) if runtime is not None else None + artifacts = ( + LocalArtifactStore(args.artifact_root, max_bytes=1024 * 1024 * 1024) + if args.apply + else None + ) + try: + for path in candidates: + session_id = UUID(path.name.split(".jsonl", 1)[0]) + try: + legacy = load_session(session_id, path=path) + row: dict[str, object] = { + "session_id": str(session_id), + "path": str(path), + "closed": legacy.closed, + "created_at": legacy.header.created_at.isoformat(), + "chunks": len(legacy.chunk_table.rows), + "status": "ready", + } + if runtime is not None and resources is not None and artifacts is not None: + artifact = artifacts.put( + args.tenant, + path.read_bytes(), + media_type="application/x-ndjson", + metadata={ + "provenance": "legacy-import", + "legacy_session_id": str(session_id), + }, + ) + resources.ensure_session( + SessionRecord( + id=session_id, + tenant_id=args.tenant, + created_at=legacy.header.created_at, + ) + ) + run = Run.create( + id=uuid5(NAMESPACE_URL, f"openrath:v1:{session_id}"), + plan_id=uuid5(NAMESPACE_URL, "openrath:v1:transcript-import"), + revision_id=uuid5(NAMESPACE_URL, "openrath:v1.3.0"), + session_id=session_id, + tenant_id=args.tenant, + status=( + RunStatus.SUCCEEDED + if legacy.closed + else RunStatus.NEEDS_REVIEW + ), + state={ + "legacy": True, + "resumable": False, + "artifact_uri": artifact.uri, + "chunk_count": len(legacy.chunk_table.rows), + "trust": "untrusted", + "provenance": "legacy-import", + }, + idempotency_key=f"legacy-session:{session_id}", + context={ + "migration": "v1-to-v2", + "provenance": "legacy-import", + }, + ) + runtime.create_run(run) + row["status"] = "imported" + row["run_id"] = str(run.id) + row["artifact_uri"] = artifact.uri + rows.append(row) + except Exception as exc: + rows.append( + { + "session_id": str(session_id), + "path": str(path), + "status": "invalid", + "error": f"{type(exc).__name__}: {exc}", + } + ) + finally: + if runtime is not None: + runtime.close() + + report = { + "schema": "openrath.v2.migration-report/1", + "mode": "apply" if args.apply else "inventory", + "source": str(source), + "tenant": args.tenant, + "summary": { + "total": len(rows), + "ready": sum(item["status"] == "ready" for item in rows), + "imported": sum(item["status"] == "imported" for item in rows), + "invalid": sum(item["status"] == "invalid" for item in rows), + }, + "sessions": rows, + } + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text( + json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/soak_v2.py b/scripts/soak_v2.py new file mode 100644 index 0000000..3a76ae4 --- /dev/null +++ b/scripts/soak_v2.py @@ -0,0 +1,106 @@ +"""Run a bounded local soak profile and report resource growth.""" + +from __future__ import annotations + +import argparse +import json +import platform +import tempfile +import threading +import time +import tracemalloc +from pathlib import Path +from uuid import uuid4 + +from rath.context import RunContext +from rath.definition import EffectClass, step +from rath.flow import Workflow +from rath.runtime import LocalRuntime, RunStatus, SQLiteRunStore +from rath.session import Session + + +class SoakWorkflow(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def execute(self, state, context): # type: ignore[no-untyped-def] + return {"value": int(state["value"]) + 1} + + def forward(self, session: Session) -> Session: + return session + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--duration-seconds", type=float, default=300) + parser.add_argument("--max-runs", type=int) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.duration_seconds <= 0: + parser.error("--duration-seconds must be positive") + if args.max_runs is not None and args.max_runs < 1: + parser.error("--max-runs must be positive") + + started = time.perf_counter() + deadline = started + args.duration_seconds + completed = 0 + failures = 0 + threads_before = threading.active_count() + tracemalloc.start() + memory_before, _ = tracemalloc.get_traced_memory() + with tempfile.TemporaryDirectory(prefix="openrath-soak-") as directory: + store = SQLiteRunStore(Path(directory) / "runtime.db") + runtime = LocalRuntime(store) + context = RunContext.local(revision_id=uuid4()) + workflow = SoakWorkflow() + while time.perf_counter() < deadline and ( + args.max_runs is None or completed + failures < args.max_runs + ): + runtime.submit( + workflow, + session_id=uuid4(), + context=context, + state={"value": completed + failures}, + ) + run = runtime.work_once(worker_id="soak") + if run is not None and run.status is RunStatus.SUCCEEDED: + completed += 1 + else: + failures += 1 + store.close() + memory_after, memory_peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + duration = time.perf_counter() - started + report = { + "schema": "openrath.v2.soak/1", + "profile": "sqlite-single-worker-one-step", + "duration_seconds": duration, + "completed_runs": completed, + "failed_runs": failures, + "throughput_runs_per_second": completed / duration, + "resource_delta": { + "threads": threading.active_count() - threads_before, + "traced_memory_bytes": memory_after - memory_before, + "peak_traced_memory_bytes": memory_peak, + }, + "environment": { + "python": platform.python_version(), + "platform": platform.platform(), + "processor": platform.processor(), + }, + "scope": ( + "Review profile only; repeat for the approved 8h/24h duration " + "on target hardware before production rollout." + ), + } + value = json.dumps(report, indent=2) + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(value, encoding="utf-8") + print(value) + if failures: + raise SystemExit(1) + if report["resource_delta"]["threads"] != 0: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/src/rath/__init__.py b/src/rath/__init__.py index 799768c..8bf8064 100644 --- a/src/rath/__init__.py +++ b/src/rath/__init__.py @@ -19,8 +19,10 @@ from rath import ( adapters, + artifacts, backend, definition, + deployment, eval, flow, memory, @@ -32,7 +34,9 @@ __all__ = [ "backend", "adapters", + "artifacts", "definition", + "deployment", "eval", "flow", "memory", diff --git a/src/rath/_async/runtime.py b/src/rath/_async/runtime.py index b7d40e1..aec8ede 100644 --- a/src/rath/_async/runtime.py +++ b/src/rath/_async/runtime.py @@ -117,6 +117,10 @@ def run(self, coro: Coroutine[Any, Any, T]) -> T: except RuntimeError: running = None if running is not None: + # The caller already created the coroutine object. Close it before + # rejecting the blocking call so Python does not emit an + # un-awaited-coroutine warning or retain captured resources. + coro.close() raise RuntimeError( "OpenRathRuntime.run() called from inside an asyncio loop; " "OpenRath's public API is synchronous and must be called from " diff --git a/src/rath/adapters/__init__.py b/src/rath/adapters/__init__.py index 7894540..8453627 100644 --- a/src/rath/adapters/__init__.py +++ b/src/rath/adapters/__init__.py @@ -1,6 +1,9 @@ """Shared v2 adapter contracts.""" from rath.adapters.context import AdapterRequestContext +from rath.adapters.memory import MemoryExecutor, MemoryHandler +from rath.adapters.provider import ProviderExecutor, ProviderHandler +from rath.adapters.sandbox import SandboxExecutor, SandboxHandler from rath.adapters.schema import SchemaValidationError, validate_json from rath.adapters.specs import ( MemoryNamespace, @@ -15,9 +18,15 @@ __all__ = [ "AdapterRequestContext", "MemoryNamespace", + "MemoryExecutor", + "MemoryHandler", "ProviderCapability", + "ProviderExecutor", + "ProviderHandler", "ProviderSpec", "SandboxIsolation", + "SandboxExecutor", + "SandboxHandler", "SandboxSpec", "SchemaValidationError", "ToolExecutor", @@ -26,4 +35,3 @@ "ToolSpec", "validate_json", ] - diff --git a/src/rath/adapters/memory.py b/src/rath/adapters/memory.py new file mode 100644 index 0000000..2124e2e --- /dev/null +++ b/src/rath/adapters/memory.py @@ -0,0 +1,73 @@ +"""Governed tenant-scoped Memory v2 execution boundary.""" + +from __future__ import annotations + +import asyncio +import inspect +from collections.abc import Awaitable, Mapping +from typing import Literal, Protocol, cast + +from rath.adapters.context import AdapterRequestContext +from rath.adapters.specs import MemoryNamespace +from rath.context import RunContext +from rath.security import Action, PolicyEngine, ResourceRef, authorize + +__all__ = ["MemoryExecutor", "MemoryHandler"] + + +class MemoryHandler(Protocol): + def __call__( + self, + operation: Literal["put", "search", "delete"], + namespace: MemoryNamespace, + payload: Mapping[str, object], + context: AdapterRequestContext, + ) -> object | Awaitable[object]: ... + + +class MemoryExecutor: + def __init__(self, policy: PolicyEngine) -> None: + self.policy = policy + + async def execute( + self, + handler: MemoryHandler, + operation: Literal["put", "search", "delete"], + namespace: MemoryNamespace, + payload: Mapping[str, object], + *, + adapter_context: AdapterRequestContext, + run_context: RunContext, + timeout_seconds: float = 30, + ) -> object: + if timeout_seconds <= 0: + raise ValueError("memory timeout must be positive") + tenant_id = run_context.security.tenant_id + if namespace.tenant_id != tenant_id or adapter_context.tenant_id != tenant_id: + raise PermissionError("memory namespace tenant mismatch") + await authorize( + self.policy, + action=Action(f"memory.{operation}"), + resource=ResourceRef( + kind="memory_namespace", + id=":".join( + item + for item in ( + namespace.tenant_id, + namespace.user_id, + namespace.agent_id, + namespace.session_id, + ) + if item is not None + ), + tenant_id=tenant_id, + attributes={"trust": namespace.trust.value}, + ), + context=run_context, + ) + result = handler(operation, namespace, payload, adapter_context) + if inspect.isawaitable(result): + return await asyncio.wait_for( + cast(Awaitable[object], result), timeout=timeout_seconds + ) + return result diff --git a/src/rath/adapters/provider.py b/src/rath/adapters/provider.py new file mode 100644 index 0000000..7d67ff2 --- /dev/null +++ b/src/rath/adapters/provider.py @@ -0,0 +1,73 @@ +"""Governed Provider v2 execution boundary.""" + +from __future__ import annotations + +import asyncio +import inspect +from collections.abc import Awaitable, Mapping +from typing import Protocol, cast + +from rath.adapters.context import AdapterRequestContext +from rath.adapters.specs import ProviderCapability, ProviderSpec +from rath.context import RunContext +from rath.security import Action, PolicyEngine, ResourceRef, authorize + +__all__ = ["ProviderExecutor", "ProviderHandler"] + + +class ProviderHandler(Protocol): + def __call__( + self, + request: Mapping[str, object], + spec: ProviderSpec, + context: AdapterRequestContext, + ) -> object | Awaitable[object]: ... + + +class ProviderExecutor: + def __init__(self, policy: PolicyEngine) -> None: + self.policy = policy + self._semaphores: dict[str, asyncio.Semaphore] = {} + + async def execute( + self, + spec: ProviderSpec, + handler: ProviderHandler, + request: Mapping[str, object], + *, + capability: ProviderCapability, + adapter_context: AdapterRequestContext, + run_context: RunContext, + ) -> object: + if capability not in spec.capabilities: + raise ValueError( + f"provider {spec.id!r} does not declare {capability.value!r}" + ) + if adapter_context.tenant_id != run_context.security.tenant_id: + raise PermissionError("adapter and run tenant mismatch") + await authorize( + self.policy, + action=Action("provider.invoke"), + resource=ResourceRef( + kind="provider", + id=spec.id, + tenant_id=adapter_context.tenant_id, + attributes={ + "kind": spec.kind, + "model": spec.model, + "capability": capability.value, + }, + ), + context=run_context, + ) + semaphore = self._semaphores.setdefault( + spec.id, asyncio.Semaphore(spec.max_concurrency) + ) + async with semaphore: + result = handler(request, spec, adapter_context) + if inspect.isawaitable(result): + return await asyncio.wait_for( + cast(Awaitable[object], result), + timeout=spec.total_timeout_seconds, + ) + return result diff --git a/src/rath/adapters/sandbox.py b/src/rath/adapters/sandbox.py new file mode 100644 index 0000000..400b934 --- /dev/null +++ b/src/rath/adapters/sandbox.py @@ -0,0 +1,69 @@ +"""Governed Sandbox v2 execution boundary.""" + +from __future__ import annotations + +import asyncio +import inspect +from collections.abc import Awaitable, Mapping +from typing import Protocol, cast + +from rath.adapters.context import AdapterRequestContext +from rath.adapters.specs import SandboxSpec +from rath.context import RunContext +from rath.security import Action, PolicyEngine, ResourceRef, authorize + +__all__ = ["SandboxExecutor", "SandboxHandler"] + + +class SandboxHandler(Protocol): + def __call__( + self, + operation: str, + payload: Mapping[str, object], + spec: SandboxSpec, + context: AdapterRequestContext, + ) -> object | Awaitable[object]: ... + + +class SandboxExecutor: + def __init__(self, policy: PolicyEngine) -> None: + self.policy = policy + + async def execute( + self, + spec: SandboxSpec, + handler: SandboxHandler, + operation: str, + payload: Mapping[str, object], + *, + adapter_context: AdapterRequestContext, + run_context: RunContext, + timeout_seconds: float = 60, + ) -> object: + if not operation: + raise ValueError("sandbox operation is required") + if timeout_seconds <= 0: + raise ValueError("sandbox timeout must be positive") + if adapter_context.tenant_id != run_context.security.tenant_id: + raise PermissionError("adapter and run tenant mismatch") + await authorize( + self.policy, + action=Action("sandbox.execute"), + resource=ResourceRef( + kind="sandbox", + id=spec.id, + tenant_id=adapter_context.tenant_id, + attributes={ + "isolation": spec.isolation.value, + "network": spec.network, + "operation": operation, + }, + ), + context=run_context, + ) + result = handler(operation, payload, spec, adapter_context) + if inspect.isawaitable(result): + return await asyncio.wait_for( + cast(Awaitable[object], result), timeout=timeout_seconds + ) + return result diff --git a/src/rath/adapters/tool.py b/src/rath/adapters/tool.py index 6e7e514..73c36f8 100644 --- a/src/rath/adapters/tool.py +++ b/src/rath/adapters/tool.py @@ -11,6 +11,7 @@ from rath.adapters.context import AdapterRequestContext from rath.adapters.schema import validate_json from rath.adapters.specs import ToolSpec +from rath.artifacts import ArtifactStore from rath.context import RunContext from rath.runtime.effects import ( EffectLedger, @@ -48,9 +49,11 @@ def __init__( policy: PolicyEngine, *, effect_ledger: EffectLedger | None = None, + artifact_store: ArtifactStore | None = None, ) -> None: self.policy = policy self.effect_ledger = effect_ledger + self.artifact_store = artifact_store async def execute( self, @@ -127,6 +130,25 @@ async def execute( or spec.max_output_bytes, ) if len(encoded) > limit: + if self.artifact_store is not None: + artifact = self.artifact_store.put( + adapter_context.tenant_id, + encoded, + media_type="application/json", + metadata={ + "tool": f"{spec.name}@{spec.version}", + "run_id": str(run_id) if run_id is not None else None, + }, + ) + reference = { + "artifact_uri": artifact.uri, + "digest": artifact.digest, + "size": artifact.size, + "media_type": artifact.media_type, + } + if invocation is not None and ledger is not None: + ledger.complete(invocation.id, reference) + return reference if invocation is not None and ledger is not None: ledger.fail( invocation.id, diff --git a/src/rath/client/remote.py b/src/rath/client/remote.py index c88c8c4..ef0e7f3 100644 --- a/src/rath/client/remote.py +++ b/src/rath/client/remote.py @@ -44,6 +44,122 @@ def get_run(self, run_id: str) -> dict[str, Any]: response.raise_for_status() return cast(dict[str, Any], response.json()) + def create_session(self) -> dict[str, Any]: + response = self._client.post("/v1/sessions", json={}) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def create_assistant( + self, *, assistant_id: str, template_id: str + ) -> dict[str, Any]: + response = self._client.post( + "/v1/assistants", + json={"id": assistant_id, "template_id": template_id}, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def list_assistants(self) -> tuple[dict[str, Any], ...]: + response = self._client.get("/v1/assistants") + response.raise_for_status() + return tuple(cast(list[dict[str, Any]], response.json()["items"])) + + def store( + self, + operation: str, + payload: dict[str, object], + *, + user_id: str | None = None, + agent_id: str | None = None, + session_id: str | None = None, + ) -> dict[str, Any]: + if operation not in {"put", "search", "delete"}: + raise ValueError("store operation must be put, search, or delete") + body = { + "payload": payload, + "user_id": user_id, + "agent_id": agent_id, + "session_id": session_id, + } + if operation == "search": + response = self._client.post("/v1/store/search", json=body) + elif operation == "put": + response = self._client.post("/v1/store/items", json=body) + else: + response = self._client.request( + "DELETE", "/v1/store/items", json=body + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def list_runs( + self, *, limit: int = 50, after: str | None = None + ) -> dict[str, Any]: + response = self._client.get( + "/v1/runs", params={"limit": limit, "after": after} + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def cancel_run(self, run_id: str) -> dict[str, Any]: + response = self._client.post(f"/v1/runs/{run_id}/cancel") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def resume_run(self, run_id: str) -> dict[str, Any]: + response = self._client.post( + f"/v1/runs/{run_id}/resume", json={"confirm": True} + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def list_interrupts( + self, *, pending_only: bool = True, limit: int = 50 + ) -> tuple[dict[str, Any], ...]: + response = self._client.get( + "/v1/interrupts", + params={"pending": str(pending_only).lower(), "limit": limit}, + ) + response.raise_for_status() + return tuple(cast(list[dict[str, Any]], response.json()["items"])) + + def decide_interrupt( + self, + interrupt_id: str, + *, + kind: str, + reason: str, + payload: dict[str, object] | None = None, + ) -> dict[str, Any]: + response = self._client.post( + f"/v1/interrupts/{interrupt_id}/decision", + json={"kind": kind, "reason": reason, "payload": payload or {}}, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def events(self, run_id: str, *, after: int = 0) -> tuple[dict[str, Any], ...]: + response = self._client.get( + f"/v1/runs/{run_id}/events", params={"after": after} + ) + response.raise_for_status() + return tuple(cast(list[dict[str, Any]], response.json()["items"])) + + def create_feedback( + self, + run_id: str, + *, + key: str, + score: float | None = None, + value: str | None = None, + ) -> dict[str, Any]: + response = self._client.post( + "/v1/feedback", + json={"run_id": run_id, "key": key, "score": score, "value": value}, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + def close(self) -> None: self._client.close() @@ -82,6 +198,115 @@ async def get_run(self, run_id: str) -> dict[str, Any]: response.raise_for_status() return cast(dict[str, Any], response.json()) + async def create_session(self) -> dict[str, Any]: + response = await self._client.post("/v1/sessions", json={}) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def create_assistant( + self, *, assistant_id: str, template_id: str + ) -> dict[str, Any]: + response = await self._client.post( + "/v1/assistants", + json={"id": assistant_id, "template_id": template_id}, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def list_assistants(self) -> tuple[dict[str, Any], ...]: + response = await self._client.get("/v1/assistants") + response.raise_for_status() + return tuple(cast(list[dict[str, Any]], response.json()["items"])) + + async def store( + self, + operation: str, + payload: dict[str, object], + *, + user_id: str | None = None, + agent_id: str | None = None, + session_id: str | None = None, + ) -> dict[str, Any]: + if operation not in {"put", "search", "delete"}: + raise ValueError("store operation must be put, search, or delete") + body = { + "payload": payload, + "user_id": user_id, + "agent_id": agent_id, + "session_id": session_id, + } + if operation == "search": + response = await self._client.post("/v1/store/search", json=body) + elif operation == "put": + response = await self._client.post("/v1/store/items", json=body) + else: + response = await self._client.request( + "DELETE", "/v1/store/items", json=body + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def list_runs( + self, *, limit: int = 50, after: str | None = None + ) -> dict[str, Any]: + response = await self._client.get( + "/v1/runs", params={"limit": limit, "after": after} + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def cancel_run(self, run_id: str) -> dict[str, Any]: + response = await self._client.post(f"/v1/runs/{run_id}/cancel") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def resume_run(self, run_id: str) -> dict[str, Any]: + response = await self._client.post( + f"/v1/runs/{run_id}/resume", json={"confirm": True} + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def list_interrupts( + self, *, pending_only: bool = True, limit: int = 50 + ) -> tuple[dict[str, Any], ...]: + response = await self._client.get( + "/v1/interrupts", + params={"pending": str(pending_only).lower(), "limit": limit}, + ) + response.raise_for_status() + return tuple(cast(list[dict[str, Any]], response.json()["items"])) + + async def decide_interrupt( + self, + interrupt_id: str, + *, + kind: str, + reason: str, + payload: dict[str, object] | None = None, + ) -> dict[str, Any]: + response = await self._client.post( + f"/v1/interrupts/{interrupt_id}/decision", + json={"kind": kind, "reason": reason, "payload": payload or {}}, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def create_feedback( + self, + run_id: str, + *, + key: str, + score: float | None = None, + value: str | None = None, + ) -> dict[str, Any]: + response = await self._client.post( + "/v1/feedback", + json={"run_id": run_id, "key": key, "score": score, "value": value}, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + async def events(self, run_id: str, *, after: int = 0) -> AsyncIterator[dict[str, Any]]: response = await self._client.get( f"/v1/runs/{run_id}/events", diff --git a/src/rath/config/store.py b/src/rath/config/store.py index 3b268c5..fcefc9c 100644 --- a/src/rath/config/store.py +++ b/src/rath/config/store.py @@ -185,6 +185,10 @@ def save(self) -> None: creds_payload["version"] = SCHEMA_VERSION atomic_write_json(creds_path, creds_payload, mode=0o600) chmod_user_only(creds_path) + else: + # Prevent a removed last key from being restored on the next + # load from an obsolete credentials sidecar. + creds_path.unlink(missing_ok=True) # Record/refresh the root layout manifest at the data root. try: diff --git a/src/rath/deployment/__init__.py b/src/rath/deployment/__init__.py new file mode 100644 index 0000000..e0c213c --- /dev/null +++ b/src/rath/deployment/__init__.py @@ -0,0 +1,19 @@ +"""Immutable deployment revision contracts.""" + +from rath.deployment.revisions import ( + DeploymentManifest, + PostgresRevisionStore, + Revision, + RevisionConflict, + RevisionStore, + SQLiteRevisionStore, +) + +__all__ = [ + "DeploymentManifest", + "PostgresRevisionStore", + "Revision", + "RevisionConflict", + "RevisionStore", + "SQLiteRevisionStore", +] diff --git a/src/rath/deployment/revisions.py b/src/rath/deployment/revisions.py new file mode 100644 index 0000000..e6ccad1 --- /dev/null +++ b/src/rath/deployment/revisions.py @@ -0,0 +1,198 @@ +"""Immutable, content-identified deployment revisions.""" + +from __future__ import annotations + +import json +import sqlite3 +from collections.abc import Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any, Protocol, runtime_checkable +from uuid import NAMESPACE_URL, UUID, uuid5 + +from rath._json import JSONValue, freeze_mapping, thaw_json +from rath.runtime import PostgresRunStore, SQLiteRunStore + +__all__ = [ + "DeploymentManifest", + "PostgresRevisionStore", + "Revision", + "RevisionConflict", + "RevisionStore", + "SQLiteRevisionStore", +] + + +class RevisionConflict(RuntimeError): + pass + + +@dataclass(frozen=True, slots=True) +class DeploymentManifest: + image_digest: str + plan_hash: str + python_version: str + dependencies_digest: str + resources: Mapping[str, JSONValue] = field(default_factory=dict) + + def __post_init__(self) -> None: + for name, value in ( + ("image_digest", self.image_digest), + ("plan_hash", self.plan_hash), + ("dependencies_digest", self.dependencies_digest), + ): + if len(value) != 64: + raise ValueError(f"{name} must be a SHA-256 digest") + int(value, 16) + object.__setattr__( + self, + "resources", + freeze_mapping(self.resources, field="deployment.resources"), + ) + + def canonical_json(self) -> str: + return json.dumps( + { + "image_digest": self.image_digest, + "plan_hash": self.plan_hash, + "python_version": self.python_version, + "dependencies_digest": self.dependencies_digest, + "resources": thaw_json(self.resources), + }, + sort_keys=True, + separators=(",", ":"), + ) + + +@dataclass(frozen=True, slots=True) +class Revision: + id: UUID + code_digest: str + manifest: DeploymentManifest + created_at: datetime + + @classmethod + def create( + cls, *, code_digest: str, manifest: DeploymentManifest + ) -> "Revision": + if len(code_digest) != 64: + raise ValueError("code_digest must be a SHA-256 digest") + int(code_digest, 16) + identity = uuid5( + NAMESPACE_URL, + f"openrath-revision:{code_digest}:{manifest.canonical_json()}", + ) + return cls(identity, code_digest, manifest, datetime.now(timezone.utc)) + + +@runtime_checkable +class RevisionStore(Protocol): + def put(self, revision: Revision) -> Revision: ... + + def get(self, revision_id: UUID) -> Revision: ... + + +def _manifest(value: str | Mapping[str, Any]) -> DeploymentManifest: + data = json.loads(value) if isinstance(value, str) else value + return DeploymentManifest( + image_digest=data["image_digest"], + plan_hash=data["plan_hash"], + python_version=data["python_version"], + dependencies_digest=data["dependencies_digest"], + resources=data["resources"], + ) + + +class SQLiteRevisionStore: + def __init__(self, run_store: SQLiteRunStore) -> None: + self.path = str(run_store.path) + + def put(self, revision: Revision) -> Revision: + with sqlite3.connect(self.path) as connection: + existing = connection.execute( + "SELECT * FROM revisions WHERE id = ?", (str(revision.id),) + ).fetchone() + if existing is not None: + loaded = self.get(revision.id) + if loaded.code_digest != revision.code_digest or loaded.manifest != revision.manifest: + raise RevisionConflict("revision identity is immutable") + return loaded + connection.execute( + """ + INSERT INTO revisions( + id, code_digest, plan_hash, manifest_json, created_at + ) VALUES (?, ?, ?, ?, ?) + """, + ( + str(revision.id), + revision.code_digest, + revision.manifest.plan_hash, + revision.manifest.canonical_json(), + revision.created_at.isoformat(), + ), + ) + return revision + + def get(self, revision_id: UUID) -> Revision: + connection = sqlite3.connect(self.path) + connection.row_factory = sqlite3.Row + try: + row = connection.execute( + "SELECT * FROM revisions WHERE id = ?", (str(revision_id),) + ).fetchone() + finally: + connection.close() + if row is None: + raise KeyError(str(revision_id)) + return Revision( + id=UUID(row["id"]), + code_digest=row["code_digest"], + manifest=_manifest(row["manifest_json"]), + created_at=datetime.fromisoformat(row["created_at"]), + ) + + +class PostgresRevisionStore: + def __init__(self, run_store: PostgresRunStore) -> None: + self.run_store = run_store + + def put(self, revision: Revision) -> Revision: + from psycopg.types.json import Jsonb + + manifest = json.loads(revision.manifest.canonical_json()) + with self.run_store.connection() as connection: + row = connection.execute( + """ + INSERT INTO revisions( + id, code_digest, plan_hash, manifest_json, created_at + ) VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (id) DO NOTHING RETURNING id + """, + ( + revision.id, + revision.code_digest, + revision.manifest.plan_hash, + Jsonb(manifest), + revision.created_at, + ), + ).fetchone() + if row is None: + loaded = self.get(revision.id) + if loaded.code_digest != revision.code_digest or loaded.manifest != revision.manifest: + raise RevisionConflict("revision identity is immutable") + return loaded + return revision + + def get(self, revision_id: UUID) -> Revision: + with self.run_store.connection() as connection: + row = connection.execute( + "SELECT * FROM revisions WHERE id = %s", (revision_id,) + ).fetchone() + if row is None: + raise KeyError(str(revision_id)) + return Revision( + id=row["id"], + code_digest=row["code_digest"], + manifest=_manifest(row["manifest_json"]), + created_at=row["created_at"], + ) diff --git a/src/rath/eval/__init__.py b/src/rath/eval/__init__.py index c4853d3..7fa7b1b 100644 --- a/src/rath/eval/__init__.py +++ b/src/rath/eval/__init__.py @@ -7,15 +7,22 @@ GateDecision, ) from rath.eval.runner import EvaluationRunner, regression_gate +from rath.eval.store import ( + EvaluationStore, + PostgresEvaluationStore, + SQLiteEvaluationStore, +) __all__ = [ "Dataset", "EvaluationResult", "EvaluationRunner", + "EvaluationStore", "Evaluator", "Example", "Experiment", "GateDecision", + "PostgresEvaluationStore", + "SQLiteEvaluationStore", "regression_gate", ] - diff --git a/src/rath/eval/store.py b/src/rath/eval/store.py new file mode 100644 index 0000000..e44d288 --- /dev/null +++ b/src/rath/eval/store.py @@ -0,0 +1,220 @@ +"""Durable evaluation dataset and experiment stores.""" + +from __future__ import annotations + +import json +import sqlite3 +from collections.abc import Mapping +from datetime import datetime, timezone +from typing import Any, Protocol, runtime_checkable +from uuid import UUID + +from rath._json import thaw_json +from rath.eval.models import Dataset, EvaluationResult, Example, Experiment +from rath.runtime import PostgresRunStore, SQLiteRunStore + +__all__ = [ + "EvaluationStore", + "PostgresEvaluationStore", + "SQLiteEvaluationStore", +] + + +def _dataset_json(dataset: Dataset) -> list[dict[str, object]]: + return [ + { + "id": str(example.id), + "inputs": thaw_json(example.inputs), + "expected": thaw_json(example.expected), + } + for example in dataset.examples + ] + + +def _results_json(experiment: Experiment) -> list[dict[str, object]]: + return [ + { + "evaluator": result.evaluator, + "score": result.score, + "passed": result.passed, + "reason": result.reason, + "metadata": thaw_json(result.metadata), + } + for result in experiment.results + ] + + +def _dataset(row: Mapping[str, Any]) -> Dataset: + values = row["examples_json"] + if isinstance(values, str): + values = json.loads(values) + return Dataset( + id=UUID(str(row["id"])), + name=row["name"], + version=row["version"], + examples=tuple( + Example( + id=UUID(item["id"]), + inputs=item["inputs"], + expected=item["expected"], + ) + for item in values + ), + ) + + +def _experiment(row: Mapping[str, Any]) -> Experiment: + values = row["results_json"] + if isinstance(values, str): + values = json.loads(values) + return Experiment( + id=UUID(str(row["id"])), + dataset_id=UUID(str(row["dataset_id"])), + revision_id=UUID(str(row["revision_id"])), + results=tuple( + EvaluationResult( + evaluator=item["evaluator"], + score=float(item["score"]), + passed=bool(item["passed"]), + reason=item["reason"], + metadata=item["metadata"], + ) + for item in values + ), + ) + + +@runtime_checkable +class EvaluationStore(Protocol): + def save_dataset(self, dataset: Dataset) -> Dataset: ... + + def get_dataset(self, dataset_id: UUID) -> Dataset: ... + + def save_experiment(self, experiment: Experiment) -> Experiment: ... + + def get_experiment(self, experiment_id: UUID) -> Experiment: ... + + +class SQLiteEvaluationStore: + def __init__(self, run_store: SQLiteRunStore) -> None: + self.path = str(run_store.path) + + def _connect(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.path) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + return connection + + def save_dataset(self, dataset: Dataset) -> Dataset: + with self._connect() as connection: + connection.execute( + """ + INSERT INTO evaluation_datasets(id, name, version, examples_json) + VALUES (?, ?, ?, ?) + ON CONFLICT(name, version) DO UPDATE + SET examples_json = excluded.examples_json + """, + ( + str(dataset.id), + dataset.name, + dataset.version, + json.dumps(_dataset_json(dataset), separators=(",", ":")), + ), + ) + return dataset + + def get_dataset(self, dataset_id: UUID) -> Dataset: + with self._connect() as connection: + row = connection.execute( + "SELECT * FROM evaluation_datasets WHERE id = ?", + (str(dataset_id),), + ).fetchone() + if row is None: + raise KeyError(str(dataset_id)) + return _dataset(row) + + def save_experiment(self, experiment: Experiment) -> Experiment: + with self._connect() as connection: + connection.execute( + """ + INSERT INTO evaluation_experiments( + id, dataset_id, revision_id, results_json, created_at + ) VALUES (?, ?, ?, ?, ?) + """, + ( + str(experiment.id), + str(experiment.dataset_id), + str(experiment.revision_id), + json.dumps(_results_json(experiment), separators=(",", ":")), + datetime.now(timezone.utc).isoformat(), + ), + ) + return experiment + + def get_experiment(self, experiment_id: UUID) -> Experiment: + with self._connect() as connection: + row = connection.execute( + "SELECT * FROM evaluation_experiments WHERE id = ?", + (str(experiment_id),), + ).fetchone() + if row is None: + raise KeyError(str(experiment_id)) + return _experiment(row) + + +class PostgresEvaluationStore: + def __init__(self, run_store: PostgresRunStore) -> None: + self.run_store = run_store + + def save_dataset(self, dataset: Dataset) -> Dataset: + from psycopg.types.json import Jsonb + + with self.run_store.connection() as connection: + connection.execute( + """ + INSERT INTO evaluation_datasets(id, name, version, examples_json) + VALUES (%s, %s, %s, %s) + ON CONFLICT(name, version) DO UPDATE + SET examples_json = excluded.examples_json + """, + (dataset.id, dataset.name, dataset.version, Jsonb(_dataset_json(dataset))), + ) + return dataset + + def get_dataset(self, dataset_id: UUID) -> Dataset: + with self.run_store.connection() as connection: + row = connection.execute( + "SELECT * FROM evaluation_datasets WHERE id = %s", (dataset_id,) + ).fetchone() + if row is None: + raise KeyError(str(dataset_id)) + return _dataset(row) + + def save_experiment(self, experiment: Experiment) -> Experiment: + from psycopg.types.json import Jsonb + + with self.run_store.connection() as connection: + connection.execute( + """ + INSERT INTO evaluation_experiments( + id, dataset_id, revision_id, results_json + ) VALUES (%s, %s, %s, %s) + """, + ( + experiment.id, + experiment.dataset_id, + experiment.revision_id, + Jsonb(_results_json(experiment)), + ), + ) + return experiment + + def get_experiment(self, experiment_id: UUID) -> Experiment: + with self.run_store.connection() as connection: + row = connection.execute( + "SELECT * FROM evaluation_experiments WHERE id = %s", + (experiment_id,), + ).fetchone() + if row is None: + raise KeyError(str(experiment_id)) + return _experiment(row) diff --git a/src/rath/observability/__init__.py b/src/rath/observability/__init__.py index 8f95d66..6e0e93d 100644 --- a/src/rath/observability/__init__.py +++ b/src/rath/observability/__init__.py @@ -5,13 +5,17 @@ SpanRecord, Telemetry, ) +from rath.observability.logging import StructuredLogger +from rath.observability.otel import OpenTelemetry from rath.observability.redaction import redact __all__ = [ "InMemoryTelemetry", "GuardedTelemetry", "NoOpTelemetry", + "OpenTelemetry", "SpanRecord", + "StructuredLogger", "Telemetry", "redact", ] diff --git a/src/rath/observability/logging.py b/src/rath/observability/logging.py new file mode 100644 index 0000000..6c0052c --- /dev/null +++ b/src/rath/observability/logging.py @@ -0,0 +1,54 @@ +"""Redacted newline-delimited JSON records for operational logging.""" + +from __future__ import annotations + +import json +import logging +from collections.abc import Callable, Mapping +from datetime import datetime, timezone + +from rath.context import TraceContext +from rath.observability.redaction import redact + +__all__ = ["StructuredLogger"] + + +class StructuredLogger: + """Emit stable JSON records without ever failing the application path.""" + + def __init__(self, sink: Callable[[str], None] | None = None) -> None: + self._sink = sink or logging.getLogger("openrath").info + + def emit( + self, + event: str, + *, + context: TraceContext | None = None, + fields: Mapping[str, object] | None = None, + ) -> None: + record: dict[str, object] = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "event": event, + } + if context is not None: + record.update( + { + "trace_id": context.trace_id, + "span_id": context.span_id, + } + ) + safe = redact(dict(fields or {})) + assert isinstance(safe, dict) + record.update(safe) + try: + self._sink( + json.dumps( + record, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + default=str, + ) + ) + except Exception: + return diff --git a/src/rath/observability/otel.py b/src/rath/observability/otel.py new file mode 100644 index 0000000..a7157a8 --- /dev/null +++ b/src/rath/observability/otel.py @@ -0,0 +1,88 @@ +"""OpenTelemetry SDK bridge with W3C trace correlation.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from typing import Any + +from rath.context import TraceContext +from rath.observability.redaction import redact + +__all__ = ["OpenTelemetry"] + + +class OpenTelemetry: + """Telemetry implementation backed by configured OpenTelemetry providers.""" + + def __init__( + self, + *, + service_name: str = "openrath", + tracer_provider: Any | None = None, + meter_provider: Any | None = None, + ) -> None: + try: + from opentelemetry import metrics, trace + except ImportError as exc: + raise RuntimeError( + "OpenTelemetry support requires `pip install openrath[otel]`" + ) from exc + self._trace = trace + self._tracer = trace.get_tracer( + service_name, tracer_provider=tracer_provider + ) + self._meter = metrics.get_meter( + service_name, meter_provider=meter_provider + ) + self._counters: dict[str, Any] = {} + + @contextmanager + def span( + self, + name: str, + *, + context: TraceContext, + attributes: Mapping[str, object] | None = None, + ) -> Iterator[None]: + trace = self._trace + parent = trace.NonRecordingSpan( + trace.SpanContext( + trace_id=int(context.trace_id, 16), + span_id=int(context.span_id, 16), + is_remote=True, + trace_flags=trace.TraceFlags( + trace.TraceFlags.SAMPLED + if context.sampled + else trace.TraceFlags.DEFAULT + ), + trace_state=trace.TraceState(), + ) + ) + parent_context = trace.set_span_in_context(parent) + safe = redact(dict(attributes or {})) + assert isinstance(safe, dict) + scalar_attributes = { + key: value + for key, value in safe.items() + if isinstance(value, (bool, str, int, float)) + } + with self._tracer.start_as_current_span( + name, + context=parent_context, + attributes=scalar_attributes, + ): + yield + + def increment( + self, + name: str, + value: int = 1, + *, + attributes: Mapping[str, str] | None = None, + ) -> None: + counter = self._counters.get(name) + if counter is None: + counter = self._meter.create_counter(name) + self._counters[name] = counter + counter.add(value, attributes=dict(attributes or {})) diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py index 9ebf6ed..4031af0 100644 --- a/src/rath/runtime/__init__.py +++ b/src/rath/runtime/__init__.py @@ -27,6 +27,14 @@ assert_transition, ) from rath.runtime.postgres import PostgresRunStore +from rath.runtime.signals import ( + GuardedSignalBus, + InMemorySignalBus, + RedisSignalBus, + RunSignal, + SignalBus, + SignalKind, +) from rath.runtime.sqlite import SQLiteRunStore from rath.runtime.store import RunStore @@ -41,17 +49,23 @@ "Interrupt", "InterruptKind", "InvocationStatus", + "GuardedSignalBus", + "InMemorySignalBus", "InvalidRunTransition", "LocalRuntime", "PostgresRunStore", "PostgresEffectLedger", "Reconciliation", + "RedisSignalBus", "Run", "RunEvent", "RunStatus", + "RunSignal", "ResourceLease", "RunStore", "SQLiteRunStore", + "SignalBus", + "SignalKind", "SQLiteEffectLedger", "StepContext", "ToolInvocation", diff --git a/src/rath/runtime/local.py b/src/rath/runtime/local.py index c68a3ef..7bb8cf9 100644 --- a/src/rath/runtime/local.py +++ b/src/rath/runtime/local.py @@ -3,18 +3,39 @@ from __future__ import annotations import inspect -from collections.abc import Mapping -from dataclasses import dataclass +import threading +import time +from asyncio import TimeoutError as AsyncTimeoutError +from collections.abc import Callable, Mapping +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import TimeoutError as FutureTimeout +from dataclasses import dataclass, field from datetime import datetime from typing import Any, Coroutine, cast from uuid import UUID -from rath._json import thaw_json -from rath.context import RunContext +from rath._json import JSONValue, thaw_json +from rath.context import DeadlineExceededError, RunContext, TraceContext from rath.definition import ExecutionPlan, NodeKind, WorkflowCompiler -from rath.observability import GuardedTelemetry, NoOpTelemetry, Telemetry -from rath.runtime.models import Checkpoint, ClaimedRun, Run, RunStatus +from rath.observability import ( + GuardedTelemetry, + NoOpTelemetry, + StructuredLogger, + Telemetry, +) +from rath.runtime.models import ( + TERMINAL_RUN_STATUSES, + ApprovalDecision, + Checkpoint, + ClaimedRun, + ConflictError, + Interrupt, + InterruptKind, + Run, + RunStatus, +) from rath.runtime.store import RunStore +from rath.security import Principal, PrincipalKind, SecurityContext __all__ = ["LocalRuntime", "StepContext"] @@ -25,6 +46,23 @@ class StepContext: request: RunContext worker_id: str fencing_token: int + _interrupt_handler: Callable[ + [InterruptKind, Mapping[str, object], float | None], ApprovalDecision + ] = field(repr=False) + + def interrupt( + self, + kind: InterruptKind, + request: Mapping[str, object], + *, + timeout_seconds: float | None = None, + ) -> ApprovalDecision: + """Suspend durably on first call and return the decision after resume.""" + return self._interrupt_handler(kind, request, timeout_seconds) + + +class _RunSuspended(RuntimeError): + pass @dataclass(frozen=True, slots=True) @@ -41,9 +79,11 @@ def __init__( store: RunStore, *, telemetry: Telemetry | None = None, + structured_logger: StructuredLogger | None = None, ) -> None: self.store = store self.telemetry = GuardedTelemetry(telemetry or NoOpTelemetry()) + self.structured_logger = structured_logger or StructuredLogger() self._registrations: dict[UUID, _Registration] = {} self._contexts: dict[UUID, RunContext] = {} @@ -65,6 +105,7 @@ def submit( context: RunContext, state: Mapping[str, object] | None = None, idempotency_key: str | None = None, + priority: int = 0, ) -> Run: context.ensure_active() plan = self.register(workflow, revision_id=context.revision_id) @@ -76,9 +117,38 @@ def submit( state=state, next_nodes=(plan.definition.entrypoint,), idempotency_key=idempotency_key, + context={ + "principal": { + "id": context.security.principal.id, + "kind": context.security.principal.kind.value, + "claims": context.security.principal.claims, + }, + "project_id": context.security.project_id, + "grants": sorted(context.security.grants), + "attributes": context.security.attributes, + "request_id": str(context.request_id), + "trace_id": context.trace_context.trace_id, + "span_id": context.trace_context.span_id, + "sampled": context.trace_context.sampled, + "deadline": ( + context.deadline.isoformat() if context.deadline is not None else None + ), + }, + priority=priority, ) created = self.store.create_run(run) self._contexts[created.id] = context + self.structured_logger.emit( + "run.submitted", + context=context.trace_context, + fields={ + "run_id": str(created.id), + "tenant_id": created.tenant_id, + "plan_id": str(created.plan_id), + "revision_id": str(created.revision_id), + "status": created.status.value, + }, + ) return created def work_once( @@ -96,21 +166,98 @@ def work_once( ) if claim is None: return None + claim_context = self._contexts.get(claim.run.id) + self.structured_logger.emit( + "run.claimed", + context=( + claim_context.trace_context if claim_context is not None else None + ), + fields={ + "run_id": str(claim.run.id), + "tenant_id": claim.run.tenant_id, + "worker_id": worker_id, + "fencing_token": claim.lease.fencing_token, + }, + ) try: - return self._execute_claim(claim, max_steps=max_steps) + stop_heartbeat = threading.Event() + heartbeat = threading.Thread( + target=self._heartbeat, + args=(claim, lease_seconds, stop_heartbeat), + daemon=True, + name=f"openrath-lease-{claim.run.id}", + ) + heartbeat.start() + try: + result = self._execute_claim(claim, max_steps=max_steps) + self._forget_context_if_terminal(result) + return result + finally: + stop_heartbeat.set() + heartbeat.join(timeout=min(1.0, lease_seconds)) + except _RunSuspended: + waiting = self.store.get_run(claim.run.id) + self.structured_logger.emit( + "run.suspended", + context=( + claim_context.trace_context + if claim_context is not None + else None + ), + fields={ + "run_id": str(waiting.id), + "tenant_id": waiting.tenant_id, + "status": waiting.status.value, + }, + ) + return waiting except BaseException as exc: - return self.store.finish_claim( + current = self.store.get_run(claim.run.id) + if current.status in TERMINAL_RUN_STATUSES: + self._forget_context_if_terminal(current) + return current + target = ( + RunStatus.TIMED_OUT + if isinstance( + exc, + ( + AsyncTimeoutError, + DeadlineExceededError, + TimeoutError, + FutureTimeout, + ), + ) + else RunStatus.FAILED + ) + failed = self.store.finish_claim( claim.run.id, worker_id=worker_id, fencing_token=claim.lease.fencing_token, - expected_run_version=self.store.get_run(claim.run.id).version, - target=RunStatus.FAILED, + expected_run_version=current.version, + target=target, event_type="run.execution.failed", event_data={ "error_type": type(exc).__name__, "message": str(exc), }, ) + self.structured_logger.emit( + "run.failed", + context=( + claim_context.trace_context + if claim_context is not None + else None + ), + fields={ + "run_id": str(failed.id), + "tenant_id": failed.tenant_id, + "status": failed.status.value, + "error_type": type(exc).__name__, + "error": str(exc), + }, + ) + self._forget_context_if_terminal(failed) + return failed def _execute_claim( self, @@ -123,37 +270,80 @@ def _execute_claim( raise RuntimeError(f"execution plan {claim.run.plan_id} is not registered") context = self._contexts.get(claim.run.id) if context is None: - context = RunContext.local(revision_id=claim.run.revision_id) + context = self._restore_context(claim.run) run = claim.run steps = 0 by_id = {node.id: node for node in registration.plan.nodes} while run.next_nodes and (max_steps is None or steps < max_steps): + context.ensure_active() + durable = self.store.get_run(run.id) + if durable.status in TERMINAL_RUN_STATUSES: + return durable + if durable.version != run.version: + raise ConflictError("run changed while worker was executing") node_id = run.next_nodes[0] node = by_id[node_id] state_value = thaw_json(run.state) assert isinstance(state_value, dict) handler = getattr(registration.workflow, node.id) + latest = self.store.latest_checkpoint(run.id) + checkpoint_sequence = 1 if latest is None else latest.sequence + 1 + + def request_interrupt( + kind: InterruptKind, + request: Mapping[str, object], + timeout_seconds: float | None, + ) -> ApprovalDecision: + for existing in self.store.list_interrupts( + tenant_id=run.tenant_id, + pending_only=False, + ): + if ( + existing.run_id == run.id + and existing.request.get("_openrath_node_id") == node.id + and existing.request.get( + "_openrath_checkpoint_sequence" + ) + == checkpoint_sequence + ): + if existing.decision is None: + raise _RunSuspended("run is waiting for a decision") + return existing.decision + interrupt = Interrupt.create( + run_id=run.id, + kind=kind, + request={ + **request, + "_openrath_node_id": node.id, + "_openrath_checkpoint_sequence": checkpoint_sequence, + }, + timeout_seconds=timeout_seconds, + ) + self.store.create_interrupt( + interrupt, + expected_run_version=run.version, + ) + raise _RunSuspended("run was suspended for a decision") + step_context = StepContext( run_id=run.id, request=context, worker_id=claim.lease.holder_worker_id, fencing_token=claim.lease.fencing_token, + _interrupt_handler=request_interrupt, ) with self.telemetry.span( "openrath.node", context=context.trace_context, attributes={"run_id": str(run.id), "node_id": node.id}, ): - if node.kind is NodeKind.ROUTER: - result = handler(state_value) - else: - result = handler(state_value, step_context) - if inspect.isawaitable(result): - from rath._async.runtime import runtime as async_runtime - - result = async_runtime().run( - cast(Coroutine[Any, Any, object], result) - ) + result = self._invoke_with_retry( + handler, + state_value, + step_context, + node=node, + run=run, + ) next_nodes: tuple[str, ...] if node.kind is NodeKind.ROUTER: @@ -178,10 +368,9 @@ def _execute_claim( ) next_nodes = node.successors - latest = self.store.latest_checkpoint(run.id) checkpoint = Checkpoint.create( run_id=run.id, - sequence=1 if latest is None else latest.sequence + 1, + sequence=checkpoint_sequence, plan_hash=registration.plan.definition_hash, state=next_state, next_nodes=next_nodes, @@ -200,11 +389,160 @@ def _execute_claim( ) if not run.next_nodes: - return self.store.finish_claim( + completed = self.store.finish_claim( run.id, worker_id=claim.lease.holder_worker_id, fencing_token=claim.lease.fencing_token, expected_run_version=run.version, target=RunStatus.SUCCEEDED, ) + self.structured_logger.emit( + "run.completed", + context=context.trace_context, + fields={ + "run_id": str(completed.id), + "tenant_id": completed.tenant_id, + "status": completed.status.value, + }, + ) + return completed return run + + def _invoke_with_retry( + self, + handler: object, + state: dict[str, object], + step_context: StepContext, + *, + node: object, + run: Run, + ) -> object: + from rath.definition import NodeSpec + + spec = cast(NodeSpec, node) + last_error: BaseException | None = None + for attempt in range(1, spec.retry.max_attempts + 1): + try: + callable_handler = cast(Any, handler) + if spec.is_async: + value = callable_handler( + state, + step_context, + ) if spec.kind is not NodeKind.ROUTER else callable_handler(state) + assert inspect.isawaitable(value) + from rath._async.runtime import runtime as async_runtime + + coroutine = cast(Coroutine[Any, Any, object], value) + if spec.timeout_seconds is not None: + import asyncio + + coroutine = cast( + Coroutine[Any, Any, object], + asyncio.wait_for(coroutine, spec.timeout_seconds), + ) + return async_runtime().run(coroutine) + arguments = ( + (state,) + if spec.kind is NodeKind.ROUTER + else (state, step_context) + ) + if spec.timeout_seconds is None: + return callable_handler(*arguments) + pool = ThreadPoolExecutor(max_workers=1) + try: + future = pool.submit(callable_handler, *arguments) + return future.result(timeout=spec.timeout_seconds) + finally: + pool.shutdown(wait=False, cancel_futures=True) + except _RunSuspended: + raise + except BaseException as exc: + last_error = exc + self.store.append_run_event( + run.id, + "run.step.attempt.failed", + { + "node_id": spec.id, + "attempt": attempt, + "error_type": type(exc).__name__, + }, + ) + if attempt >= spec.retry.max_attempts: + raise + delay = min( + spec.retry.max_seconds, + spec.retry.base_seconds * (2 ** (attempt - 1)), + ) + time.sleep(delay) + assert last_error is not None + raise last_error + + def _heartbeat( + self, + claim: ClaimedRun, + lease_seconds: float, + stopped: threading.Event, + ) -> None: + interval = max(0.1, lease_seconds / 3) + while not stopped.wait(interval): + try: + self.store.renew_lease( + claim.run.id, + worker_id=claim.lease.holder_worker_id, + fencing_token=claim.lease.fencing_token, + lease_seconds=lease_seconds, + ) + except Exception: + return + + def _forget_context_if_terminal(self, run: Run) -> None: + if run.status in TERMINAL_RUN_STATUSES: + self._contexts.pop(run.id, None) + + @staticmethod + def _restore_context(run: Run) -> RunContext: + raw = thaw_json(run.context) + if not isinstance(raw, dict) or not raw: + return RunContext( + security=SecurityContext( + principal=Principal( + id="legacy-runtime-worker", + kind=PrincipalKind.SYSTEM, + claims={"context": "legacy-run"}, + ), + tenant_id=run.tenant_id, + ), + revision_id=run.revision_id, + ) + principal = raw["principal"] + assert isinstance(principal, dict) + deadline = raw.get("deadline") + return RunContext( + security=SecurityContext( + principal=Principal( + id=str(principal["id"]), + kind=PrincipalKind(str(principal["kind"])), + claims=cast( + Mapping[str, JSONValue], principal.get("claims") or {} + ), + ), + tenant_id=run.tenant_id, + project_id=( + str(raw["project_id"]) + if raw.get("project_id") is not None + else None + ), + grants=frozenset(str(item) for item in raw.get("grants", [])), + attributes=cast( + Mapping[str, JSONValue], raw.get("attributes") or {} + ), + ), + revision_id=run.revision_id, + request_id=UUID(str(raw["request_id"])), + trace_context=TraceContext( + trace_id=str(raw["trace_id"]), + span_id=str(raw["span_id"]), + sampled=bool(raw.get("sampled", True)), + ), + deadline=datetime.fromisoformat(str(deadline)) if deadline else None, + ) diff --git a/src/rath/runtime/migrations/postgres/0001_initial.sql b/src/rath/runtime/migrations/postgres/0001_initial.sql index 37366a8..57f9b03 100644 --- a/src/rath/runtime/migrations/postgres/0001_initial.sql +++ b/src/rath/runtime/migrations/postgres/0001_initial.sql @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS runs ( state_json JSONB NOT NULL, next_nodes_json JSONB NOT NULL, idempotency_key TEXT, + context_json JSONB NOT NULL DEFAULT '{}'::jsonb, + priority INTEGER NOT NULL DEFAULT 0, request_fingerprint TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, @@ -23,6 +25,10 @@ CREATE TABLE IF NOT EXISTS runs ( CREATE INDEX IF NOT EXISTS runs_tenant_status_idx ON runs (tenant_id, status, created_at); +CREATE UNIQUE INDEX IF NOT EXISTS runs_one_active_per_session_idx + ON runs (session_id) + WHERE status IN ('queued', 'running', 'waiting', 'needs_review'); + CREATE TABLE IF NOT EXISTS run_events ( run_id UUID NOT NULL REFERENCES runs(id) ON DELETE CASCADE, sequence BIGINT NOT NULL, @@ -51,6 +57,7 @@ CREATE TABLE IF NOT EXISTS interrupts ( kind TEXT NOT NULL, request_json JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ, decision_kind TEXT, decision_actor_id TEXT, decision_reason TEXT, @@ -61,6 +68,12 @@ CREATE TABLE IF NOT EXISTS interrupts ( CREATE INDEX IF NOT EXISTS interrupts_run_pending_idx ON interrupts (run_id, decided_at); +ALTER TABLE interrupts + ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS interrupts_expiry_idx + ON interrupts (expires_at) WHERE decided_at IS NULL; + CREATE TABLE IF NOT EXISTS run_leases ( id UUID PRIMARY KEY, run_id UUID NOT NULL UNIQUE REFERENCES runs(id) ON DELETE CASCADE, @@ -92,3 +105,66 @@ CREATE TABLE IF NOT EXISTS tool_invocations ( CREATE INDEX IF NOT EXISTS tool_invocations_reconcile_idx ON tool_invocations (status, effect_class, updated_at); + +CREATE TABLE IF NOT EXISTS server_sessions ( + id UUID PRIMARY KEY, + tenant_id TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS server_sessions_tenant_idx + ON server_sessions (tenant_id, created_at); + +CREATE TABLE IF NOT EXISTS server_assistants ( + tenant_id TEXT NOT NULL, + id TEXT NOT NULL, + template_id TEXT NOT NULL, + revision_id UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (tenant_id, id) +); + +CREATE TABLE IF NOT EXISTS feedback ( + id UUID PRIMARY KEY, + tenant_id TEXT NOT NULL, + run_id UUID NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + key TEXT NOT NULL, + score DOUBLE PRECISION, + value TEXT, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS feedback_run_idx ON feedback (run_id, created_at); + +ALTER TABLE runs + ADD COLUMN IF NOT EXISTS context_json JSONB NOT NULL DEFAULT '{}'::jsonb; + +ALTER TABLE runs + ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 0; + +CREATE TABLE IF NOT EXISTS evaluation_datasets ( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL, + examples_json JSONB NOT NULL, + UNIQUE (name, version) +); + +CREATE TABLE IF NOT EXISTS evaluation_experiments ( + id UUID PRIMARY KEY, + dataset_id UUID NOT NULL REFERENCES evaluation_datasets(id), + revision_id UUID NOT NULL, + results_json JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS evaluation_experiments_revision_idx + ON evaluation_experiments (revision_id, created_at); + +CREATE TABLE IF NOT EXISTS revisions ( + id UUID PRIMARY KEY, + code_digest TEXT NOT NULL, + plan_hash TEXT NOT NULL, + manifest_json JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); diff --git a/src/rath/runtime/models.py b/src/rath/runtime/models.py index c6d4ae0..498a1a7 100644 --- a/src/rath/runtime/models.py +++ b/src/rath/runtime/models.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from enum import Enum from uuid import UUID, uuid4 @@ -133,15 +133,22 @@ class Run: updated_at: datetime version: int = 0 idempotency_key: str | None = None + context: Mapping[str, JSONValue] = field(default_factory=dict) + priority: int = 0 def __post_init__(self) -> None: if not self.tenant_id.strip(): raise ValueError("run tenant_id must not be empty") if self.version < 0: raise ValueError("run version must not be negative") + if not -100 <= self.priority <= 100: + raise ValueError("run priority must be between -100 and 100") _aware(self.created_at, field_name="run.created_at") _aware(self.updated_at, field_name="run.updated_at") object.__setattr__(self, "state", freeze_mapping(self.state, field="run.state")) + object.__setattr__( + self, "context", freeze_mapping(self.context, field="run.context") + ) object.__setattr__(self, "next_nodes", tuple(self.next_nodes)) @classmethod @@ -156,6 +163,8 @@ def create( state: Mapping[str, object] | None = None, next_nodes: tuple[str, ...] = (), idempotency_key: str | None = None, + context: Mapping[str, object] | None = None, + priority: int = 0, id: UUID | None = None, ) -> "Run": now = datetime.now(timezone.utc) @@ -169,6 +178,8 @@ def create( state=freeze_mapping(state, field="run.state"), next_nodes=next_nodes, idempotency_key=idempotency_key, + context=freeze_mapping(context, field="run.context"), + priority=priority, created_at=now, updated_at=now, ) @@ -293,11 +304,16 @@ class Interrupt: kind: InterruptKind request: Mapping[str, JSONValue] created_at: datetime + expires_at: datetime | None = None decision: ApprovalDecision | None = None decided_at: datetime | None = None def __post_init__(self) -> None: _aware(self.created_at, field_name="interrupt.created_at") + if self.expires_at is not None: + _aware(self.expires_at, field_name="interrupt.expires_at") + if self.expires_at <= self.created_at: + raise ValueError("interrupt expires_at must be after created_at") if self.decided_at is not None: _aware(self.decided_at, field_name="interrupt.decided_at") if (self.decision is None) != (self.decided_at is None): @@ -315,13 +331,22 @@ def create( run_id: UUID, kind: InterruptKind, request: Mapping[str, object], + timeout_seconds: float | None = None, ) -> "Interrupt": + if timeout_seconds is not None and timeout_seconds <= 0: + raise ValueError("interrupt timeout_seconds must be positive") + created_at = datetime.now(timezone.utc) return cls( id=uuid4(), run_id=run_id, kind=kind, request=freeze_mapping(request, field="interrupt.request"), - created_at=datetime.now(timezone.utc), + created_at=created_at, + expires_at=( + created_at + timedelta(seconds=timeout_seconds) + if timeout_seconds is not None + else None + ), ) diff --git a/src/rath/runtime/postgres.py b/src/rath/runtime/postgres.py index a990c5e..9c1068c 100644 --- a/src/rath/runtime/postgres.py +++ b/src/rath/runtime/postgres.py @@ -54,23 +54,32 @@ def __init__(self, dsn: str, *, schema: str = "openrath") -> None: self.schema = schema self._closed = False self._migrate() - - def _connect(self) -> Any: - if self._closed: - raise RuntimeError("PostgresRunStore is closed") try: - import psycopg from psycopg import sql from psycopg.rows import dict_row + from psycopg_pool import ConnectionPool except ImportError as exc: raise RuntimeError( "Postgres support requires `pip install openrath[postgres]`" ) from exc - connection = psycopg.connect(self.dsn, row_factory=dict_row) + self._sql = sql + self._pool = ConnectionPool( + self.dsn, + min_size=1, + max_size=20, + timeout=10, + kwargs={"row_factory": dict_row}, + configure=self._configure_connection, + open=True, + ) + + def _configure_connection(self, connection: Any) -> None: connection.execute( - sql.SQL("SET search_path TO {}").format(sql.Identifier(self.schema)) + self._sql.SQL("SET search_path TO {}").format( + self._sql.Identifier(self.schema) + ) ) - return connection + connection.commit() def _migrate(self) -> None: try: @@ -105,17 +114,20 @@ def _migrate(self) -> None: @contextmanager def _transaction(self) -> Iterator[Any]: - connection = self._connect() - try: + if self._closed: + raise RuntimeError("PostgresRunStore is closed") + with self._pool.connection() as connection: + yield connection + + @contextmanager + def connection(self) -> Iterator[Any]: + """Borrow a schema-configured pooled connection for related stores.""" + with self._transaction() as connection: yield connection - connection.commit() - except BaseException: - connection.rollback() - raise - finally: - connection.close() def close(self) -> None: + if not self._closed: + self._pool.close() self._closed = True def create_run(self, run: Run) -> Run: @@ -140,10 +152,11 @@ def create_run(self, run: Run) -> Run: """ INSERT INTO runs( id, plan_id, revision_id, session_id, tenant_id, status, - state_json, next_nodes_json, idempotency_key, + state_json, next_nodes_json, idempotency_key, context_json, + priority, request_fingerprint, created_at, updated_at, version ) VALUES ( - %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s + %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ) ON CONFLICT DO NOTHING RETURNING id @@ -158,6 +171,8 @@ def create_run(self, run: Run) -> Run: _json(run.state), _json(run.next_nodes), run.idempotency_key, + _json(run.context), + run.priority, fingerprint, run.created_at, run.updated_at, @@ -263,6 +278,16 @@ def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: for row in rows ) + def append_run_event( + self, + run_id: UUID, + type: str, + data: Mapping[str, object], + ) -> RunEvent: + with self._transaction() as connection: + self._required_run_row(connection, run_id) + return self._append_event(connection, run_id, type, data) + def append_checkpoint(self, checkpoint: Checkpoint) -> None: with self._transaction() as connection: self._required_run_row(connection, checkpoint.run_id, lock=True) @@ -405,8 +430,10 @@ def create_interrupt( assert_transition(current.status, RunStatus.WAITING) connection.execute( """ - INSERT INTO interrupts(id, run_id, kind, request_json, created_at) - VALUES (%s, %s, %s, %s, %s) + INSERT INTO interrupts( + id, run_id, kind, request_json, created_at, expires_at + ) + VALUES (%s, %s, %s, %s, %s, %s) """, ( interrupt.id, @@ -414,6 +441,7 @@ def create_interrupt( interrupt.kind.value, _json(interrupt.request), interrupt.created_at, + interrupt.expires_at, ), ) row = self._update_status( @@ -422,6 +450,13 @@ def create_interrupt( target=RunStatus.WAITING, expected_version=expected_run_version, ) + connection.execute( + """ + UPDATE run_leases SET active = FALSE, updated_at = %s + WHERE run_id = %s + """, + (interrupt.created_at, interrupt.run_id), + ) self._append_event( connection, interrupt.run_id, @@ -439,6 +474,86 @@ def get_interrupt(self, interrupt_id: UUID) -> Interrupt: raise KeyError(str(interrupt_id)) return self._interrupt_from_row(row) + def list_interrupts( + self, + *, + tenant_id: str, + pending_only: bool = True, + ) -> tuple[Interrupt, ...]: + pending_clause = "AND i.decided_at IS NULL" if pending_only else "" + with self.connection() as connection: + rows = connection.execute( + f""" + SELECT i.* FROM interrupts AS i + JOIN runs AS r ON r.id = i.run_id + WHERE r.tenant_id = %s {pending_clause} + ORDER BY i.created_at, i.id + """, + (tenant_id,), + ).fetchall() + return tuple(self._interrupt_from_row(row) for row in rows) + + def expire_interrupts( + self, + *, + now: datetime | None = None, + ) -> tuple[UUID, ...]: + expired_at = now or _now() + if expired_at.tzinfo is None: + raise ValueError("now must be timezone-aware") + expired: list[UUID] = [] + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT i.*, r.version AS run_version, r.status AS run_status + FROM interrupts AS i + JOIN runs AS r ON r.id = i.run_id + WHERE i.decided_at IS NULL + AND i.expires_at IS NOT NULL + AND i.expires_at <= %s + ORDER BY i.expires_at, i.id + FOR UPDATE OF i, r SKIP LOCKED + """, + (expired_at,), + ).fetchall() + for row in rows: + connection.execute( + """ + UPDATE interrupts SET decision_kind = %s, + decision_actor_id = %s, decision_reason = %s, + decision_payload_json = %s, decided_at = %s + WHERE id = %s AND decided_at IS NULL + """, + ( + ApprovalDecisionKind.REJECT.value, + "openrath-system", + "interrupt deadline expired", + _json({}), + expired_at, + row["id"], + ), + ) + if RunStatus(row["run_status"]) is RunStatus.WAITING: + current = self._run_from_row( + self._required_run_row( + connection, row["run_id"], lock=True + ) + ) + self._update_status( + connection, + current, + target=RunStatus.TIMED_OUT, + expected_version=int(row["run_version"]), + ) + self._append_event( + connection, + row["run_id"], + "run.interrupt.expired", + {"interrupt_id": str(row["id"])}, + ) + expired.append(row["id"]) + return tuple(expired) + def decide_interrupt( self, interrupt_id: UUID, @@ -513,7 +628,8 @@ def claim_next( row = connection.execute( """ SELECT * FROM runs WHERE status = %s - ORDER BY created_at, id FOR UPDATE SKIP LOCKED LIMIT 1 + ORDER BY priority DESC, created_at, id + FOR UPDATE SKIP LOCKED LIMIT 1 """, (RunStatus.QUEUED.value,), ).fetchone() @@ -775,7 +891,7 @@ def _append_event( run_id: UUID, type: str, data: Mapping[str, object], - ) -> None: + ) -> RunEvent: row = connection.execute( """ SELECT COALESCE(MAX(sequence), 0) AS sequence @@ -783,12 +899,20 @@ def _append_event( """, (run_id,), ).fetchone() + created_at = _now() connection.execute( """ INSERT INTO run_events(run_id, sequence, type, data_json, created_at) VALUES (%s, %s, %s, %s, %s) """, - (run_id, int(row["sequence"]) + 1, type, _json(data), _now()), + (run_id, int(row["sequence"]) + 1, type, _json(data), created_at), + ) + return RunEvent( + run_id=run_id, + sequence=int(row["sequence"]) + 1, + type=type, + data=freeze_json(data, field="run event data"), # type: ignore[arg-type] + created_at=created_at, ) def _fingerprint(self, run: Run) -> str: @@ -800,6 +924,7 @@ def _fingerprint(self, run: Run) -> str: "tenant_id": run.tenant_id, "state": run.state, "next_nodes": run.next_nodes, + "priority": run.priority, }, field="run fingerprint", ) @@ -825,6 +950,8 @@ def _run_from_row(row: Mapping[str, Any]) -> Run: state=row["state_json"], next_nodes=tuple(row["next_nodes_json"]), idempotency_key=row["idempotency_key"], + context=row["context_json"], + priority=int(row["priority"]), created_at=row["created_at"], updated_at=row["updated_at"], version=int(row["version"]), @@ -862,6 +989,7 @@ def _interrupt_from_row(row: Mapping[str, Any]) -> Interrupt: kind=InterruptKind(row["kind"]), request=row["request_json"], created_at=row["created_at"], + expires_at=row["expires_at"], decision=decision, decided_at=row["decided_at"], ) diff --git a/src/rath/runtime/signals.py b/src/rath/runtime/signals.py new file mode 100644 index 0000000..d7e51e5 --- /dev/null +++ b/src/rath/runtime/signals.py @@ -0,0 +1,137 @@ +"""Optional low-latency signals; never a durable Run source of truth.""" + +from __future__ import annotations + +import json +import queue +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from typing import Protocol, runtime_checkable +from uuid import UUID + +__all__ = [ + "GuardedSignalBus", + "InMemorySignalBus", + "RedisSignalBus", + "RunSignal", + "SignalBus", + "SignalKind", +] + + +class SignalKind(str, Enum): + WAKE = "wake" + CANCEL = "cancel" + + +@dataclass(frozen=True, slots=True) +class RunSignal: + kind: SignalKind + run_id: UUID + tenant_id: str + created_at: datetime + + def __post_init__(self) -> None: + if not self.tenant_id: + raise ValueError("tenant_id must not be empty") + if self.created_at.tzinfo is None: + raise ValueError("created_at must be timezone-aware") + + +@runtime_checkable +class SignalBus(Protocol): + def publish(self, signal: RunSignal) -> None: ... + + def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: ... + + +class InMemorySignalBus: + def __init__(self) -> None: + self._queue: queue.Queue[RunSignal] = queue.Queue() + + def publish(self, signal: RunSignal) -> None: + self._queue.put_nowait(signal) + + def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: + try: + return self._queue.get(timeout=timeout_seconds) + except queue.Empty: + return None + + +class RedisSignalBus: + """Redis list transport used only to reduce polling latency.""" + + def __init__( + self, + url: str, + *, + namespace: str = "openrath", + client: object | None = None, + ) -> None: + if not namespace or any(char.isspace() for char in namespace): + raise ValueError("namespace must be a non-empty token") + if client is None: + try: + import redis + except ImportError as exc: + raise RuntimeError( + "Redis signaling requires `pip install openrath[redis]`" + ) from exc + client = redis.Redis.from_url( + url, + decode_responses=True, + socket_connect_timeout=2, + socket_timeout=2, + ) + self.client = client + self.key = f"{namespace}:run-signals" + + def publish(self, signal: RunSignal) -> None: + payload = json.dumps( + { + "kind": signal.kind.value, + "run_id": str(signal.run_id), + "tenant_id": signal.tenant_id, + "created_at": signal.created_at.isoformat(), + }, + separators=(",", ":"), + ) + self.client.lpush(self.key, payload) # type: ignore[attr-defined] + self.client.ltrim(self.key, 0, 9999) # type: ignore[attr-defined] + + def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: + timeout = max(0, int(timeout_seconds)) + result = self.client.brpop(self.key, timeout=timeout) # type: ignore[attr-defined] + if result is None: + return None + _, payload = result + data = json.loads(payload) + return RunSignal( + kind=SignalKind(data["kind"]), + run_id=UUID(data["run_id"]), + tenant_id=data["tenant_id"], + created_at=datetime.fromisoformat(data["created_at"]), + ) + + +class GuardedSignalBus: + """Best-effort wrapper preserving database success when Redis is unavailable.""" + + def __init__(self, inner: SignalBus) -> None: + self.inner = inner + self.failures = 0 + + def publish(self, signal: RunSignal) -> None: + try: + self.inner.publish(signal) + except Exception: + self.failures += 1 + + def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: + try: + return self.inner.receive(timeout_seconds=timeout_seconds) + except Exception: + self.failures += 1 + return None diff --git a/src/rath/runtime/sqlite.py b/src/rath/runtime/sqlite.py index fcf89f4..c13d4da 100644 --- a/src/rath/runtime/sqlite.py +++ b/src/rath/runtime/sqlite.py @@ -47,6 +47,8 @@ state_json TEXT NOT NULL, next_nodes_json TEXT NOT NULL, idempotency_key TEXT, + context_json TEXT NOT NULL DEFAULT '{}', + priority INTEGER NOT NULL DEFAULT 0, request_fingerprint TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, @@ -57,6 +59,10 @@ CREATE INDEX IF NOT EXISTS runs_tenant_status_idx ON runs (tenant_id, status, created_at); +CREATE UNIQUE INDEX IF NOT EXISTS runs_one_active_per_session_idx + ON runs (session_id) + WHERE status IN ('queued', 'running', 'waiting', 'needs_review'); + CREATE TABLE IF NOT EXISTS run_events ( run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, sequence INTEGER NOT NULL, @@ -85,6 +91,7 @@ kind TEXT NOT NULL, request_json TEXT NOT NULL, created_at TEXT NOT NULL, + expires_at TEXT, decision_kind TEXT, decision_actor_id TEXT, decision_reason TEXT, @@ -126,6 +133,63 @@ CREATE INDEX IF NOT EXISTS tool_invocations_reconcile_idx ON tool_invocations (status, effect_class, updated_at); + +CREATE TABLE IF NOT EXISTS server_sessions ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS server_sessions_tenant_idx + ON server_sessions (tenant_id, created_at); + +CREATE TABLE IF NOT EXISTS server_assistants ( + tenant_id TEXT NOT NULL, + id TEXT NOT NULL, + template_id TEXT NOT NULL, + revision_id TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (tenant_id, id) +); + +CREATE TABLE IF NOT EXISTS feedback ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE, + key TEXT NOT NULL, + score REAL, + value TEXT, + created_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS feedback_run_idx ON feedback (run_id, created_at); + +CREATE TABLE IF NOT EXISTS evaluation_datasets ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL, + examples_json TEXT NOT NULL, + UNIQUE (name, version) +); + +CREATE TABLE IF NOT EXISTS evaluation_experiments ( + id TEXT PRIMARY KEY, + dataset_id TEXT NOT NULL REFERENCES evaluation_datasets(id), + revision_id TEXT NOT NULL, + results_json TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS evaluation_experiments_revision_idx + ON evaluation_experiments (revision_id, created_at); + +CREATE TABLE IF NOT EXISTS revisions ( + id TEXT PRIMARY KEY, + code_digest TEXT NOT NULL, + plan_hash TEXT NOT NULL, + manifest_json TEXT NOT NULL, + created_at TEXT NOT NULL +); """ @@ -183,6 +247,34 @@ def _migrate(self) -> None: connection = self._connect() try: connection.executescript(_SCHEMA) + columns = { + row[1] + for row in connection.execute("PRAGMA table_info(runs)").fetchall() + } + if "context_json" not in columns: + connection.execute( + "ALTER TABLE runs ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'" + ) + if "priority" not in columns: + connection.execute( + "ALTER TABLE runs ADD COLUMN priority INTEGER NOT NULL DEFAULT 0" + ) + interrupt_columns = { + row[1] + for row in connection.execute( + "PRAGMA table_info(interrupts)" + ).fetchall() + } + if "expires_at" not in interrupt_columns: + connection.execute( + "ALTER TABLE interrupts ADD COLUMN expires_at TEXT" + ) + connection.execute( + """ + CREATE INDEX IF NOT EXISTS interrupts_expiry_idx + ON interrupts (expires_at) WHERE decided_at IS NULL + """ + ) connection.execute( """ INSERT OR IGNORE INTO schema_migrations(version, applied_at) @@ -231,9 +323,10 @@ def create_run(self, run: Run) -> Run: """ INSERT INTO runs( id, plan_id, revision_id, session_id, tenant_id, status, - state_json, next_nodes_json, idempotency_key, + state_json, next_nodes_json, idempotency_key, context_json, + priority, request_fingerprint, created_at, updated_at, version - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( str(run.id), @@ -245,6 +338,8 @@ def create_run(self, run: Run) -> Run: _dump(run.state), _dump(run.next_nodes), run.idempotency_key, + _dump(run.context), + run.priority, fingerprint, run.created_at.isoformat(), run.updated_at.isoformat(), @@ -359,6 +454,16 @@ def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: for row in rows ) + def append_run_event( + self, + run_id: UUID, + type: str, + data: Mapping[str, object], + ) -> RunEvent: + with self._transaction() as connection: + self._required_run_row(connection, run_id) + return self._append_event(connection, run_id, type, data) + def append_checkpoint(self, checkpoint: Checkpoint) -> None: with self._transaction() as connection: self._required_run_row(connection, checkpoint.run_id) @@ -554,8 +659,8 @@ def create_interrupt( connection.execute( """ INSERT INTO interrupts( - id, run_id, kind, request_json, created_at - ) VALUES (?, ?, ?, ?, ?) + id, run_id, kind, request_json, created_at, expires_at + ) VALUES (?, ?, ?, ?, ?, ?) """, ( str(interrupt.id), @@ -563,6 +668,11 @@ def create_interrupt( interrupt.kind.value, _dump(interrupt.request), interrupt.created_at.isoformat(), + ( + interrupt.expires_at.isoformat() + if interrupt.expires_at is not None + else None + ), ), ) self._update_status( @@ -571,6 +681,13 @@ def create_interrupt( target=RunStatus.WAITING, expected_version=expected_run_version, ) + connection.execute( + """ + UPDATE run_leases SET active = 0, updated_at = ? + WHERE run_id = ? + """, + (interrupt.created_at.isoformat(), str(interrupt.run_id)), + ) self._append_event( connection, interrupt.run_id, @@ -594,6 +711,84 @@ def get_interrupt(self, interrupt_id: UUID) -> Interrupt: raise KeyError(str(interrupt_id)) return self._interrupt_from_row(row) + def list_interrupts( + self, + *, + tenant_id: str, + pending_only: bool = True, + ) -> tuple[Interrupt, ...]: + pending_clause = "AND i.decided_at IS NULL" if pending_only else "" + with self._connect() as connection: + rows = connection.execute( + f""" + SELECT i.* FROM interrupts AS i + JOIN runs AS r ON r.id = i.run_id + WHERE r.tenant_id = ? {pending_clause} + ORDER BY i.created_at, i.id + """, + (tenant_id,), + ).fetchall() + return tuple(self._interrupt_from_row(row) for row in rows) + + def expire_interrupts( + self, + *, + now: datetime | None = None, + ) -> tuple[UUID, ...]: + expired_at = now or _now() + if expired_at.tzinfo is None: + raise ValueError("now must be timezone-aware") + expired: list[UUID] = [] + with self._transaction() as connection: + rows = connection.execute( + """ + SELECT i.*, r.version AS run_version, r.status AS run_status + FROM interrupts AS i + JOIN runs AS r ON r.id = i.run_id + WHERE i.decided_at IS NULL + AND i.expires_at IS NOT NULL + AND i.expires_at <= ? + ORDER BY i.expires_at, i.id + """, + (expired_at.isoformat(),), + ).fetchall() + for row in rows: + run_id = UUID(row["run_id"]) + connection.execute( + """ + UPDATE interrupts SET decision_kind = ?, + decision_actor_id = ?, decision_reason = ?, + decision_payload_json = ?, decided_at = ? + WHERE id = ? AND decided_at IS NULL + """, + ( + ApprovalDecisionKind.REJECT.value, + "openrath-system", + "interrupt deadline expired", + _dump({}), + expired_at.isoformat(), + row["id"], + ), + ) + if RunStatus(row["run_status"]) is RunStatus.WAITING: + current = self._run_from_row( + self._required_run_row(connection, run_id) + ) + self._update_status( + connection, + current, + target=RunStatus.TIMED_OUT, + expected_version=int(row["run_version"]), + ) + self._append_event( + connection, + run_id, + "run.interrupt.expired", + {"interrupt_id": row["id"]}, + ) + expired.append(UUID(row["id"])) + return tuple(expired) + def decide_interrupt( self, interrupt_id: UUID, @@ -670,7 +865,7 @@ def claim_next( """ SELECT * FROM runs WHERE status = ? - ORDER BY created_at, id + ORDER BY priority DESC, created_at, id LIMIT 1 """, (RunStatus.QUEUED.value,), @@ -926,7 +1121,7 @@ def _append_event( run_id: UUID, type: str, data: Mapping[str, object], - ) -> None: + ) -> RunEvent: row = connection.execute( """ SELECT COALESCE(MAX(sequence), 0) AS sequence @@ -935,12 +1130,20 @@ def _append_event( (str(run_id),), ).fetchone() sequence = int(row["sequence"]) + 1 + created_at = _now() connection.execute( """ INSERT INTO run_events(run_id, sequence, type, data_json, created_at) VALUES (?, ?, ?, ?, ?) """, - (str(run_id), sequence, type, _dump(data), _now().isoformat()), + (str(run_id), sequence, type, _dump(data), created_at.isoformat()), + ) + return RunEvent( + run_id=run_id, + sequence=sequence, + type=type, + data=freeze_json(data, field="run event data"), # type: ignore[arg-type] + created_at=created_at, ) def _required_run_row( @@ -986,6 +1189,7 @@ def _fingerprint(self, run: Run) -> str: "tenant_id": run.tenant_id, "state": run.state, "next_nodes": run.next_nodes, + "priority": run.priority, } ) return hashlib.sha256(payload.encode("utf-8")).hexdigest() @@ -993,8 +1197,10 @@ def _fingerprint(self, run: Run) -> str: def _run_from_row(self, row: sqlite3.Row) -> Run: state = _load(row["state_json"]) next_nodes = _load(row["next_nodes_json"]) + context = _load(row["context_json"]) assert isinstance(state, dict) assert isinstance(next_nodes, list) + assert isinstance(context, dict) return Run( id=UUID(row["id"]), plan_id=UUID(row["plan_id"]), @@ -1005,6 +1211,8 @@ def _run_from_row(self, row: sqlite3.Row) -> Run: state=state, next_nodes=tuple(str(item) for item in next_nodes), idempotency_key=row["idempotency_key"], + context=context, + priority=int(row["priority"]), created_at=_parse_time(row["created_at"]), updated_at=_parse_time(row["updated_at"]), version=int(row["version"]), @@ -1048,6 +1256,11 @@ def _interrupt_from_row(self, row: sqlite3.Row) -> Interrupt: kind=InterruptKind(row["kind"]), request=request, created_at=_parse_time(row["created_at"]), + expires_at=( + _parse_time(row["expires_at"]) + if row["expires_at"] is not None + else None + ), decision=decision, decided_at=( _parse_time(row["decided_at"]) diff --git a/src/rath/runtime/store.py b/src/rath/runtime/store.py index b095dca..b168dd5 100644 --- a/src/rath/runtime/store.py +++ b/src/rath/runtime/store.py @@ -41,6 +41,13 @@ def transition_run( def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: ... + def append_run_event( + self, + run_id: UUID, + type: str, + data: Mapping[str, object], + ) -> RunEvent: ... + def append_checkpoint(self, checkpoint: Checkpoint) -> None: ... def latest_checkpoint(self, run_id: UUID) -> Checkpoint | None: ... @@ -65,6 +72,19 @@ def create_interrupt( def get_interrupt(self, interrupt_id: UUID) -> Interrupt: ... + def list_interrupts( + self, + *, + tenant_id: str, + pending_only: bool = True, + ) -> tuple[Interrupt, ...]: ... + + def expire_interrupts( + self, + *, + now: datetime | None = None, + ) -> tuple[UUID, ...]: ... + def decide_interrupt( self, interrupt_id: UUID, diff --git a/src/rath/server/__init__.py b/src/rath/server/__init__.py index a59a984..a574b85 100644 --- a/src/rath/server/__init__.py +++ b/src/rath/server/__init__.py @@ -1,5 +1,25 @@ from rath.server.app import AgentServer, create_app from rath.server.auth import AuthProvider, StaticTokenAuth +from rath.server.resources import ( + AssistantRecord, + FeedbackRecord, + InMemoryResourceStore, + PostgresResourceStore, + ResourceStore, + SessionRecord, + SQLiteResourceStore, +) -__all__ = ["AgentServer", "AuthProvider", "StaticTokenAuth", "create_app"] - +__all__ = [ + "AgentServer", + "AuthProvider", + "AssistantRecord", + "FeedbackRecord", + "InMemoryResourceStore", + "PostgresResourceStore", + "ResourceStore", + "SQLiteResourceStore", + "SessionRecord", + "StaticTokenAuth", + "create_app", +] diff --git a/src/rath/server/app.py b/src/rath/server/app.py index f8c4f7d..872194e 100644 --- a/src/rath/server/app.py +++ b/src/rath/server/app.py @@ -2,27 +2,71 @@ from __future__ import annotations +import asyncio import json -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Mapping +from contextlib import asynccontextmanager from dataclasses import dataclass -from uuid import UUID +from importlib.metadata import PackageNotFoundError, version +from typing import Any, cast +from uuid import UUID, uuid4 from starlette.applications import Starlette +from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import JSONResponse, Response, StreamingResponse from starlette.routing import Route -from rath._json import thaw_json +from rath._json import JSONValue, thaw_json +from rath.adapters import ( + AdapterRequestContext, + MemoryExecutor, + MemoryHandler, + MemoryNamespace, +) from rath.context import RunContext -from rath.errors import RathError -from rath.runtime import LocalRuntime, Run, RunStatus, SQLiteRunStore -from rath.security import SecurityContext +from rath.errors import ErrorCode, RathError +from rath.runtime import ( + ApprovalDecision, + ApprovalDecisionKind, + Interrupt, + LocalRuntime, + Run, + RunSignal, + RunStatus, + RunStore, + SignalBus, + SignalKind, +) +from rath.security import PolicyConstraints, SecurityContext, TrustLevel from rath.server.auth import AuthProvider +from rath.server.resources import ResourceStore, default_resource_store __all__ = ["AgentServer", "create_app"] +class _SecurityHeadersMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next: Any) -> Response: + requested = request.headers.get("x-request-id") + try: + request_id = str(UUID(requested)) if requested else str(uuid4()) + except ValueError: + request_id = str(uuid4()) + request.state.request_id = request_id + response = await call_next(request) + response.headers["X-Request-ID"] = request_id + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["Referrer-Policy"] = "no-referrer" + response.headers["Cache-Control"] = response.headers.get( + "Cache-Control", "no-store" + ) + return cast(Response, response) + + def _run_json(run: Run) -> dict[str, object]: + context = thaw_json(run.context) + correlation = context if isinstance(context, dict) else {} return { "id": str(run.id), "plan_id": str(run.plan_id), @@ -33,11 +77,44 @@ def _run_json(run: Run) -> dict[str, object]: "state": thaw_json(run.state), "next_nodes": list(run.next_nodes), "version": run.version, + "priority": run.priority, + "request_id": correlation.get("request_id"), + "trace_id": correlation.get("trace_id"), "created_at": run.created_at.isoformat(), "updated_at": run.updated_at.isoformat(), } +def _interrupt_json(value: Interrupt) -> dict[str, object]: + return { + "id": str(value.id), + "run_id": str(value.run_id), + "kind": value.kind.value, + "request": thaw_json(value.request), + "created_at": value.created_at.isoformat(), + "expires_at": ( + value.expires_at.isoformat() + if value.expires_at is not None + else None + ), + "decision": ( + { + "kind": value.decision.kind.value, + "actor_id": value.decision.actor_id, + "reason": value.decision.reason, + "payload": thaw_json(value.decision.payload), + } + if value.decision is not None + else None + ), + "decided_at": ( + value.decided_at.isoformat() + if value.decided_at is not None + else None + ), + } + + @dataclass(frozen=True, slots=True) class _Assistant: id: str @@ -48,15 +125,39 @@ class _Assistant: class AgentServer: def __init__( self, - store: SQLiteRunStore, + store: RunStore, runtime: LocalRuntime, *, auth: AuthProvider, + resources: ResourceStore | None = None, + embedded_worker: bool = False, + worker_id: str = "agent-server", + signals: SignalBus | None = None, + worker_lease_seconds: float = 30.0, + max_queued_runs_per_tenant: int = 1000, + memory_executor: MemoryExecutor | None = None, + memory_handler: MemoryHandler | None = None, ) -> None: self.store = store self.runtime = runtime self.auth = auth self.assistants: dict[str, _Assistant] = {} + self.resources = resources or default_resource_store(store) + self.embedded_worker = embedded_worker + self.worker_id = worker_id + self.signals = signals + if worker_lease_seconds <= 0: + raise ValueError("worker_lease_seconds must be positive") + self.worker_lease_seconds = worker_lease_seconds + if max_queued_runs_per_tenant < 1: + raise ValueError("max_queued_runs_per_tenant must be positive") + self.max_queued_runs_per_tenant = max_queued_runs_per_tenant + if (memory_executor is None) != (memory_handler is None): + raise ValueError( + "memory_executor and memory_handler must be configured together" + ) + self.memory_executor = memory_executor + self.memory_handler = memory_handler self.app = create_app(self) def register_assistant( @@ -77,6 +178,31 @@ def register_assistant( def create_app(server: AgentServer) -> Starlette: + try: + package_version = version("openrath") + except PackageNotFoundError: + package_version = "0+unknown" + + def error_response(code: str, message: str, status: int) -> JSONResponse: + return JSONResponse( + {"error": {"code": code, "message": message}}, status_code=status + ) + + async def json_body(request: Request) -> dict[str, object]: + maximum = 1024 * 1024 + content_length = request.headers.get("content-length") + if content_length is not None and int(content_length) > maximum: + raise ValueError("request body exceeds 1 MiB") + body = bytearray() + async for chunk in request.stream(): + body.extend(chunk) + if len(body) > maximum: + raise ValueError("request body exceeds 1 MiB") + value = json.loads(body or b"{}") + if not isinstance(value, dict): + raise ValueError("request body must be a JSON object") + return value + async def authenticate( request: Request, ) -> tuple[SecurityContext | None, JSONResponse | None]: @@ -103,21 +229,196 @@ async def info(request: Request) -> Response: { "name": "openrath-agent-server", "api_version": "v1", - "capabilities": ["runs", "events", "sse", "interrupts"], + "version": package_version, + "capabilities": [ + "assistants", + "sessions", + "runs", + "events", + "sse", + "interrupts", + "feedback", + ] + + (["store"] if server.memory_handler is not None else []), + } + ) + + async def openapi(request: Request) -> Response: + return JSONResponse( + { + "openapi": "3.1.0", + "info": { + "title": "OpenRath Agent Server", + "version": package_version, + }, + "paths": { + "/v1/assistants": {}, + "/v1/sessions": {}, + "/v1/runs": {}, + "/v1/runs/{run_id}/events": {}, + "/v1/runs/{run_id}/stream": {}, + "/v1/interrupts/{interrupt_id}/decision": {}, + "/v1/interrupts": {}, + "/v1/feedback": {}, + "/v1/store/items": {}, + "/v1/store/search": {}, + }, } ) + async def metrics(request: Request) -> Response: + queued = running = 0 + for tenant_id in server.resources.count_tenants(): + for run in server.store.list_runs(tenant_id=tenant_id): + queued += int(run.status is RunStatus.QUEUED) + running += int(run.status is RunStatus.RUNNING) + body = ( + "# TYPE openrath_runs gauge\n" + f'openrath_runs{{status="queued"}} {queued}\n' + f'openrath_runs{{status="running"}} {running}\n' + ) + return Response(body, media_type="text/plain; version=0.0.4") + async def list_assistants(request: Request) -> Response: context, error = await authenticate(request) if error: return error assert context is not None + templates = [ + { + "id": item.id, + "template_id": item.id, + "revision_id": str(item.revision_id), + "kind": "template", + } + for item in server.assistants.values() + ] + aliases = [ + { + "id": item.id, + "template_id": item.template_id, + "revision_id": str(item.revision_id), + "kind": "alias", + } + for item in server.resources.list_assistants(context.tenant_id) + ] + return JSONResponse( + {"items": templates + aliases} + ) + + async def create_assistant(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + body = await json_body(request) + assistant_id = str(body["id"]).strip() + template_id = str(body["template_id"]).strip() + if not assistant_id or not template_id: + raise ValueError("id and template_id are required") + if assistant_id in server.assistants: + raise ValueError("assistant id is reserved by a deployment template") + template = server.assistants[template_id] + item = server.resources.create_assistant( + tenant_id=context.tenant_id, + id=assistant_id, + template_id=template_id, + revision_id=template.revision_id, + ) + return JSONResponse( + { + "id": item.id, + "template_id": item.template_id, + "revision_id": str(item.revision_id), + "kind": "alias", + "created_at": item.created_at.isoformat(), + }, + status_code=201, + ) + except KeyError as exc: + return error_response( + "request.invalid_argument", + f"unknown deployment template: {exc}", + 400, + ) + except (ValueError, TypeError) as exc: + return error_response("request.invalid_argument", str(exc), 400) + + async def get_assistant(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + assistant_id = request.path_params["assistant_id"] + try: + item = server.assistants[assistant_id] + return JSONResponse( + { + "id": item.id, + "template_id": item.id, + "revision_id": str(item.revision_id), + "kind": "template", + } + ) + except KeyError: + try: + alias = server.resources.get_assistant( + context.tenant_id, assistant_id + ) + except KeyError: + return error_response( + "resource.not_found", "assistant not found", 404 + ) + return JSONResponse( + { + "id": alias.id, + "template_id": alias.template_id, + "revision_id": str(alias.revision_id), + "kind": "alias", + "created_at": alias.created_at.isoformat(), + } + ) + + async def create_session(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + session = server.resources.create_session(context.tenant_id) return JSONResponse( { - "items": [ - {"id": item.id, "revision_id": str(item.revision_id)} - for item in server.assistants.values() - ] + "id": str(session.id), + "tenant_id": session.tenant_id, + "created_at": session.created_at.isoformat(), + }, + status_code=201, + ) + + async def get_session(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + session = server.resources.get_session( + UUID(request.path_params["session_id"]) + ) + if session.tenant_id != context.tenant_id: + raise KeyError + except (KeyError, ValueError): + return error_response("resource.not_found", "session not found", 404) + runs = [ + _run_json(run) + for run in server.store.list_runs(tenant_id=context.tenant_id) + if run.session_id == session.id + ] + return JSONResponse( + { + "id": str(session.id), + "tenant_id": session.tenant_id, + "created_at": session.created_at.isoformat(), + "runs": runs, } ) @@ -127,19 +428,52 @@ async def create_run(request: Request) -> Response: return error assert context is not None try: - body = await request.json() - assistant = server.assistants[str(body["assistant_id"])] + body = await json_body(request) + assistant_id = str(body["assistant_id"]) + assistant = server.assistants.get(assistant_id) + if assistant is None: + alias = server.resources.get_assistant( + context.tenant_id, assistant_id + ) + assistant = server.assistants.get(alias.template_id) + if assistant is None or assistant.revision_id != alias.revision_id: + raise ValueError( + "assistant deployment revision is unavailable" + ) session_id = UUID(str(body["session_id"])) + try: + known_session = server.resources.get_session(session_id) + except KeyError: + known_session = None + if ( + known_session is not None + and known_session.tenant_id != context.tenant_id + ): + raise KeyError("session_id") run_context = RunContext( security=context, revision_id=assistant.revision_id, ) + state = body.get("state") or {} + if not isinstance(state, Mapping): + raise ValueError("state must be a JSON object") + queued = sum( + run.status is RunStatus.QUEUED + for run in server.store.list_runs(tenant_id=context.tenant_id) + ) + if queued >= server.max_queued_runs_per_tenant: + return error_response( + "resource.exhausted", + "tenant run queue is at capacity", + 429, + ) run = server.runtime.submit( assistant.workflow, session_id=session_id, context=run_context, - state=body.get("state") or {}, + state=cast(Mapping[str, object], state), idempotency_key=request.headers.get("idempotency-key"), + priority=int(str(body.get("priority", 0))), ) return JSONResponse(_run_json(run), status_code=201) except KeyError as exc: @@ -155,6 +489,46 @@ async def create_run(request: Request) -> Response: except RathError as exc: return JSONResponse({"error": exc.to_dict()}, status_code=409) + async def create_session_run(request: Request) -> Response: + try: + session_id = UUID(request.path_params["session_id"]) + body = await json_body(request) + except (ValueError, TypeError): + return error_response( + "request.invalid_argument", "invalid session or body", 400 + ) + body["session_id"] = str(session_id) + request._body = json.dumps(body).encode() # noqa: SLF001 + return await create_run(request) + + async def list_runs(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + limit = min(max(int(request.query_params.get("limit", "50")), 1), 200) + after = request.query_params.get("after") + after_id = UUID(after) if after else None + except ValueError: + return error_response( + "request.invalid_argument", "invalid pagination cursor", 400 + ) + values = list(server.store.list_runs(tenant_id=context.tenant_id)) + if after_id is not None: + try: + offset = next(i for i, item in enumerate(values) if item.id == after_id) + 1 + except StopIteration: + return error_response( + "request.invalid_argument", "unknown pagination cursor", 400 + ) + values = values[offset:] + page = values[:limit] + next_cursor = str(page[-1].id) if len(values) > limit else None + return JSONResponse( + {"items": [_run_json(item) for item in page], "next": next_cursor} + ) + async def get_run(request: Request) -> Response: context, error = await authenticate(request) if error: @@ -188,6 +562,15 @@ async def cancel_run(request: Request) -> Response: expected_version=run.version, target=RunStatus.CANCELLED, ) + if server.signals is not None: + server.signals.publish( + RunSignal( + kind=SignalKind.CANCEL, + run_id=run.id, + tenant_id=run.tenant_id, + created_at=cancelled.updated_at, + ) + ) return JSONResponse(_run_json(cancelled)) except (KeyError, ValueError): return JSONResponse( @@ -197,6 +580,33 @@ async def cancel_run(request: Request) -> Response: except RathError as exc: return JSONResponse({"error": exc.to_dict()}, status_code=409) + async def resume_run(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + run = server.store.get_run(UUID(request.path_params["run_id"])) + if run.tenant_id != context.tenant_id: + raise KeyError + body = await json_body(request) + if run.status is not RunStatus.NEEDS_REVIEW or body.get("confirm") is not True: + return error_response( + "request.invalid_argument", + "resume requires NEEDS_REVIEW status and confirm=true", + 400, + ) + resumed = server.store.transition_run( + run.id, + expected_version=run.version, + target=RunStatus.QUEUED, + ) + return JSONResponse(_run_json(resumed)) + except (KeyError, ValueError): + return error_response("resource.not_found", "run not found", 404) + except RathError as exc: + return JSONResponse({"error": exc.to_dict()}, status_code=409) + async def events(request: Request) -> Response: context, error = await authenticate(request) if error: @@ -212,7 +622,13 @@ async def events(request: Request) -> Response: {"error": {"code": "resource.not_found", "message": "run not found"}}, status_code=404, ) - after = int(request.query_params.get("after", "0")) + try: + after = int(request.query_params.get("after", "0")) + limit = min(max(int(request.query_params.get("limit", "200")), 1), 1000) + except ValueError: + return error_response( + "request.invalid_argument", "invalid event cursor", 400 + ) items = [ { "id": str(event.sequence), @@ -224,31 +640,330 @@ async def events(request: Request) -> Response: } for event in server.store.list_run_events(run_id) if event.sequence > after - ] - return JSONResponse({"items": items}) + ][:limit] + return JSONResponse( + { + "items": items, + "next": str(items[-1]["sequence"]) if len(items) == limit else None, + } + ) async def stream(request: Request) -> Response: + last_event = request.headers.get("last-event-id") response = await events(request) if response.status_code != 200: return response payload = json.loads(bytes(response.body)) + if last_event and "after" not in request.query_params: + payload["items"] = [ + item + for item in payload["items"] + if int(item["sequence"]) > int(last_event) + ] + follow = request.query_params.get("follow", "false").lower() == "true" + run_id = UUID(request.path_params["run_id"]) async def generate() -> AsyncIterator[str]: - for item in payload["items"]: - yield f"id: {item['sequence']}\nevent: {item['type']}\ndata: {json.dumps(item, separators=(',', ':'))}\n\n" + cursor = int(last_event or request.query_params.get("after", "0")) + initial = payload["items"] + while True: + emitted = False + items = initial or [ + { + "sequence": event.sequence, + "type": event.type, + "run_id": str(event.run_id), + "time": event.created_at.isoformat(), + "data": thaw_json(event.data), + } + for event in server.store.list_run_events(run_id) + if event.sequence > cursor + ] + initial = [] + for item in items: + emitted = True + cursor = int(item["sequence"]) + yield ( + f"id: {cursor}\nevent: {item['type']}\n" + f"data: {json.dumps(item, separators=(',', ':'))}\n\n" + ) + if not follow: + break + run = server.store.get_run(run_id) + if run.status in { + RunStatus.SUCCEEDED, + RunStatus.FAILED, + RunStatus.CANCELLED, + RunStatus.TIMED_OUT, + } and not emitted: + break + if not emitted: + yield ": keep-alive\n\n" + await asyncio.sleep(0.25) + + return StreamingResponse( + generate(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + ) + + async def decide_interrupt(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + interrupt_id = UUID(request.path_params["interrupt_id"]) + interrupt = server.store.get_interrupt(interrupt_id) + run = server.store.get_run(interrupt.run_id) + if run.tenant_id != context.tenant_id: + raise KeyError + body = await json_body(request) + payload = body.get("payload") or {} + if not isinstance(payload, Mapping): + raise ValueError("decision payload must be a JSON object") + decision = ApprovalDecision( + kind=ApprovalDecisionKind(str(body["kind"])), + actor_id=context.principal.id, + reason=str(body["reason"]), + payload=cast(Mapping[str, JSONValue], payload), + ) + updated = server.store.decide_interrupt( + interrupt_id, + decision=decision, + expected_run_version=run.version, + ) + return JSONResponse(_run_json(updated)) + except (KeyError, ValueError, TypeError) as exc: + return error_response("request.invalid_argument", str(exc), 400) + except RathError as exc: + return JSONResponse({"error": exc.to_dict()}, status_code=409) + + async def list_interrupts(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + limit = min(max(int(request.query_params.get("limit", "50")), 1), 200) + pending_only = ( + request.query_params.get("pending", "true").lower() != "false" + ) + except ValueError: + return error_response( + "request.invalid_argument", "invalid pagination limit", 400 + ) + values = server.store.list_interrupts( + tenant_id=context.tenant_id, + pending_only=pending_only, + ) + return JSONResponse( + { + "items": [_interrupt_json(item) for item in values[:limit]], + "next": ( + str(values[limit - 1].id) if len(values) > limit else None + ), + } + ) + + async def create_feedback(request: Request) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + try: + body = await json_body(request) + run = server.store.get_run(UUID(str(body["run_id"]))) + if run.tenant_id != context.tenant_id: + raise KeyError + score = body.get("score") + numeric_score = float(str(score)) if score is not None else None + if numeric_score is not None and not -1 <= numeric_score <= 1: + raise ValueError("score must be between -1 and 1") + key = str(body["key"]) + if not key: + raise ValueError("feedback key is required") + feedback = server.resources.create_feedback( + tenant_id=context.tenant_id, + run_id=run.id, + key=key, + score=numeric_score, + value=str(body["value"]) if body.get("value") is not None else None, + ) + return JSONResponse( + { + "id": str(feedback.id), + "run_id": str(feedback.run_id), + "key": feedback.key, + "score": feedback.score, + "value": feedback.value, + "created_at": feedback.created_at.isoformat(), + }, + status_code=201, + ) + except (KeyError, ValueError, TypeError) as exc: + return error_response("request.invalid_argument", str(exc), 400) + + async def execute_store_operation( + request: Request, + operation: str, + ) -> Response: + context, auth_error = await authenticate(request) + if auth_error: + return auth_error + assert context is not None + if server.memory_executor is None or server.memory_handler is None: + return error_response( + "runtime.unavailable", + "store capability is not configured", + 501, + ) + try: + body = await json_body(request) + requested_tenant = body.get("tenant_id") + if ( + requested_tenant is not None + and str(requested_tenant) != context.tenant_id + ): + raise PermissionError("memory namespace tenant mismatch") + namespace = MemoryNamespace( + tenant_id=context.tenant_id, + user_id=( + str(body["user_id"]) if body.get("user_id") is not None else None + ), + agent_id=( + str(body["agent_id"]) + if body.get("agent_id") is not None + else None + ), + session_id=( + str(body["session_id"]) + if body.get("session_id") is not None + else None + ), + # HTTP input never upgrades memory trust. + trust=TrustLevel.UNTRUSTED, + ) + payload = body.get("payload", {}) + if not isinstance(payload, Mapping): + raise ValueError("payload must be a JSON object") + run_context = RunContext( + security=context, + revision_id=UUID(int=0), + ) + adapter_context = AdapterRequestContext( + run_id=uuid4(), + node_id=f"store.{operation}", + tenant_id=context.tenant_id, + deadline=None, + trace_context=run_context.trace_context, + idempotency_key=request.headers.get("idempotency-key"), + policy_constraints=PolicyConstraints(), + ) + result = await server.memory_executor.execute( + server.memory_handler, + cast(Any, operation), + namespace, + cast(Mapping[str, object], payload), + adapter_context=adapter_context, + run_context=run_context, + ) + return JSONResponse({"result": result}) + except PermissionError as exc: + return error_response("security.forbidden", str(exc), 403) + except (KeyError, ValueError, TypeError) as exc: + return error_response("request.invalid_argument", str(exc), 400) + except RathError as exc: + status = ( + 403 + if exc.code + in { + ErrorCode.FORBIDDEN, + ErrorCode.APPROVAL_REQUIRED, + ErrorCode.POLICY_ERROR, + } + else 409 + ) + return JSONResponse({"error": exc.to_dict()}, status_code=status) + + async def put_store_item(request: Request) -> Response: + return await execute_store_operation(request, "put") - return StreamingResponse(generate(), media_type="text/event-stream") + async def search_store(request: Request) -> Response: + return await execute_store_operation(request, "search") - return Starlette( + async def delete_store_item(request: Request) -> Response: + return await execute_store_operation(request, "delete") + + async def worker_loop() -> None: + while True: + await asyncio.to_thread(server.store.expire_interrupts) + await asyncio.to_thread(server.store.requeue_expired_leases) + result = await asyncio.to_thread( + server.runtime.work_once, + worker_id=server.worker_id, + lease_seconds=server.worker_lease_seconds, + ) + await asyncio.sleep(0 if result is not None else 0.1) + + @asynccontextmanager + async def lifespan(app: Starlette) -> AsyncIterator[None]: + task = ( + asyncio.create_task(worker_loop(), name="openrath-embedded-worker") + if server.embedded_worker + else None + ) + try: + yield + finally: + if task is not None: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + app = Starlette( routes=[ Route("/health/live", live), Route("/health/ready", ready), Route("/info", info), - Route("/v1/assistants", list_assistants), + Route("/openapi.json", openapi), + Route("/metrics", metrics), + Route("/v1/assistants", list_assistants, methods=["GET"]), + Route("/v1/assistants", create_assistant, methods=["POST"]), + Route("/v1/assistants/{assistant_id}", get_assistant), + Route("/v1/sessions", create_session, methods=["POST"]), + Route("/v1/sessions/{session_id}", get_session), + Route( + "/v1/sessions/{session_id}/runs", + create_session_run, + methods=["POST"], + ), Route("/v1/runs", create_run, methods=["POST"]), + Route("/v1/runs", list_runs, methods=["GET"]), Route("/v1/runs/{run_id}", get_run), Route("/v1/runs/{run_id}/cancel", cancel_run, methods=["POST"]), + Route("/v1/runs/{run_id}/resume", resume_run, methods=["POST"]), Route("/v1/runs/{run_id}/events", events), Route("/v1/runs/{run_id}/stream", stream), - ] + Route("/v1/interrupts", list_interrupts, methods=["GET"]), + Route( + "/v1/interrupts/{interrupt_id}/decision", + decide_interrupt, + methods=["POST"], + ), + Route("/v1/feedback", create_feedback, methods=["POST"]), + Route("/v1/store/items", put_store_item, methods=["POST"]), + Route("/v1/store/items", delete_store_item, methods=["DELETE"]), + Route("/v1/store/search", search_store, methods=["POST"]), + ], + lifespan=lifespan, ) + + app.add_middleware(_SecurityHeadersMiddleware) + + return app diff --git a/src/rath/server/auth.py b/src/rath/server/auth.py index 3db8d1a..0ccc042 100644 --- a/src/rath/server/auth.py +++ b/src/rath/server/auth.py @@ -2,6 +2,8 @@ from __future__ import annotations +import hashlib +import hmac from typing import Protocol, runtime_checkable from rath.security import SecurityContext @@ -20,10 +22,18 @@ class StaticTokenAuth: def __init__(self, tokens: dict[str, SecurityContext]) -> None: if not tokens: raise ValueError("at least one authentication token is required") - self._tokens = dict(tokens) + self._tokens = { + hashlib.sha256(token.encode()).digest(): context + for token, context in tokens.items() + } async def authenticate(self, authorization: str | None) -> SecurityContext | None: if not authorization or not authorization.startswith("Bearer "): return None - return self._tokens.get(authorization.removeprefix("Bearer ").strip()) - + supplied = hashlib.sha256( + authorization.removeprefix("Bearer ").strip().encode() + ).digest() + for expected, context in self._tokens.items(): + if hmac.compare_digest(supplied, expected): + return context + return None diff --git a/src/rath/server/cli.py b/src/rath/server/cli.py new file mode 100644 index 0000000..bd20c7d --- /dev/null +++ b/src/rath/server/cli.py @@ -0,0 +1,133 @@ +"""Agent Server and schema migration command-line entry points.""" + +from __future__ import annotations + +import argparse +import contextlib +import importlib +import os +import signal +import threading +from typing import Any + + +def _load_reference(reference: str) -> Any: + if ":" not in reference: + raise ValueError("OPENRATH_APP must use module:attribute syntax") + module_name, attribute = reference.split(":", 1) + value = getattr(importlib.import_module(module_name), attribute) + if callable(value) and not hasattr(value, "router"): + value = value() + return value + + +def _load_app(reference: str) -> Any: + value = _load_reference(reference) + return getattr(value, "app", value) + + +def server_main() -> None: + parser = argparse.ArgumentParser(prog="openrath-server") + parser.add_argument( + "--app", + default=os.getenv("OPENRATH_APP"), + help="ASGI application factory or object as module:attribute", + ) + parser.add_argument("--host", default=os.getenv("OPENRATH_HOST", "0.0.0.0")) + parser.add_argument( + "--port", type=int, default=int(os.getenv("OPENRATH_PORT", "8000")) + ) + parser.add_argument( + "--workers", type=int, default=int(os.getenv("OPENRATH_WEB_WORKERS", "1")) + ) + arguments = parser.parse_args() + if not arguments.app: + parser.error("--app or OPENRATH_APP is required") + if arguments.workers != 1: + parser.error( + "process workers must be 1; scale OpenRath with container replicas" + ) + import uvicorn + + uvicorn.run( + _load_app(arguments.app), + host=arguments.host, + port=arguments.port, + workers=arguments.workers, + proxy_headers=False, + server_header=False, + ) + + +def migrate_main() -> None: + parser = argparse.ArgumentParser(prog="openrath-migrate") + parser.add_argument( + "--dsn", + default=os.getenv("OPENRATH_POSTGRES_DSN"), + help="PostgreSQL DSN; defaults to OPENRATH_POSTGRES_DSN", + ) + parser.add_argument("--schema", default=os.getenv("OPENRATH_DB_SCHEMA", "openrath")) + parser.add_argument( + "--check", + action="store_true", + help="verify the current schema without applying changes", + ) + arguments = parser.parse_args() + if not arguments.dsn: + parser.error("--dsn or OPENRATH_POSTGRES_DSN is required") + if arguments.check: + import psycopg + from psycopg import sql + + with psycopg.connect(arguments.dsn) as connection: + value = connection.execute( + sql.SQL( + "SELECT version FROM {}.schema_migrations ORDER BY version DESC LIMIT 1" + ).format(sql.Identifier(arguments.schema)) + ).fetchone() + if value is None or int(value[0]) < 1: + raise SystemExit("OpenRath schema is not current") + return + from rath.runtime import PostgresRunStore + + PostgresRunStore(arguments.dsn, schema=arguments.schema).close() + + +def worker_main() -> None: + parser = argparse.ArgumentParser(prog="openrath-worker") + parser.add_argument( + "--app", + default=os.getenv("OPENRATH_APP"), + help="AgentServer object or factory as module:attribute", + ) + parser.add_argument( + "--worker-id", + default=os.getenv("OPENRATH_WORKER_ID", os.getenv("HOSTNAME", "worker")), + ) + parser.add_argument( + "--lease-seconds", + type=float, + default=float(os.getenv("OPENRATH_WORKER_LEASE_SECONDS", "30")), + ) + arguments = parser.parse_args() + if not arguments.app: + parser.error("--app or OPENRATH_APP is required") + server = _load_reference(arguments.app) + if not hasattr(server, "runtime") or not hasattr(server, "store"): + parser.error("worker application reference must resolve to AgentServer") + stopped = threading.Event() + + def stop(signum: int, frame: object) -> None: + stopped.set() + + for name in ("SIGINT", "SIGTERM"): + with contextlib.suppress(AttributeError): + signal.signal(getattr(signal, name), stop) + while not stopped.is_set(): + server.store.expire_interrupts() + server.store.requeue_expired_leases() + result = server.runtime.work_once( + worker_id=arguments.worker_id, + lease_seconds=arguments.lease_seconds, + ) + stopped.wait(0 if result is not None else 0.1) diff --git a/src/rath/server/resources.py b/src/rath/server/resources.py new file mode 100644 index 0000000..f382999 --- /dev/null +++ b/src/rath/server/resources.py @@ -0,0 +1,495 @@ +"""Durable Session and Feedback resource persistence.""" + +from __future__ import annotations + +import sqlite3 +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Protocol, runtime_checkable +from uuid import UUID, uuid4 + +from rath.runtime import PostgresRunStore, RunStore, SQLiteRunStore + +__all__ = [ + "AssistantRecord", + "FeedbackRecord", + "InMemoryResourceStore", + "PostgresResourceStore", + "ResourceStore", + "SQLiteResourceStore", + "SessionRecord", + "default_resource_store", +] + + +@dataclass(frozen=True, slots=True) +class AssistantRecord: + id: str + tenant_id: str + template_id: str + revision_id: UUID + created_at: datetime + + +@dataclass(frozen=True, slots=True) +class SessionRecord: + id: UUID + tenant_id: str + created_at: datetime + + +@dataclass(frozen=True, slots=True) +class FeedbackRecord: + id: UUID + tenant_id: str + run_id: UUID + key: str + score: float | None + value: str | None + created_at: datetime + + +@runtime_checkable +class ResourceStore(Protocol): + def create_assistant( + self, + *, + tenant_id: str, + id: str, + template_id: str, + revision_id: UUID, + ) -> AssistantRecord: ... + + def get_assistant(self, tenant_id: str, id: str) -> AssistantRecord: ... + + def list_assistants(self, tenant_id: str) -> tuple[AssistantRecord, ...]: ... + + def create_session(self, tenant_id: str) -> SessionRecord: ... + + def get_session(self, session_id: UUID) -> SessionRecord: ... + + def ensure_session(self, session: SessionRecord) -> SessionRecord: ... + + def count_tenants(self) -> tuple[str, ...]: ... + + def create_feedback( + self, + *, + tenant_id: str, + run_id: UUID, + key: str, + score: float | None, + value: str | None, + ) -> FeedbackRecord: ... + + +class InMemoryResourceStore: + def __init__(self) -> None: + self.sessions: dict[UUID, SessionRecord] = {} + self.feedback: dict[UUID, FeedbackRecord] = {} + self.assistants: dict[tuple[str, str], AssistantRecord] = {} + + def create_assistant( + self, + *, + tenant_id: str, + id: str, + template_id: str, + revision_id: UUID, + ) -> AssistantRecord: + key = (tenant_id, id) + item = AssistantRecord( + id, tenant_id, template_id, revision_id, datetime.now(timezone.utc) + ) + existing = self.assistants.setdefault(key, item) + if ( + existing.template_id != template_id + or existing.revision_id != revision_id + ): + raise ValueError("assistant id already has a different revision") + return existing + + def get_assistant(self, tenant_id: str, id: str) -> AssistantRecord: + try: + return self.assistants[(tenant_id, id)] + except KeyError as exc: + raise KeyError(id) from exc + + def list_assistants(self, tenant_id: str) -> tuple[AssistantRecord, ...]: + return tuple( + value + for (owner, _), value in sorted(self.assistants.items()) + if owner == tenant_id + ) + + def create_session(self, tenant_id: str) -> SessionRecord: + value = SessionRecord(uuid4(), tenant_id, datetime.now(timezone.utc)) + self.sessions[value.id] = value + return value + + def get_session(self, session_id: UUID) -> SessionRecord: + try: + return self.sessions[session_id] + except KeyError as exc: + raise KeyError(str(session_id)) from exc + + def ensure_session(self, session: SessionRecord) -> SessionRecord: + existing = self.sessions.setdefault(session.id, session) + if existing.tenant_id != session.tenant_id: + raise ValueError("session tenant mismatch") + return existing + + def count_tenants(self) -> tuple[str, ...]: + return tuple(sorted({item.tenant_id for item in self.sessions.values()})) + + def create_feedback( + self, + *, + tenant_id: str, + run_id: UUID, + key: str, + score: float | None, + value: str | None, + ) -> FeedbackRecord: + item = FeedbackRecord( + uuid4(), + tenant_id, + run_id, + key, + score, + value, + datetime.now(timezone.utc), + ) + self.feedback[item.id] = item + return item + + +class SQLiteResourceStore: + """Resource store sharing the embedded Run database.""" + + def __init__(self, run_store: SQLiteRunStore) -> None: + self.path = str(run_store.path) + + def _connect(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.path) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + return connection + + def create_assistant( + self, + *, + tenant_id: str, + id: str, + template_id: str, + revision_id: UUID, + ) -> AssistantRecord: + item = AssistantRecord( + id, tenant_id, template_id, revision_id, datetime.now(timezone.utc) + ) + with self._connect() as connection: + connection.execute( + """ + INSERT OR IGNORE INTO server_assistants( + tenant_id, id, template_id, revision_id, created_at + ) VALUES (?, ?, ?, ?, ?) + """, + ( + tenant_id, + id, + template_id, + str(revision_id), + item.created_at.isoformat(), + ), + ) + existing = self.get_assistant(tenant_id, id) + if ( + existing.template_id != template_id + or existing.revision_id != revision_id + ): + raise ValueError("assistant id already has a different revision") + return existing + + def get_assistant(self, tenant_id: str, id: str) -> AssistantRecord: + with self._connect() as connection: + row = connection.execute( + """ + SELECT * FROM server_assistants + WHERE tenant_id = ? AND id = ? + """, + (tenant_id, id), + ).fetchone() + if row is None: + raise KeyError(id) + return AssistantRecord( + row["id"], + row["tenant_id"], + row["template_id"], + UUID(row["revision_id"]), + datetime.fromisoformat(row["created_at"]), + ) + + def list_assistants(self, tenant_id: str) -> tuple[AssistantRecord, ...]: + with self._connect() as connection: + rows = connection.execute( + """ + SELECT * FROM server_assistants + WHERE tenant_id = ? ORDER BY created_at, id + """, + (tenant_id,), + ).fetchall() + return tuple( + AssistantRecord( + row["id"], + row["tenant_id"], + row["template_id"], + UUID(row["revision_id"]), + datetime.fromisoformat(row["created_at"]), + ) + for row in rows + ) + + def create_session(self, tenant_id: str) -> SessionRecord: + value = SessionRecord(uuid4(), tenant_id, datetime.now(timezone.utc)) + with self._connect() as connection: + connection.execute( + """ + INSERT INTO server_sessions(id, tenant_id, created_at) + VALUES (?, ?, ?) + """, + (str(value.id), value.tenant_id, value.created_at.isoformat()), + ) + return value + + def get_session(self, session_id: UUID) -> SessionRecord: + with self._connect() as connection: + row = connection.execute( + "SELECT * FROM server_sessions WHERE id = ?", (str(session_id),) + ).fetchone() + if row is None: + raise KeyError(str(session_id)) + return SessionRecord( + UUID(row["id"]), + row["tenant_id"], + datetime.fromisoformat(row["created_at"]), + ) + + def ensure_session(self, session: SessionRecord) -> SessionRecord: + with self._connect() as connection: + connection.execute( + """ + INSERT OR IGNORE INTO server_sessions(id, tenant_id, created_at) + VALUES (?, ?, ?) + """, + (str(session.id), session.tenant_id, session.created_at.isoformat()), + ) + existing = self.get_session(session.id) + if existing.tenant_id != session.tenant_id: + raise ValueError("session tenant mismatch") + return existing + + def count_tenants(self) -> tuple[str, ...]: + with self._connect() as connection: + rows = connection.execute( + "SELECT DISTINCT tenant_id FROM server_sessions ORDER BY tenant_id" + ).fetchall() + return tuple(row["tenant_id"] for row in rows) + + def create_feedback( + self, + *, + tenant_id: str, + run_id: UUID, + key: str, + score: float | None, + value: str | None, + ) -> FeedbackRecord: + item = FeedbackRecord( + uuid4(), + tenant_id, + run_id, + key, + score, + value, + datetime.now(timezone.utc), + ) + with self._connect() as connection: + connection.execute( + """ + INSERT INTO feedback( + id, tenant_id, run_id, key, score, value, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + str(item.id), + item.tenant_id, + str(item.run_id), + item.key, + item.score, + item.value, + item.created_at.isoformat(), + ), + ) + return item + + +class PostgresResourceStore: + """Multi-replica resource store sharing the production Run schema.""" + + def __init__(self, run_store: PostgresRunStore) -> None: + self.run_store = run_store + + def create_assistant( + self, + *, + tenant_id: str, + id: str, + template_id: str, + revision_id: UUID, + ) -> AssistantRecord: + item = AssistantRecord( + id, tenant_id, template_id, revision_id, datetime.now(timezone.utc) + ) + with self.run_store.connection() as connection: + connection.execute( + """ + INSERT INTO server_assistants( + tenant_id, id, template_id, revision_id, created_at + ) VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (tenant_id, id) DO NOTHING + """, + (tenant_id, id, template_id, revision_id, item.created_at), + ) + existing = self.get_assistant(tenant_id, id) + if ( + existing.template_id != template_id + or existing.revision_id != revision_id + ): + raise ValueError("assistant id already has a different revision") + return existing + + def get_assistant(self, tenant_id: str, id: str) -> AssistantRecord: + with self.run_store.connection() as connection: + row = connection.execute( + """ + SELECT * FROM server_assistants + WHERE tenant_id = %s AND id = %s + """, + (tenant_id, id), + ).fetchone() + if row is None: + raise KeyError(id) + return AssistantRecord( + row["id"], + row["tenant_id"], + row["template_id"], + row["revision_id"], + row["created_at"], + ) + + def list_assistants(self, tenant_id: str) -> tuple[AssistantRecord, ...]: + with self.run_store.connection() as connection: + rows = connection.execute( + """ + SELECT * FROM server_assistants + WHERE tenant_id = %s ORDER BY created_at, id + """, + (tenant_id,), + ).fetchall() + return tuple( + AssistantRecord( + row["id"], + row["tenant_id"], + row["template_id"], + row["revision_id"], + row["created_at"], + ) + for row in rows + ) + + def create_session(self, tenant_id: str) -> SessionRecord: + value = SessionRecord(uuid4(), tenant_id, datetime.now(timezone.utc)) + with self.run_store.connection() as connection: + connection.execute( + """ + INSERT INTO server_sessions(id, tenant_id, created_at) + VALUES (%s, %s, %s) + """, + (value.id, value.tenant_id, value.created_at), + ) + return value + + def get_session(self, session_id: UUID) -> SessionRecord: + with self.run_store.connection() as connection: + row = connection.execute( + "SELECT * FROM server_sessions WHERE id = %s", (session_id,) + ).fetchone() + if row is None: + raise KeyError(str(session_id)) + return SessionRecord(row["id"], row["tenant_id"], row["created_at"]) + + def ensure_session(self, session: SessionRecord) -> SessionRecord: + with self.run_store.connection() as connection: + connection.execute( + """ + INSERT INTO server_sessions(id, tenant_id, created_at) + VALUES (%s, %s, %s) ON CONFLICT (id) DO NOTHING + """, + (session.id, session.tenant_id, session.created_at), + ) + existing = self.get_session(session.id) + if existing.tenant_id != session.tenant_id: + raise ValueError("session tenant mismatch") + return existing + + def count_tenants(self) -> tuple[str, ...]: + with self.run_store.connection() as connection: + rows = connection.execute( + "SELECT DISTINCT tenant_id FROM server_sessions ORDER BY tenant_id" + ).fetchall() + return tuple(row["tenant_id"] for row in rows) + + def create_feedback( + self, + *, + tenant_id: str, + run_id: UUID, + key: str, + score: float | None, + value: str | None, + ) -> FeedbackRecord: + item = FeedbackRecord( + uuid4(), + tenant_id, + run_id, + key, + score, + value, + datetime.now(timezone.utc), + ) + with self.run_store.connection() as connection: + connection.execute( + """ + INSERT INTO feedback( + id, tenant_id, run_id, key, score, value, created_at + ) VALUES (%s, %s, %s, %s, %s, %s, %s) + """, + ( + item.id, + item.tenant_id, + item.run_id, + item.key, + item.score, + item.value, + item.created_at, + ), + ) + return item + + +def default_resource_store(run_store: RunStore) -> ResourceStore: + if isinstance(run_store, SQLiteRunStore): + return SQLiteResourceStore(run_store) + if isinstance(run_store, PostgresRunStore): + return PostgresResourceStore(run_store) + return InMemoryResourceStore() diff --git a/tests/chaos/test_runtime_failures.py b/tests/chaos/test_runtime_failures.py new file mode 100644 index 0000000..d3731f3 --- /dev/null +++ b/tests/chaos/test_runtime_failures.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import threading +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from uuid import uuid4 + +from rath.context import RunContext +from rath.definition import EffectClass, step +from rath.flow import Workflow +from rath.runtime import LocalRuntime, RunStatus, SQLiteRunStore +from rath.session import Session + + +def test_cancel_wins_over_late_worker_checkpoint(tmp_path: Path) -> None: + entered = threading.Event() + release = threading.Event() + + class _Blocked(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def wait(self, state, context): # type: ignore[no-untyped-def] + entered.set() + release.wait(timeout=5) + return {"late": True} + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + submitted = runtime.submit( + _Blocked(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + with ThreadPoolExecutor(max_workers=1) as pool: + worker = pool.submit(runtime.work_once, worker_id="worker", lease_seconds=1) + assert entered.wait(timeout=2) + running = store.get_run(submitted.id) + cancelled = store.transition_run( + running.id, + expected_version=running.version, + target=RunStatus.CANCELLED, + ) + release.set() + result = worker.result(timeout=5) + + assert result is not None + assert result.status is RunStatus.CANCELLED + assert store.get_run(submitted.id) == cancelled + assert store.latest_checkpoint(submitted.id) is None + + +def test_duplicate_submission_does_not_duplicate_execution(tmp_path: Path) -> None: + executions = 0 + + class _Count(Workflow): + @step(entry=True, effects=EffectClass.IDEMPOTENT) + def count(self, state, context): # type: ignore[no-untyped-def] + nonlocal executions + executions += 1 + return state + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + context = RunContext.local(revision_id=uuid4()) + workflow = _Count() + session_id = uuid4() + first = runtime.submit( + workflow, + session_id=session_id, + context=context, + idempotency_key="delivery-1", + ) + second = runtime.submit( + workflow, + session_id=session_id, + context=context, + idempotency_key="delivery-1", + ) + runtime.work_once(worker_id="worker") + + assert first.id == second.id + assert executions == 1 diff --git a/tests/config/test_credentials_split.py b/tests/config/test_credentials_split.py index f007a62..c488336 100644 --- a/tests/config/test_credentials_split.py +++ b/tests/config/test_credentials_split.py @@ -146,3 +146,23 @@ def test_inline_key_takes_precedence_over_credentials( ) store = ConfigStore.load() assert store.get_llm_provider("main").api_key == "sk-inline" + + +def test_removing_last_key_deletes_stale_credentials_sidecar( + _isolate_openrath_home: Path, +) -> None: + store = ConfigStore(path=resolve_config_path()) + store.config.llm.providers["main"] = LLMProviderConfig( + provider_kind="openai", + model="gpt-5", + api_key="sk-remove-me", + ) + store.save() + assert _credentials_path().is_file() + + store.config.llm.providers["main"].api_key = None + store.save() + + assert not _credentials_path().exists() + reloaded = ConfigStore(path=resolve_config_path()) + assert reloaded.get_llm_provider("main").api_key is None diff --git a/tests/conformance/v2/test_adapter_contracts.py b/tests/conformance/v2/test_adapter_contracts.py index 12a107b..65731f5 100644 --- a/tests/conformance/v2/test_adapter_contracts.py +++ b/tests/conformance/v2/test_adapter_contracts.py @@ -7,14 +7,23 @@ from rath.adapters import ( AdapterRequestContext, + MemoryExecutor, + MemoryNamespace, + ProviderCapability, + ProviderExecutor, + ProviderSpec, + SandboxExecutor, + SandboxIsolation, + SandboxSpec, SchemaValidationError, ToolExecutor, ToolOutputTooLarge, ToolSpec, ) +from rath.artifacts import LocalArtifactStore from rath.context import RunContext from rath.definition import EffectClass -from rath.security import LocalTrustedPolicy, PolicyConstraints +from rath.security import LocalTrustedPolicy, PolicyConstraints, TrustLevel def _contexts(): # type: ignore[no-untyped-def] @@ -85,3 +94,73 @@ def test_tool_output_budget_is_enforced() -> None: ) ) + +def test_large_output_can_be_externalized_to_artifact_store(tmp_path) -> None: + run, adapter = _contexts() + spec = ToolSpec( + name="report", + version="1", + input_schema={"type": "object"}, + effects=EffectClass.READ_ONLY, + risk="low", + ) + result = asyncio.run( + ToolExecutor( + LocalTrustedPolicy(), + artifact_store=LocalArtifactStore(tmp_path / "artifacts"), + ).execute( + spec, + lambda arguments, context: {"data": "x" * 100}, + {}, + adapter_context=adapter, + run_context=run, + ) + ) + + assert result["artifact_uri"].startswith("artifact://local/") + assert result["size"] > 32 + + +def test_provider_sandbox_and_memory_share_context_policy_timeout_contract() -> None: + async def exercise() -> None: + run, adapter = _contexts() + policy = LocalTrustedPolicy() + provider_result = await ProviderExecutor(policy).execute( + ProviderSpec( + id="chat", + kind="openai", + model="gpt", + capabilities=frozenset({ProviderCapability.CHAT}), + ), + lambda request, spec, context: {"text": "ok"}, + {"messages": []}, + capability=ProviderCapability.CHAT, + adapter_context=adapter, + run_context=run, + ) + sandbox_result = await SandboxExecutor(policy).execute( + SandboxSpec( + id="local", + isolation=SandboxIsolation.TRUSTED_HOST, + network="deny", + ), + lambda operation, payload, spec, context: {"exit_code": 0}, + "command", + {"argv": ["true"]}, + adapter_context=adapter, + run_context=run, + ) + memory_result = await MemoryExecutor(policy).execute( + lambda operation, namespace, payload, context: {"items": []}, + "search", + MemoryNamespace(tenant_id="local", trust=TrustLevel.UNTRUSTED), + {"query": "q"}, + adapter_context=adapter, + run_context=run, + ) + + assert provider_result == {"text": "ok"} + assert sandbox_result == {"exit_code": 0} + assert memory_result == {"items": []} + + asyncio.run(exercise()) diff --git a/tests/deployment/test_revisions.py b/tests/deployment/test_revisions.py new file mode 100644 index 0000000..e9af25e --- /dev/null +++ b/tests/deployment/test_revisions.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from pathlib import Path + +from rath.deployment import DeploymentManifest, Revision, SQLiteRevisionStore +from rath.runtime import SQLiteRunStore + + +def test_revision_identity_is_deterministic_and_persistent(tmp_path: Path) -> None: + manifest = DeploymentManifest( + image_digest="a" * 64, + plan_hash="b" * 64, + python_version="3.12", + dependencies_digest="c" * 64, + resources={"provider": "openai"}, + ) + first = Revision.create(code_digest="d" * 64, manifest=manifest) + second = Revision.create(code_digest="d" * 64, manifest=manifest) + run_store = SQLiteRunStore(tmp_path / "runtime.db") + store = SQLiteRevisionStore(run_store) + + store.put(first) + + assert first.id == second.id + assert store.get(first.id) == first + assert store.put(second).id == first.id diff --git a/tests/eval/test_store.py b/tests/eval/test_store.py new file mode 100644 index 0000000..9e379c7 --- /dev/null +++ b/tests/eval/test_store.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from pathlib import Path +from uuid import uuid4 + +from rath.eval import ( + Dataset, + EvaluationResult, + Example, + Experiment, + SQLiteEvaluationStore, +) +from rath.runtime import SQLiteRunStore + + +def test_evaluation_dataset_and_experiment_persist(tmp_path: Path) -> None: + run_store = SQLiteRunStore(tmp_path / "runtime.db") + store = SQLiteEvaluationStore(run_store) + dataset = Dataset( + id=uuid4(), + name="qa", + version="1", + examples=(Example.create({"question": "q"}, {"answer": "a"}),), + ) + experiment = Experiment( + id=uuid4(), + dataset_id=dataset.id, + revision_id=uuid4(), + results=( + EvaluationResult( + evaluator="exact", + score=1, + passed=True, + reason="match", + ), + ), + ) + store.save_dataset(dataset) + store.save_experiment(experiment) + + assert store.get_dataset(dataset.id) == dataset + assert store.get_experiment(experiment.id) == experiment diff --git a/tests/integration/test_postgres_run_store.py b/tests/integration/test_postgres_run_store.py index 96a34df..dc96570 100644 --- a/tests/integration/test_postgres_run_store.py +++ b/tests/integration/test_postgres_run_store.py @@ -8,6 +8,13 @@ import pytest from rath.definition import EffectClass +from rath.eval import ( + Dataset, + EvaluationResult, + Example, + Experiment, + PostgresEvaluationStore, +) from rath.runtime import ( ApprovalDecision, ApprovalDecisionKind, @@ -21,6 +28,7 @@ RunStatus, arguments_digest, ) +from rath.server import PostgresResourceStore pytestmark = pytest.mark.skipif( not os.getenv("OPENRATH_TEST_POSTGRES_DSN"), @@ -82,6 +90,7 @@ def test_postgres_lifecycle_and_interrupt(store: PostgresRunStore) -> None: waiting = store.create_interrupt( interrupt, expected_run_version=running.version ) + assert store.list_interrupts(tenant_id="postgres-test") == (interrupt,) resumed = store.decide_interrupt( interrupt.id, decision=ApprovalDecision( @@ -94,11 +103,34 @@ def test_postgres_lifecycle_and_interrupt(store: PostgresRunStore) -> None: assert resumed.status is RunStatus.QUEUED assert store.get_interrupt(interrupt.id).decision is not None + assert store.list_interrupts(tenant_id="postgres-test") == () assert [event.sequence for event in store.list_run_events(queued.id)] == list( range(1, len(store.list_run_events(queued.id)) + 1) ) +def test_postgres_interrupt_deadline_is_atomic(store: PostgresRunStore) -> None: + queued = store.create_run(_run()) + running = store.transition_run( + queued.id, + expected_version=queued.version, + target=RunStatus.RUNNING, + ) + interrupt = Interrupt.create( + run_id=running.id, + kind=InterruptKind.INPUT, + request={"question": "continue?"}, + timeout_seconds=1, + ) + store.create_interrupt(interrupt, expected_run_version=running.version) + assert interrupt.expires_at is not None + + assert store.expire_interrupts( + now=interrupt.expires_at + timedelta(seconds=1) + ) == (interrupt.id,) + assert store.get_run(running.id).status is RunStatus.TIMED_OUT + + def test_postgres_idempotency_and_claim_are_concurrency_safe( store: PostgresRunStore, ) -> None: @@ -180,3 +212,67 @@ def test_postgres_effect_ledger_persists_ambiguous_dispatch( older_than=datetime.now(timezone.utc) + timedelta(seconds=1) ) assert stale[0].status.value == "ambiguous" + + +def test_postgres_server_resources_share_run_transaction_domain( + store: PostgresRunStore, +) -> None: + resources = PostgresResourceStore(store) + revision_id = uuid4() + assistant = resources.create_assistant( + tenant_id="postgres-test", + id="tenant-agent", + template_id="deployed-template", + revision_id=revision_id, + ) + session = resources.create_session("postgres-test") + run = store.create_run( + Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=session.id, + tenant_id="postgres-test", + ) + ) + feedback = resources.create_feedback( + tenant_id="postgres-test", + run_id=run.id, + key="quality", + score=1, + value=None, + ) + + assert resources.get_assistant("postgres-test", "tenant-agent") == assistant + assert resources.list_assistants("postgres-test") == (assistant,) + assert resources.list_assistants("other") == () + assert resources.get_session(session.id) == session + assert resources.count_tenants() == ("postgres-test",) + assert feedback.run_id == run.id + + +def test_postgres_evaluation_results_are_durable(store: PostgresRunStore) -> None: + evaluations = PostgresEvaluationStore(store) + dataset = Dataset( + id=uuid4(), + name="integration", + version="1", + examples=(Example.create({"input": "x"}, {"output": "y"}),), + ) + experiment = Experiment( + id=uuid4(), + dataset_id=dataset.id, + revision_id=uuid4(), + results=( + EvaluationResult( + evaluator="test", + score=1, + passed=True, + reason="ok", + ), + ), + ) + evaluations.save_dataset(dataset) + evaluations.save_experiment(experiment) + + assert evaluations.get_dataset(dataset.id) == dataset + assert evaluations.get_experiment(experiment.id) == experiment diff --git a/tests/integration/test_redis_signals.py b/tests/integration/test_redis_signals.py new file mode 100644 index 0000000..6787e66 --- /dev/null +++ b/tests/integration/test_redis_signals.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import os +from datetime import datetime, timezone +from uuid import uuid4 + +import pytest + +from rath.runtime import RedisSignalBus, RunSignal, SignalKind + +pytestmark = pytest.mark.skipif( + not os.getenv("OPENRATH_TEST_REDIS_URL"), + reason="OPENRATH_TEST_REDIS_URL is not configured", +) + + +def test_redis_signal_real_round_trip() -> None: + namespace = f"openrath-test-{uuid4().hex}" + bus = RedisSignalBus(os.environ["OPENRATH_TEST_REDIS_URL"], namespace=namespace) + signal = RunSignal( + kind=SignalKind.CANCEL, + run_id=uuid4(), + tenant_id="tenant", + created_at=datetime.now(timezone.utc), + ) + bus.publish(signal) + assert bus.receive(timeout_seconds=1) == signal diff --git a/tests/integration/test_v1_migration_postgres.py b/tests/integration/test_v1_migration_postgres.py new file mode 100644 index 0000000..2bd1d92 --- /dev/null +++ b/tests/integration/test_v1_migration_postgres.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path +from uuid import uuid4 + +import pytest + +from rath.runtime import PostgresRunStore, RunStatus +from rath.session import Session +from rath.session.chunk import ChunkTable +from rath.session.persistence.writer import SessionWriter + +pytestmark = pytest.mark.skipif( + not os.getenv("OPENRATH_TEST_POSTGRES_DSN"), + reason="OPENRATH_TEST_POSTGRES_DSN is not configured", +) + + +def test_v1_session_import_real_postgres(tmp_path: Path) -> None: + dsn = os.environ["OPENRATH_TEST_POSTGRES_DSN"] + schema = f"migration_{uuid4().hex}" + source = tmp_path / "sessions" + source.mkdir() + session = Session(chunk_table=ChunkTable(rows=())) + SessionWriter(session, path=source / f"{session.id}.jsonl").close() + report = tmp_path / "result.json" + artifact_root = tmp_path / "artifacts" + try: + result = subprocess.run( + [ + sys.executable, + "scripts/migrate_v1_to_v2.py", + "--source", + str(source), + "--report", + str(report), + "--tenant", + "migration-tenant", + "--apply", + "--postgres-dsn", + dsn, + "--schema", + schema, + "--artifact-root", + str(artifact_root), + ], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + payload = json.loads(report.read_text(encoding="utf-8")) + assert payload["summary"]["imported"] == 1 + store = PostgresRunStore(dsn, schema=schema) + runs = store.list_runs(tenant_id="migration-tenant") + assert len(runs) == 1 + assert runs[0].status is RunStatus.SUCCEEDED + assert runs[0].state["resumable"] is False + store.close() + finally: + import psycopg + from psycopg import sql + + with psycopg.connect(dsn) as connection: + connection.execute( + sql.SQL("DROP SCHEMA IF EXISTS {} CASCADE").format( + sql.Identifier(schema) + ) + ) diff --git a/tests/migration/test_v1_to_v2.py b/tests/migration/test_v1_to_v2.py new file mode 100644 index 0000000..8be67ea --- /dev/null +++ b/tests/migration/test_v1_to_v2.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +from rath.session import Session +from rath.session.chunk import ChunkTable +from rath.session.persistence.writer import SessionWriter + + +def test_v1_migration_inventory_is_read_only_and_machine_readable( + tmp_path: Path, +) -> None: + source = tmp_path / "sessions" + source.mkdir() + session = Session(chunk_table=ChunkTable(rows=())) + legacy_path = source / f"{session.id}.jsonl" + SessionWriter(session, path=legacy_path).close() + before = legacy_path.read_bytes() + report = tmp_path / "inventory.json" + + result = subprocess.run( + [ + sys.executable, + "scripts/migrate_v1_to_v2.py", + "--source", + str(source), + "--report", + str(report), + "--tenant", + "tenant-1", + ], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + assert legacy_path.read_bytes() == before + payload = json.loads(report.read_text(encoding="utf-8")) + assert payload["mode"] == "inventory" + assert payload["summary"] == { + "total": 1, + "ready": 1, + "imported": 0, + "invalid": 0, + } diff --git a/tests/observability/test_telemetry.py b/tests/observability/test_telemetry.py index 7ef4045..15717ba 100644 --- a/tests/observability/test_telemetry.py +++ b/tests/observability/test_telemetry.py @@ -3,7 +3,13 @@ from contextlib import contextmanager from rath.context import TraceContext -from rath.observability import GuardedTelemetry, InMemoryTelemetry, redact +from rath.observability import ( + GuardedTelemetry, + InMemoryTelemetry, + OpenTelemetry, + StructuredLogger, + redact, +) def test_span_records_status_and_correlation() -> None: @@ -37,3 +43,46 @@ def increment(self, name, value=1, *, attributes=None): # type: ignore[no-untyp value = 42 guarded.increment("counter") assert value == 42 + + +def test_otel_bridge_preserves_parent_trace_and_redacts_secrets() -> None: + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + telemetry = OpenTelemetry(tracer_provider=provider) + context = TraceContext.new() + with telemetry.span( + "openrath.run", + context=context, + attributes={"run_id": "r1", "api_key": "secret"}, + ): + pass + + span = exporter.get_finished_spans()[0] + assert f"{span.context.trace_id:032x}" == context.trace_id + assert span.attributes["api_key"] == "" + + +def test_structured_logger_correlates_and_redacts() -> None: + import json + + records: list[str] = [] + trace = TraceContext.new() + logger = StructuredLogger(records.append) + logger.emit( + "run.failed", + context=trace, + fields={"run_id": "run-1", "api_key": "secret"}, + ) + + parsed = json.loads(records[0]) + assert parsed["event"] == "run.failed" + assert parsed["trace_id"] == trace.trace_id + assert parsed["run_id"] == "run-1" + assert parsed["api_key"] == "" diff --git a/tests/runtime/test_local_runtime.py b/tests/runtime/test_local_runtime.py index 3a97f37..87c338c 100644 --- a/tests/runtime/test_local_runtime.py +++ b/tests/runtime/test_local_runtime.py @@ -5,9 +5,17 @@ from uuid import uuid4 from rath.context import RunContext -from rath.definition import EffectClass, router, step +from rath.definition import EffectClass, RetryPolicy, router, step from rath.flow import Workflow -from rath.runtime import LocalRuntime, RunStatus, SQLiteRunStore +from rath.runtime import ( + ApprovalDecision, + ApprovalDecisionKind, + InterruptKind, + LocalRuntime, + RunStatus, + SQLiteRunStore, +) +from rath.security import Principal, PrincipalKind, SecurityContext from rath.session import Session @@ -105,3 +113,167 @@ def forward(self, session: Session) -> Session: assert failed.status is RunStatus.FAILED assert any(event.type == "run.execution.failed" for event in store.list_run_events(failed.id)) + +def test_worker_restores_security_context_after_process_restart( + tmp_path: Path, +) -> None: + seen: list[tuple[str, str]] = [] + + class _ContextWorkflow(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def capture(self, state, context): # type: ignore[no-untyped-def] + seen.append( + ( + context.request.security.tenant_id, + context.request.security.principal.id, + ) + ) + return state + + def forward(self, session: Session) -> Session: + return session + + path = tmp_path / "runtime.db" + revision_id = uuid4() + workflow = _ContextWorkflow() + first_store = SQLiteRunStore(path) + first = LocalRuntime(first_store) + first.submit( + workflow, + session_id=uuid4(), + context=RunContext( + security=SecurityContext( + principal=Principal(id="user-42", kind=PrincipalKind.USER), + tenant_id="tenant-42", + grants=frozenset({"tool.read"}), + ), + revision_id=revision_id, + ), + ) + first_store.close() + + second_store = SQLiteRunStore(path) + second = LocalRuntime(second_store) + second.register(workflow, revision_id=revision_id) + completed = second.work_once(worker_id="worker-after-restart") + + assert completed is not None + assert completed.status is RunStatus.SUCCEEDED + assert seen == [("tenant-42", "user-42")] + + +def test_runtime_retries_declared_idempotent_step(tmp_path: Path) -> None: + attempts = 0 + + class _Retry(Workflow): + @step( + entry=True, + effects=EffectClass.IDEMPOTENT, + idempotency_key="run-step", + retry=RetryPolicy(max_attempts=3, base_seconds=0.001), + ) + def unstable(self, state, context): # type: ignore[no-untyped-def] + nonlocal attempts + attempts += 1 + if attempts < 3: + raise ConnectionError("temporary") + return {"ok": True} + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + submitted = runtime.submit( + _Retry(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + completed = runtime.work_once(worker_id="worker") + + assert completed is not None and completed.status is RunStatus.SUCCEEDED + assert attempts == 3 + failures = [ + event + for event in store.list_run_events(submitted.id) + if event.type == "run.step.attempt.failed" + ] + assert len(failures) == 2 + + +def test_runtime_enforces_async_step_timeout(tmp_path: Path) -> None: + import asyncio + + class _Timeout(Workflow): + @step( + entry=True, + effects=EffectClass.READ_ONLY, + timeout_seconds=0.01, + ) + async def slow(self, state, context): # type: ignore[no-untyped-def] + await asyncio.sleep(1) + return state + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + runtime.submit( + _Timeout(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + completed = runtime.work_once(worker_id="worker") + + assert completed is not None + assert completed.status is RunStatus.TIMED_OUT + + +def test_runtime_durably_suspends_and_resumes_human_decision( + tmp_path: Path, +) -> None: + class _Approval(Workflow): + @step(entry=True, effects=EffectClass.NON_IDEMPOTENT) + def approve(self, state, context): # type: ignore[no-untyped-def] + decision = context.interrupt( + InterruptKind.APPROVAL, + {"tool": "email.send", "arguments": {"to": "user@example.com"}}, + ) + return { + "decision": decision.kind.value, + "edited": decision.payload.get("arguments"), + } + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + submitted = runtime.submit( + _Approval(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + + waiting = runtime.work_once(worker_id="worker-1") + assert waiting is not None and waiting.status is RunStatus.WAITING + (interrupt,) = store.list_interrupts(tenant_id="local") + resumed = store.decide_interrupt( + interrupt.id, + decision=ApprovalDecision( + kind=ApprovalDecisionKind.EDIT, + actor_id="reviewer", + reason="replace recipient", + payload={"arguments": {"to": "safe@example.com"}}, + ), + expected_run_version=waiting.version, + ) + assert resumed.status is RunStatus.QUEUED + + completed = runtime.work_once(worker_id="worker-2") + assert completed is not None and completed.status is RunStatus.SUCCEEDED + assert completed.id == submitted.id + assert completed.state["decision"] == "edit" + assert completed.state["edited"] == {"to": "safe@example.com"} + assert len(store.list_interrupts(tenant_id="local", pending_only=False)) == 1 diff --git a/tests/runtime/test_scheduler_leases.py b/tests/runtime/test_scheduler_leases.py index 4cb4e71..ef1a092 100644 --- a/tests/runtime/test_scheduler_leases.py +++ b/tests/runtime/test_scheduler_leases.py @@ -20,6 +20,34 @@ def _run() -> Run: ) +def test_claim_prefers_higher_priority_then_fifo(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + low = store.create_run( + Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant-1", + priority=0, + ) + ) + high = store.create_run( + Run.create( + plan_id=uuid4(), + revision_id=uuid4(), + session_id=uuid4(), + tenant_id="tenant-2", + priority=10, + ) + ) + + claim = store.claim_next(worker_id="worker", lease_seconds=30) + + assert claim is not None + assert claim.run.id == high.id + assert claim.run.id != low.id + + def test_claim_is_exclusive_and_moves_run_to_running(tmp_path: Path) -> None: store = SQLiteRunStore(tmp_path / "runtime.db") queued = store.create_run(_run()) @@ -95,4 +123,3 @@ def test_expired_lease_is_requeued_with_new_fencing_token(tmp_path: Path) -> Non worker_id="worker-1", fencing_token=first.lease.fencing_token, ) - diff --git a/tests/runtime/test_signals.py b/tests/runtime/test_signals.py new file mode 100644 index 0000000..1369ede --- /dev/null +++ b/tests/runtime/test_signals.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from uuid import uuid4 + +from rath.runtime import ( + GuardedSignalBus, + InMemorySignalBus, + RunSignal, + SignalKind, +) + + +def test_in_memory_signal_round_trip() -> None: + bus = InMemorySignalBus() + signal = RunSignal( + kind=SignalKind.WAKE, + run_id=uuid4(), + tenant_id="tenant", + created_at=datetime.now(timezone.utc), + ) + bus.publish(signal) + assert bus.receive() == signal + + +def test_signal_failure_does_not_change_durable_outcome() -> None: + class Broken: + def publish(self, signal: RunSignal) -> None: + raise ConnectionError("redis down") + + def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: + raise ConnectionError("redis down") + + bus = GuardedSignalBus(Broken()) + bus.publish( + RunSignal( + kind=SignalKind.CANCEL, + run_id=uuid4(), + tenant_id="tenant", + created_at=datetime.now(timezone.utc), + ) + ) + assert bus.receive() is None + assert bus.failures == 2 diff --git a/tests/runtime/test_sqlite_run_store.py b/tests/runtime/test_sqlite_run_store.py index f42074b..5abc759 100644 --- a/tests/runtime/test_sqlite_run_store.py +++ b/tests/runtime/test_sqlite_run_store.py @@ -1,6 +1,7 @@ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor +from datetime import timedelta from pathlib import Path from uuid import uuid4 @@ -139,6 +140,8 @@ def test_interrupt_decision_and_waiting_resume_are_atomic(tmp_path: Path) -> Non expected_run_version=running.version, ) assert waiting.status is RunStatus.WAITING + assert store.list_interrupts(tenant_id="tenant-1") == (interrupt,) + assert store.list_interrupts(tenant_id="other") == () resumed = store.decide_interrupt( interrupt.id, @@ -149,11 +152,14 @@ def test_interrupt_decision_and_waiting_resume_are_atomic(tmp_path: Path) -> Non ), expected_run_version=waiting.version, ) - assert resumed.status is RunStatus.QUEUED decided = store.get_interrupt(interrupt.id) assert decided.decision is not None assert decided.decision.actor_id == "user-1" + assert store.list_interrupts(tenant_id="tenant-1") == () + assert store.list_interrupts( + tenant_id="tenant-1", pending_only=False + ) == (decided,) with pytest.raises(ConflictError, match="already decided"): store.decide_interrupt( interrupt.id, @@ -165,3 +171,31 @@ def test_interrupt_decision_and_waiting_resume_are_atomic(tmp_path: Path) -> Non expected_run_version=resumed.version, ) + +def test_interrupt_deadline_expires_run_atomically(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + queued = store.create_run(_run()) + running = store.transition_run( + queued.id, + expected_version=queued.version, + target=RunStatus.RUNNING, + ) + interrupt = Interrupt.create( + run_id=running.id, + kind=InterruptKind.INPUT, + request={"question": "continue?"}, + timeout_seconds=1, + ) + store.create_interrupt(interrupt, expected_run_version=running.version) + assert interrupt.expires_at is not None + + assert store.expire_interrupts( + now=interrupt.expires_at + timedelta(seconds=1) + ) == (interrupt.id,) + expired = store.get_interrupt(interrupt.id) + assert expired.decision is not None + assert expired.decision.kind is ApprovalDecisionKind.REJECT + assert store.get_run(running.id).status is RunStatus.TIMED_OUT + assert store.expire_interrupts( + now=interrupt.expires_at + timedelta(seconds=2) + ) == () diff --git a/tests/server/test_agent_server.py b/tests/server/test_agent_server.py index e4c2d64..8479da6 100644 --- a/tests/server/test_agent_server.py +++ b/tests/server/test_agent_server.py @@ -5,10 +5,16 @@ import httpx +from rath.adapters import MemoryExecutor from rath.definition import EffectClass, step from rath.flow import Workflow -from rath.runtime import LocalRuntime, SQLiteRunStore -from rath.security import Principal, PrincipalKind, SecurityContext +from rath.runtime import InterruptKind, LocalRuntime, RunStatus, SQLiteRunStore +from rath.security import ( + LocalTrustedPolicy, + Principal, + PrincipalKind, + SecurityContext, +) from rath.server import AgentServer, StaticTokenAuth from rath.session import Session @@ -22,6 +28,19 @@ def forward(self, session: Session) -> Session: return session +class _Approval(Workflow): + @step(entry=True, effects=EffectClass.NON_IDEMPOTENT) + def approve(self, state, context): # type: ignore[no-untyped-def] + decision = context.interrupt( + InterruptKind.APPROVAL, + {"tool": "email.send"}, + ) + return {"decision": decision.kind.value} + + def forward(self, session: Session) -> Session: + return session + + def test_server_auth_tenant_idempotency_and_sse_sync(tmp_path: Path) -> None: import asyncio @@ -56,6 +75,9 @@ async def exercise() -> None: first = await client.post("/v1/runs", headers=headers, json=body) second = await client.post("/v1/runs", headers=headers, json=body) assert first.status_code == 201 + assert first.headers["x-request-id"] + assert first.json()["request_id"] + assert first.json()["trace_id"] assert second.json()["id"] == first.json()["id"] runtime.work_once(worker_id="worker") run_id = first.json()["id"] @@ -68,5 +90,165 @@ async def exercise() -> None: assert stream.status_code == 200 assert "run.checkpoint.created" in stream.text assert (await client.get("/health/ready")).status_code == 200 + assert (await client.get("/openapi.json")).status_code == 200 + assert (await client.get("/metrics")).status_code == 200 + + session = await client.post("/v1/sessions", headers=headers) + assert session.status_code == 201 + session_id = session.json()["id"] + session_run = await client.post( + f"/v1/sessions/{session_id}/runs", + headers={"Authorization": "Bearer token"}, + json={"assistant_id": "echo", "state": {"value": 2}}, + ) + assert session_run.status_code == 201 + listed = await client.get("/v1/runs?limit=1", headers=headers) + assert len(listed.json()["items"]) == 1 + assert listed.json()["next"] is not None + assistant = await client.get("/v1/assistants/echo", headers=headers) + assert assistant.status_code == 200 + alias = await client.post( + "/v1/assistants", + headers=headers, + json={"id": "tenant-echo", "template_id": "echo"}, + ) + assert alias.status_code == 201 + assert alias.json()["kind"] == "alias" + alias_run = await client.post( + "/v1/runs", + headers={"Authorization": "Bearer token"}, + json={ + "assistant_id": "tenant-echo", + "session_id": str(uuid4()), + "state": {"value": 3}, + }, + ) + assert alias_run.status_code == 201 + listed_assistants = await client.get( + "/v1/assistants", headers=headers + ) + assert {item["id"] for item in listed_assistants.json()["items"]} == { + "echo", + "tenant-echo", + } + reconnected = await client.get( + f"/v1/runs/{run_id}/stream", + headers={**headers, "Last-Event-ID": "1"}, + ) + assert "id: 1\n" not in reconnected.text + feedback = await client.post( + "/v1/feedback", + headers=headers, + json={"run_id": run_id, "key": "quality", "score": 1}, + ) + assert feedback.status_code == 201 + assert feedback.headers["x-content-type-options"] == "nosniff" + oversized = await client.post( + "/v1/runs", + headers={"Authorization": "Bearer token"}, + content=b"x" * (1024 * 1024 + 1), + ) + assert oversized.status_code == 400 + + asyncio.run(exercise()) + + +def test_store_api_is_policy_governed_and_tenant_scoped(tmp_path: Path) -> None: + import asyncio + + async def exercise() -> None: + store = SQLiteRunStore(tmp_path / "store-api.db") + runtime = LocalRuntime(store) + local = SecurityContext.local() + calls: list[tuple[str, str, dict[str, object]]] = [] + + def memory_handler(operation, namespace, payload, context): # type: ignore[no-untyped-def] + calls.append((operation, namespace.tenant_id, dict(payload))) + return {"operation": operation, "tenant_id": namespace.tenant_id} + + server = AgentServer( + store, + runtime, + auth=StaticTokenAuth({"token": local}), + memory_executor=MemoryExecutor(LocalTrustedPolicy()), + memory_handler=memory_handler, + ) + transport = httpx.ASGITransport(app=server.app) + async with httpx.AsyncClient( + transport=transport, + base_url="http://test", + ) as client: + headers = {"Authorization": "Bearer token"} + response = await client.post( + "/v1/store/search", + headers=headers, + json={"payload": {"query": "safe"}}, + ) + assert response.status_code == 200 + assert response.json()["result"]["tenant_id"] == "local" + denied = await client.post( + "/v1/store/search", + headers=headers, + json={"tenant_id": "other", "payload": {"query": "unsafe"}}, + ) + assert denied.status_code == 403 + assert calls == [("search", "local", {"query": "safe"})] + + asyncio.run(exercise()) + + +def test_server_interrupt_inbox_and_decision_resume(tmp_path: Path) -> None: + import asyncio + + async def exercise() -> None: + store = SQLiteRunStore(tmp_path / "interrupt-api.db") + runtime = LocalRuntime(store) + tenant = SecurityContext( + principal=Principal(id="reviewer", kind=PrincipalKind.USER), + tenant_id="tenant", + ) + server = AgentServer( + store, + runtime, + auth=StaticTokenAuth({"token": tenant}), + ) + server.register_assistant( + "approval", + _Approval(), + revision_id=uuid4(), + ) + transport = httpx.ASGITransport(app=server.app) + async with httpx.AsyncClient( + transport=transport, + base_url="http://test", + ) as client: + headers = {"Authorization": "Bearer token"} + created = await client.post( + "/v1/runs", + headers=headers, + json={ + "assistant_id": "approval", + "session_id": str(uuid4()), + }, + ) + assert created.status_code == 201 + waiting = runtime.work_once(worker_id="worker-1") + assert waiting is not None and waiting.status is RunStatus.WAITING + inbox = await client.get("/v1/interrupts", headers=headers) + assert inbox.status_code == 200 + (interrupt,) = inbox.json()["items"] + decided = await client.post( + f"/v1/interrupts/{interrupt['id']}/decision", + headers=headers, + json={ + "kind": "approve", + "reason": "expected test operation", + }, + ) + assert decided.status_code == 200 + completed = runtime.work_once(worker_id="worker-2") + assert completed is not None + assert completed.status is RunStatus.SUCCEEDED + assert completed.state["decision"] == "approve" asyncio.run(exercise()) diff --git a/uv.lock b/uv.lock index e117418..a6393ba 100644 --- a/uv.lock +++ b/uv.lock @@ -1566,7 +1566,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.27.1" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1584,9 +1584,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] @@ -1926,7 +1926,7 @@ requires-dist = [ { name = "boto3", marker = "extra == 's3'", specifier = ">=1.40,<2" }, { name = "httpx", marker = "extra == 'server'", specifier = ">=0.28,<1" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.80,<1.88" }, - { name = "mcp", specifier = ">=1.0.0" }, + { name = "mcp", specifier = ">=1.28.1,<2" }, { name = "openai", specifier = ">=1.0.0" }, { name = "opensandbox", marker = "extra == 'opensandbox'", specifier = ">=0.1.13" }, { name = "opensandbox-code-interpreter", marker = "extra == 'opensandbox'", specifier = ">=0.1.2" }, From a98c6d392f788273731f998ec904c27066abc519 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 19:33:16 +0800 Subject: [PATCH 11/22] docs(v2): add production review evidence --- review/v2.0.0/README.md | 88 + review/v2.0.0/benchmark.json | 17 + review/v2.0.0/evidence.json | 54 + review/v2.0.0/release-approval.md | 32 + review/v2.0.0/sbom.cdx.json | 9402 +++++++++++++++++++++++ review/v2.0.0/soak.json | 19 + review/v2.0.0/vulnerability-report.json | 8360 ++++++++++++++++++++ 7 files changed, 17972 insertions(+) create mode 100644 review/v2.0.0/README.md create mode 100644 review/v2.0.0/benchmark.json create mode 100644 review/v2.0.0/evidence.json create mode 100644 review/v2.0.0/release-approval.md create mode 100644 review/v2.0.0/sbom.cdx.json create mode 100644 review/v2.0.0/soak.json create mode 100644 review/v2.0.0/vulnerability-report.json diff --git a/review/v2.0.0/README.md b/review/v2.0.0/README.md new file mode 100644 index 0000000..abfbbfb --- /dev/null +++ b/review/v2.0.0/README.md @@ -0,0 +1,88 @@ +# OpenRath v2.0.0 review candidate + +Status: **implementation complete; release on hold for user review** + +This directory is the local evidence package for the v2.0.0 implementation. +It is not a release. The package metadata intentionally remains `1.3.0`; no +v2 tag, registry push, GitHub release, or production deployment is permitted +until the owner approves [release-approval.md](release-approval.md). + +## Implemented production surface + +- Immutable events, explicit `@step`/`@router` compilation, canonical plans, + revision identity, and compatibility reporting. +- Durable Run state machine, transactional events/checkpoints, CAS updates, + retries, deadlines, cancellation, priority, per-session serialization, and + queue backpressure. +- SQLite local storage and PostgreSQL production storage with pooled + connections, `SKIP LOCKED` claims, leases, heartbeats, fencing, and orphan + recovery. +- Durable human-in-the-loop interrupts with inbox, approve/edit/reject/respond, + actor/reason audit data, timeout expiry, and same-step resume. +- Effect ledger for read-only, idempotent, and non-idempotent operations; + ambiguous outcomes enter review instead of unsafe replay. +- Tenant-scoped local/S3 artifacts and governed Provider, Tool, Sandbox, and + Memory boundaries with explicit context, policy, timeout, trust, and + credential references. +- Agent Server resources for Assistants, Sessions, Runs, Events/SSE, + Interrupts, Store, and Feedback; sync/async remote clients; authentication, + tenant isolation, correlation IDs, pagination, body limits, and security + headers. +- OpenTelemetry trace/metric bridge, redacted structured JSON logs, durable + feedback, datasets, experiments, and regression gates. +- Immutable deployment revisions, migration CLI, v1 history importer, + non-root/read-only OCI image, split API/worker Compose and Kubernetes + references, runbooks, capacity calculator, backup/restore procedure, CI, + SBOM, and vulnerability gate. + +## Evidence summary + +| Gate | Result | +|---|---| +| Ruff | passed | +| mypy | passed, 164 source files | +| Test suite | 1024 passed, 14 conditional skips, 1 third-party deprecation warning | +| Real backends | PostgreSQL 17, Redis 8, and MinIO lifecycle tests passed | +| Worker crash recovery | succeeded; 2 claims, 1 lease-expiry recovery event | +| Backup/restore | restored isolated database; 1 Run and 6 RunEvents verified | +| Container lifecycle | ready, Run succeeded, UID 10001, read-only root filesystem | +| Kubernetes | kubeconform strict: 10 valid, 0 invalid | +| Vulnerability scan | Trivy 0.67.2: 0 HIGH/CRITICAL after upgrading MCP to 1.28.1 | +| SBOM | CycloneDX 1.6 generated | +| Benchmark | 500 Runs; see [benchmark.json](benchmark.json) | +| Review soak | 1050 Runs/30 s, 0 failures, 0 thread delta; see [soak.json](soak.json) | +| Wheel smoke | isolated install, package migration resource, and server CLI passed | + +The local review image is +`sha256:92fa787d8b2be51b2248c13628765ad630f0820b757f74d46514ab94f332c7f6`. +It is intentionally not pushed. + +## Evidence files + +- [benchmark.json](benchmark.json): hardware-bound latency/throughput profile. +- [soak.json](soak.json): bounded resource-leak review profile. +- [sbom.cdx.json](sbom.cdx.json): CycloneDX software bill of materials. +- [vulnerability-report.json](vulnerability-report.json): machine-readable + HIGH/CRITICAL scan result. +- [release-approval.md](release-approval.md): explicit owner review and release + authorization gate. +- [API governance and maintenance policy](../../deploy/docs/api-governance-v2.md): + stability labels, SemVer, deprecation, v1 compatibility, and private + vulnerability reporting. + +## Scope boundaries and remaining operator acceptance + +The following are not unreported implementation gaps: + +- Webhooks and cron triggers remain P2 and are deferred; Runs can be submitted + through the stable API or an external scheduler. +- Exactly-once execution for arbitrary third-party side effects is not + promised; the supported contract is ledger + idempotency + `NEEDS_REVIEW`. +- Enterprise UI, SAML/SCIM, billing, and cross-region active-active are outside + v2.0.0 scope. + +Before production rollout, the operator must repeat the supplied soak tool for +the approved 8h/24h duration on target hardware, exercise the chosen live LLM, +OpenSandbox, and OpenViking credentials if those optional adapters are enabled, +and perform a target-cluster rollout/rollback drill. These environment-specific +checks do not authorize a release and must be attached to the approval record. diff --git a/review/v2.0.0/benchmark.json b/review/v2.0.0/benchmark.json new file mode 100644 index 0000000..86ce2b0 --- /dev/null +++ b/review/v2.0.0/benchmark.json @@ -0,0 +1,17 @@ +{ + "schema": "openrath.v2.benchmark/1", + "profile": "sqlite-single-worker-one-step", + "runs": 500, + "throughput_runs_per_second": 38.318744250606294, + "latency_ms": { + "mean": 26.053890199998932, + "p50": 24.10760000020673, + "p95": 27.95830000013666, + "p99": 103.07360000024346 + }, + "environment": { + "python": "3.10.18", + "platform": "Windows-10-10.0.26200-SP0", + "processor": "Intel64 Family 6 Model 183 Stepping 1, GenuineIntel" + } +} \ No newline at end of file diff --git a/review/v2.0.0/evidence.json b/review/v2.0.0/evidence.json new file mode 100644 index 0000000..dc3abb5 --- /dev/null +++ b/review/v2.0.0/evidence.json @@ -0,0 +1,54 @@ +{ + "schema": "openrath.v2.review-evidence/1", + "release_state": "hold_for_owner_review", + "package_version": "1.3.0", + "review_target": "2.0.0", + "tests": { + "passed": 1024, + "skipped": 14, + "warnings": 1, + "real_backends": [ + "postgresql-17", + "redis-8", + "minio-s3-compatible" + ] + }, + "static_checks": { + "ruff": "passed", + "mypy_source_files": 164, + "uv_lock": "passed" + }, + "container": { + "image": "openrath:2.0.0-review", + "image_id": "sha256:92fa787d8b2be51b2248c13628765ad630f0820b757f74d46514ab94f332c7f6", + "uid": 10001, + "read_only_root": true, + "lifecycle": "passed" + }, + "security": { + "scanner": "trivy-0.67.2", + "high_critical": 0, + "sbom": "cyclonedx-1.6", + "mcp_minimum_version": "1.28.1" + }, + "chaos": { + "worker_kill_recovered": true, + "claim_events": 2, + "lease_expired_events": 1 + }, + "backup_restore": { + "restored_runs": 1, + "restored_run_events": 6 + }, + "kubernetes": { + "validator": "kubeconform-0.7.0-strict", + "valid_resources": 10, + "invalid_resources": 0 + }, + "publication": { + "pushed": false, + "tagged": false, + "released": false, + "deployed": false + } +} diff --git a/review/v2.0.0/release-approval.md b/review/v2.0.0/release-approval.md new file mode 100644 index 0000000..ea51f16 --- /dev/null +++ b/review/v2.0.0/release-approval.md @@ -0,0 +1,32 @@ +# OpenRath v2.0.0 release approval + +Current decision: **HOLD — owner review required** + +The implementation may be reviewed, amended, and committed locally. The +following actions remain prohibited until the repository owner gives explicit +approval in a later message: + +- changing package metadata to `2.0.0`; +- creating or pushing a `v2.0.0` tag; +- pushing the review branch or image; +- creating a GitHub release; +- deploying to any shared, staging, or production environment. + +## Owner review checklist + +- [ ] Review public SDK/API compatibility and the stable error model. +- [ ] Review SecurityContext, tenant, policy, trust, secret, Tool, Memory, and + Sandbox boundaries. +- [ ] Review durable Run/checkpoint/interrupt/effect recovery semantics. +- [ ] Review PostgreSQL migrations, v1 import, backup/restore, and rollback. +- [ ] Review Agent Server authentication, resource isolation, SSE, limits, and + split API/worker deployment. +- [ ] Review SBOM, zero-HIGH/CRITICAL scan, benchmark, soak, and chaos evidence. +- [ ] Approve the v1 maintenance window and v2 support/security policy. +- [ ] Attach target-environment extended soak, optional live-adapter, and + rollout/rollback evidence when applicable. +- [ ] Explicitly authorize version bump, tag, push, image publication, and + release. + +Approval must be explicit. Silence, code review completion, or a passing CI +run does not authorize publication. diff --git a/review/v2.0.0/sbom.cdx.json b/review/v2.0.0/sbom.cdx.json new file mode 100644 index 0000000..6d3d3e8 --- /dev/null +++ b/review/v2.0.0/sbom.cdx.json @@ -0,0 +1,9402 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:a6e9217d-ff2d-4aa5-ad9a-e9dae394259a", + "version": 1, + "metadata": { + "timestamp": "2026-07-27T11:32:21+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "0.67.2" + } + ] + }, + "component": { + "bom-ref": "a8a162b2-37e7-4097-9741-311f48a2e27a", + "type": "container", + "name": "openrath:2.0.0-review", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:82d0cec9fb0c30115a70fe1863820ee9af9f2b16e211e6e5233beb4aa7c9386a" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:92fa787d8b2be51b2248c13628765ad630f0820b757f74d46514ab94f332c7f6" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "openrath:2.0.0-review" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:Size", + "value": "238930944" + } + ] + } + }, + "components": [ + { + "bom-ref": "3f6f6949-37ac-4670-9c65-1265e18f14ff", + "type": "operating-system", + "name": "debian", + "version": "13.6", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Adduser Developers " + }, + "name": "adduser", + "version": "3.152", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "adduser@3.152" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "adduser" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.152" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "APT Development Team " + }, + "name": "apt", + "version": "3.0.3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "curl" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "apt@3.0.3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "apt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.0.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "13.8+deb13u6", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "verbatim" + } + } + ], + "purl": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@13.8+deb13u6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.8+deb13u6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "base-passwd", + "version": "3.6.7", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-passwd@3.6.7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-passwd" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.6.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "bash", + "version": "5.2.37-2+b9", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "name": "Latex2e" + } + }, + { + "license": { + "id": "BSD-4-Clause-UC" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "permissive" + } + } + ], + "purl": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "bash@5.2.37-2+b9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.2.37" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "bsdutils", + "version": "1:2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "bsdutils@1:2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Julien Cristau " + }, + "name": "ca-certificates", + "version": "20250419", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "MPL-2.0" + } + } + ], + "purl": "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ca-certificates@20250419" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ca-certificates" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "20250419" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Michael Stone " + }, + "name": "coreutils", + "version": "9.7-3", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "BSD-4-Clause-UC" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + } + ], + "purl": "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "coreutils@9.7-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "coreutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "9.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Andrej Shadura " + }, + "name": "dash", + "version": "0.5.12-12", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "dash@0.5.12-12" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "dash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.5.12" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debconf Developers " + }, + "name": "debconf", + "version": "1.5.91", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + } + ], + "purl": "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debconf@1.5.91" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debconf" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.91" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Release Team " + }, + "name": "debian-archive-keyring", + "version": "2025.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debian-archive-keyring@2025.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debian-archive-keyring" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2025.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ileana Dumitrescu " + }, + "name": "debianutils", + "version": "5.23.2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "SMAIL-GPL" + } + } + ], + "purl": "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debianutils@5.23.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debianutils" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.23.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "diffutils", + "version": "1:3.10-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "GPL-3.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH texinfo-exception" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-only" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + } + ], + "purl": "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "diffutils@1:3.10-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "diffutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.10" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Dpkg Developers " + }, + "name": "dpkg", + "version": "1.22.22", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "public-domain-s-s-d" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "dpkg@1.22.22" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "dpkg" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.22.22" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Andreas Metzler " + }, + "name": "findutils", + "version": "4.10.0-3", + "licenses": [ + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH Autoconf-data-exception" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Autoconf-data-exception" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH automake-exception" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-2.2-exception" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "findutils@4.10.0-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "findutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.10.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "gcc-14-base", + "version": "14.2.0-19", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.2-only" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gcc-14-base@14.2.0-19" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-14" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "14.2.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Anibal Monsalve Salazar " + }, + "name": "grep", + "version": "3.11-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "grep@3.11-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "grep" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.11" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Milan Kupcevic " + }, + "name": "gzip", + "version": "1.13-1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "GFDL-1.3--no-invariant" + } + }, + { + "license": { + "name": "FSF-manpages" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GFDL-3" + } + } + ], + "purl": "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gzip@1.13-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gzip" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Michael Meskes " + }, + "name": "hostname", + "version": "3.25", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "hostname@3.25" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "hostname" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.25" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "init-system-helpers", + "version": "1.69~deb13u1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "init-system-helpers@1.69~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "init-system-helpers" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.69~deb13u1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libacl1", + "version": "2.3.2-2+b1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libacl1@2.3.2-2+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "acl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.3.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "APT Development Team " + }, + "name": "libapt-pkg7.0", + "version": "3.0.3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "curl" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libapt-pkg7.0@3.0.3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "apt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.0.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libattr1", + "version": "1:2.5.2-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libattr1@1:2.5.2-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "attr" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Laurent Bigonville " + }, + "name": "libaudit-common", + "version": "1:4.0.2-2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libaudit-common@1:4.0.2-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "audit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.0.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Laurent Bigonville " + }, + "name": "libaudit1", + "version": "1:4.0.2-2+b2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libaudit1@1:4.0.2-2+b2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "audit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.0.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libblkid1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libblkid1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libbsd0", + "version": "0.12.2-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Regents" + } + }, + { + "license": { + "id": "BSD-2-Clause-NetBSD" + } + }, + { + "license": { + "name": "BSD-3-clause-author" + } + }, + { + "license": { + "name": "BSD-3-clause-John-Birrell" + } + }, + { + "license": { + "name": "BSD-5-clause-Peter-Wemm" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-2-clause-verbatim" + } + }, + { + "license": { + "name": "BSD-2-clause-author" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "ISC-Original" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libbsd0@0.12.2-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libbsd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.12.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Anibal Monsalve Salazar " + }, + "name": "libbz2-1.0", + "version": "1.0.8-6", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libbz2-1.0@1.0.8-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bzip2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.0.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "libc-bin", + "version": "2.41-12+deb13u3", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "name": "LGPL-2.1-or-later WITH link-exception" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH link-exception" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "Carnegie" + } + }, + { + "license": { + "name": "Inner-Net" + } + }, + { + "license": { + "name": "MIT-like-Lord" + } + }, + { + "license": { + "name": "BSD-like-Spencer" + } + }, + { + "license": { + "name": "PCRE" + } + }, + { + "license": { + "name": "BSD-3-clause-Carnegie" + } + }, + { + "license": { + "id": "Unicode-DFS-2016" + } + }, + { + "license": { + "id": "BSL-1.0" + } + }, + { + "license": { + "name": "SunPro" + } + }, + { + "license": { + "name": "CORE-MATH" + } + }, + { + "license": { + "name": "BSD-3-clause-Berkeley" + } + }, + { + "license": { + "name": "BSD-3-clause-WIDE" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Oracle" + } + }, + { + "license": { + "name": "DEC" + } + }, + { + "license": { + "name": "IBM" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "Univ-Coimbra" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc-bin@2.41-12+deb13u3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12+deb13u3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "libc6", + "version": "2.41-12+deb13u3", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "name": "LGPL-2.1-or-later WITH link-exception" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH link-exception" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "Carnegie" + } + }, + { + "license": { + "name": "Inner-Net" + } + }, + { + "license": { + "name": "MIT-like-Lord" + } + }, + { + "license": { + "name": "BSD-like-Spencer" + } + }, + { + "license": { + "name": "PCRE" + } + }, + { + "license": { + "name": "BSD-3-clause-Carnegie" + } + }, + { + "license": { + "id": "Unicode-DFS-2016" + } + }, + { + "license": { + "id": "BSL-1.0" + } + }, + { + "license": { + "name": "SunPro" + } + }, + { + "license": { + "name": "CORE-MATH" + } + }, + { + "license": { + "name": "BSD-3-clause-Berkeley" + } + }, + { + "license": { + "name": "BSD-3-clause-WIDE" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Oracle" + } + }, + { + "license": { + "name": "DEC" + } + }, + { + "license": { + "name": "IBM" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "Univ-Coimbra" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc6@2.41-12+deb13u3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12+deb13u3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Håvard F. Aasen " + }, + "name": "libcap-ng0", + "version": "0.8.5-4+b1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcap-ng0@0.8.5-4+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcap-ng" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.8.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Christian Kastner " + }, + "name": "libcap2", + "version": "1:2.75-10+deb13u1+b1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcap2@1:2.75-10+deb13u1+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcap2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "10+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.75" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "libcrypt1", + "version": "1:4.4.38-1", + "purl": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcrypt1@1:4.4.38-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libxcrypt" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.4.38" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian QA Group " + }, + "name": "libdb5.3t64", + "version": "5.3.28+dfsg2-9", + "licenses": [ + { + "license": { + "id": "Sleepycat" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MS-PL" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "name": "MIT-old" + } + }, + { + "license": { + "name": "TCL-like" + } + }, + { + "license": { + "name": "BSD-3-clause-fjord" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "Zlib" + } + } + ], + "purl": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libdb5.3t64@5.3.28+dfsg2-9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "db5.3" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.3.28+dfsg2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Install System Team " + }, + "name": "libdebconfclient0", + "version": "0.280", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libdebconfclient0@0.280" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "cdebconf" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.280" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libffi8", + "version": "3.4.8-2", + "licenses": [ + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "MPL-1.1" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libffi8@3.4.8-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libffi" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.4.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libgcc-s1", + "version": "14.2.0-19", + "purl": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgcc-s1@14.2.0-19" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-14" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "14.2.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Nicolas Mora " + }, + "name": "libgdbm6t64", + "version": "1.24-2", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgdbm6t64@1.24-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gdbm" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.24" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "type": "library", + "supplier": { + "name": "Debian Science Maintainers " + }, + "name": "libgmp10", + "version": "2:6.3.0+dfsg-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgmp10@2:6.3.0+dfsg-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gmp" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.3.0+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Magnus Holmgren " + }, + "name": "libhogweed6t64", + "version": "3.10.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "GPL-3.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "GAP" + } + } + ], + "purl": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libhogweed6t64@3.10.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "nettle" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.10.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "liblastlog2-2", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblastlog2-2@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Nobuhiro Iwamatsu " + }, + "name": "liblz4-1", + "version": "1.10.0-4", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblz4-1@1.10.0-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "lz4" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.10.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sebastian Andrzej Siewior " + }, + "name": "liblzma5", + "version": "5.8.1-1+deb13u1", + "licenses": [ + { + "license": { + "id": "0BSD" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Autoconf-exception-macro" + } + }, + { + "license": { + "name": "none" + } + }, + { + "license": { + "name": "PD" + } + }, + { + "license": { + "name": "permissive-nowarranty" + } + }, + { + "license": { + "name": "FSFUL" + } + }, + { + "license": { + "name": "noderivs" + } + }, + { + "license": { + "name": "PD-debian" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblzma5@5.8.1-1+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "xz-utils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libmd0", + "version": "1.1.0-2+b1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Aaron-D-Gifford" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-2-Clause-NetBSD" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "Beerware" + } + }, + { + "license": { + "name": "public-domain-md4" + } + }, + { + "license": { + "name": "public-domain-md5" + } + }, + { + "license": { + "name": "public-domain-sha1" + } + } + ], + "purl": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libmd0@1.1.0-2+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libmd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.1.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libmount1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libmount1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "libncursesw6", + "version": "6.5+20250216-2", + "purl": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libncursesw6@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Magnus Holmgren " + }, + "name": "libnettle8t64", + "version": "3.10.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "GPL-3.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "GAP" + } + } + ], + "purl": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libnettle8t64@3.10.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "nettle" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.10.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam-modules-bin", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-modules-bin@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam-modules", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-modules@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam-runtime", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-runtime@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam0g", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam0g@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthew Vernon " + }, + "name": "libpcre2-8-0", + "version": "10.46-1~deb13u1", + "licenses": [ + { + "license": { + "name": "BSD-3-clause-Cambridge WITH BINARY-LIBRARY-LIKE-PACKAGES-exception" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpcre2-8-0@10.46-1~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pcre2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "10.46" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "libreadline8t64", + "version": "8.2-6", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + }, + { + "license": { + "name": "ISC-no-attribution" + } + } + ], + "purl": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libreadline8t64@8.2-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "readline" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Kees Cook " + }, + "name": "libseccomp2", + "version": "2.6.0-2", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libseccomp2@2.6.0-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libseccomp" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.6.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libselinux1", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libselinux1@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libselinux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsemanage-common", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsemanage-common@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsemanage" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsemanage2", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsemanage2@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsemanage" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsepol2", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsepol2@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsepol" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libsmartcols1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsmartcols1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Laszlo Boszormenyi (GCS) " + }, + "name": "libsqlite3-0", + "version": "3.46.1-7+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsqlite3-0@3.46.1-7+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sqlite3" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "7+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.46.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "libssl3t64", + "version": "3.5.6-1~deb13u2", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libssl3t64@3.5.6-1~deb13u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libstdc++6", + "version": "14.2.0-19", + "purl": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libstdc++6@14.2.0-19" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-14" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "14.2.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "libsystemd0", + "version": "257.13-1~deb13u1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH Linux-syscall-note-exception" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsystemd0@257.13-1~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "systemd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "257.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "libtinfo6", + "version": "6.5+20250216-2", + "licenses": [ + { + "license": { + "name": "MIT-X11" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libtinfo6@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "libudev1", + "version": "257.13-1~deb13u1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH Linux-syscall-note-exception" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libudev1@257.13-1~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "systemd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "257.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libuuid1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libuuid1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Josue Ortega " + }, + "name": "libxxhash0", + "version": "0.8.3-2", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libxxhash0@0.8.3-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "xxhash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.8.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "RPM packaging team " + }, + "name": "libzstd1", + "version": "1.5.7+dfsg-1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libzstd1@1.5.7+dfsg-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libzstd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.7+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "login.defs", + "version": "1:4.17.4-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "login.defs@1:4.17.4-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "shadow" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.17.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "login", + "version": "1:4.16.0-2+really2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "login@1:4.16.0-2+really2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Boyuan Yang " + }, + "name": "mawk", + "version": "1.3.4.20250131-1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "CC-BY-3.0" + } + } + ], + "purl": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "mawk@1.3.4.20250131-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "mawk" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.4.20250131" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "mount", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "mount@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "ncurses-base", + "version": "6.5+20250216-2", + "licenses": [ + { + "license": { + "name": "MIT-X11" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ncurses-base@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "ncurses-bin", + "version": "6.5+20250216-2", + "licenses": [ + { + "license": { + "name": "MIT-X11" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ncurses-bin@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "openssl-provider-legacy", + "version": "3.5.6-1~deb13u2", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "openssl-provider-legacy@3.5.6-1~deb13u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "openssl", + "version": "3.5.6-1~deb13u2", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "openssl@3.5.6-1~deb13u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "passwd", + "version": "1:4.17.4-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "passwd@1:4.17.4-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "shadow" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.17.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Niko Tyni " + }, + "name": "perl-base", + "version": "5.40.1-6", + "licenses": [ + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "REGCOMP" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH bison-exception+" + } + }, + { + "license": { + "name": "Unicode" + } + }, + { + "license": { + "name": "BZIP" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "BSD-3-Clause WITH weird-numbering" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "TEXT-TABS" + } + }, + { + "license": { + "name": "BSD-4-clause-POWERDOG" + } + }, + { + "license": { + "name": "BSD-3-clause-GENERIC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "SDBM-PUBLIC-DOMAIN" + } + }, + { + "license": { + "name": "DONT-CHANGE-THE-GPL" + } + }, + { + "license": { + "name": "Artistic-dist" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "Artistic-2" + } + } + ], + "purl": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "perl-base@5.40.1-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "perl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.40.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "readline-common", + "version": "8.2-6", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + }, + { + "license": { + "name": "ISC-no-attribution" + } + } + ], + "purl": "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "readline-common@8.2-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "readline" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Clint Adams " + }, + "name": "sed", + "version": "4.9-2+deb13u1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-4-Clause-UC" + } + }, + { + "license": { + "name": "BSL-1" + } + }, + { + "license": { + "name": "pcre" + } + } + ], + "purl": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sed@4.9-2+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sed" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.9" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Rust Maintainers " + }, + "name": "sqv", + "version": "1.3.0-3+b2", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sqv@1.3.0-3+b2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "rust-sequoia-sqv" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian sysvinit maintainers " + }, + "name": "sysvinit-utils", + "version": "3.14-4", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sysvinit-utils@3.14-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sysvinit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.14" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Janos Lenart " + }, + "name": "tar", + "version": "1.35+dfsg-3.1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tar@1.35+dfsg-3.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tar" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.35+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "util-linux", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "util-linux@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Mark Brown " + }, + "name": "zlib1g", + "version": "1:1.3.dfsg+really1.3.1-1+b1", + "licenses": [ + { + "license": { + "id": "Zlib" + } + } + ], + "purl": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "zlib1g@1:1.3.dfsg+really1.3.1-1+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "zlib" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.dfsg+really1.3.1" + } + ] + }, + { + "bom-ref": "pkg:pypi/annotated-types@0.7.0", + "type": "library", + "name": "annotated-types", + "version": "0.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "b11011181822ac765c9f66c8aa42c26952de6a96" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/annotated-types@0.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/anthropic@0.104.1", + "type": "library", + "name": "anthropic", + "version": "0.104.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "6a0fce5932599b482bf25ed3db3c06e9211f1358" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/anthropic@0.104.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/anthropic-0.104.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/anyio@4.13.0", + "type": "library", + "name": "anyio", + "version": "4.13.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "5f30168435645daddf756ecef34992631c6e778b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/anyio@4.13.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/anyio-4.13.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/attrs@26.1.0", + "type": "library", + "name": "attrs", + "version": "26.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "89068272cc1dc340d8fd910a62be241c42414339" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/attrs@26.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/attrs-26.1.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/boto3@1.43.56", + "type": "library", + "name": "boto3", + "version": "1.43.56", + "hashes": [ + { + "alg": "SHA-1", + "content": "f5c7842f2414d0cd2cefdb918d07c52018450ff7" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/boto3@1.43.56", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/boto3-1.43.56.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/botocore@1.43.56", + "type": "library", + "name": "botocore", + "version": "1.43.56", + "hashes": [ + { + "alg": "SHA-1", + "content": "b8760d1db82eba72c06ec96252b62d541a21b09e" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/botocore@1.43.56", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/botocore-1.43.56.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/certifi@2026.5.20", + "type": "library", + "name": "certifi", + "version": "2026.5.20", + "hashes": [ + { + "alg": "SHA-1", + "content": "cb42a7b0ba6491d51e71ff594a39bfaf5b8f9d22" + } + ], + "licenses": [ + { + "license": { + "id": "MPL-2.0" + } + } + ], + "purl": "pkg:pypi/certifi@2026.5.20", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/certifi-2026.5.20.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/cffi@2.0.0", + "type": "library", + "name": "cffi", + "version": "2.0.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "87e9c9d276c4f4c31f5a314d6a5472f45655674c" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/cffi@2.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/click@8.4.1", + "type": "library", + "name": "click", + "version": "8.4.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "c486957c59cc28072021bdcfe6d681e9ddafc860" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/click@8.4.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/click-8.4.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/cryptography@49.0.0", + "type": "library", + "name": "cryptography", + "version": "49.0.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "739372eb2cc71602103a206e7a42a926930cecb6" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:pypi/cryptography@49.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/cryptography-49.0.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/distro@1.9.0", + "type": "library", + "name": "distro", + "version": "1.9.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "ce14620cf14e15a64d2ff574796543f99619e7f3" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/distro@1.9.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/distro-1.9.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/docstring-parser@0.18.0", + "type": "library", + "name": "docstring_parser", + "version": "0.18.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "cd475f73b404c399cf87b9fb990fc14669479d24" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/docstring-parser@0.18.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/docstring_parser-0.18.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/h11@0.16.0", + "type": "library", + "name": "h11", + "version": "0.16.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "5d41eddffefef5f6e8ff383a2537e81a38a37807" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/h11@0.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/httpcore@1.0.9", + "type": "library", + "name": "httpcore", + "version": "1.0.9", + "hashes": [ + { + "alg": "SHA-1", + "content": "2981d359ae33f31d339189a9680db85785339a56" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/httpcore@1.0.9", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/httpx-sse@0.4.3", + "type": "library", + "name": "httpx-sse", + "version": "0.4.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "df05446be58f0a3a306e50c7ceebef24bb383a6d" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/httpx-sse@0.4.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/httpx_sse-0.4.3.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/httpx@0.28.1", + "type": "library", + "name": "httpx", + "version": "0.28.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "537da7e4f29438278e124e10e02d1a500fe33bcc" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/httpx@0.28.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/idna@3.16", + "type": "library", + "name": "idna", + "version": "3.16", + "hashes": [ + { + "alg": "SHA-1", + "content": "191a7bf1dac83b0cc997024ebb4b8da5c962ae5b" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/idna@3.16", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/idna-3.16.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jiter@0.15.0", + "type": "library", + "name": "jiter", + "version": "0.15.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9de68ecc913d85eafa70a1252b2f5b1b2d2829f5" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jiter@0.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jiter-0.15.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jmespath@1.1.0", + "type": "library", + "name": "jmespath", + "version": "1.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "d3f297922cf04b0cc127a18994ad6e6b947f0cc7" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jmespath@1.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jmespath-1.1.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jsonschema-specifications@2025.9.1", + "type": "library", + "name": "jsonschema-specifications", + "version": "2025.9.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "ac33f477be9d3336ae67bc454f68a9ff39c91cf3" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jsonschema-specifications@2025.9.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jsonschema@4.26.0", + "type": "library", + "name": "jsonschema", + "version": "4.26.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "94b3d1a46cf55d74e42c401eaf4a4b71c76cee31" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jsonschema@4.26.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jsonschema-4.26.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/mcp@1.28.1", + "type": "library", + "name": "mcp", + "version": "1.28.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "22fedbbf2f1d94917eba0c0c156781325bd3d786" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/mcp@1.28.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/mcp-1.28.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/openai@2.38.0", + "type": "library", + "name": "openai", + "version": "2.38.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "ea17f685d13b1a896fb055b3bedbfcb5ed8da63a" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/openai@2.38.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/openai-2.38.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/openrath@1.3.0", + "type": "library", + "name": "openrath", + "version": "1.3.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "d04d62d29a60270300a6eefa18f542d44f8d14fc" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/openrath@1.3.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/openrath-1.3.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/opentelemetry-api@1.42.1", + "type": "library", + "name": "opentelemetry-api", + "version": "1.42.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "a54ebbf560cfb13acfd3d7f7a5c385f03e36be08" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/opentelemetry-api@1.42.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/opentelemetry_api-1.42.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/opentelemetry-sdk@1.42.1", + "type": "library", + "name": "opentelemetry-sdk", + "version": "1.42.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "cdfb9c90d3e4765c62ffce81a835445a8efef7c4" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/opentelemetry-sdk@1.42.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/opentelemetry_sdk-1.42.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "type": "library", + "name": "opentelemetry-semantic-conventions", + "version": "0.63b1", + "hashes": [ + { + "alg": "SHA-1", + "content": "5c7aaa298ae1ba489d7214ebec1427bd3f602556" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/opentelemetry_semantic_conventions-0.63b1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pip@25.0.1", + "type": "library", + "name": "pip", + "version": "25.0.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pip@25.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "usr/local/lib/python3.12/site-packages/pip-25.0.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg-binary@3.3.4", + "type": "library", + "name": "psycopg-binary", + "version": "3.3.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "e8c69d729c969a8a81834cce24f8947ad560813d" + } + ], + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:pypi/psycopg-binary@3.3.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/psycopg_binary-3.3.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg-pool@3.3.1", + "type": "library", + "name": "psycopg-pool", + "version": "3.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "60d07aa411067306f5244d0481227a78a5b11135" + } + ], + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:pypi/psycopg-pool@3.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/psycopg_pool-3.3.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg@3.3.4", + "type": "library", + "name": "psycopg", + "version": "3.3.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "dc38178dd59b090117f63d60bc627e6a168205c8" + } + ], + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:pypi/psycopg@3.3.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/psycopg-3.3.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "type": "library", + "name": "psycopg_binary", + "version": "3.3.4", + "purl": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "psycopg_binary@3.3.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "type": "library", + "name": "psycopg_binary", + "version": "3.3.4", + "purl": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "psycopg_binary@3.3.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pycparser@3.0", + "type": "library", + "name": "pycparser", + "version": "3.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "ec46323dcd4dd2f7742b74b09cc0b030e330e46f" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/pycparser@3.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pydantic-core@2.46.4", + "type": "library", + "name": "pydantic_core", + "version": "2.46.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "f44318e9ae79f745f1f5a7a59b43044ab6fff485" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pydantic-core@2.46.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pydantic_core-2.46.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pydantic-settings@2.14.1", + "type": "library", + "name": "pydantic-settings", + "version": "2.14.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "b9246a1f2d8974cb71bb7ec65fb8a9f45710d89a" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pydantic-settings@2.14.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pydantic_settings-2.14.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pydantic@2.13.4", + "type": "library", + "name": "pydantic", + "version": "2.13.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "291e482df82749c7e52c21e8275551f04de3034b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pydantic@2.13.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pydantic-2.13.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pyjwt@2.13.0", + "type": "library", + "name": "PyJWT", + "version": "2.13.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "78125d2bb60e70bc168fd8787075ceb6a69d4f65" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pyjwt@2.13.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/python-dateutil@2.9.0.post0", + "type": "library", + "name": "python-dateutil", + "version": "2.9.0.post0", + "hashes": [ + { + "alg": "SHA-1", + "content": "7a3c35abd86cd96034d5afb0d4b241dc9e13e6f8" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/python-dateutil@2.9.0.post0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/python-dotenv@1.2.2", + "type": "library", + "name": "python-dotenv", + "version": "1.2.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "a70b92340410dfaf8ce628e658f176278fa2e557" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/python-dotenv@1.2.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/python-multipart@0.0.32", + "type": "library", + "name": "python-multipart", + "version": "0.0.32", + "hashes": [ + { + "alg": "SHA-1", + "content": "9f79572b3702bdac7487183b498470341276b17e" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/python-multipart@0.0.32", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/redis@6.4.0", + "type": "library", + "name": "redis", + "version": "6.4.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9a3de9ffc83addb0d845a4f16c6a415db91a3eda" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/redis@6.4.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/redis-6.4.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/referencing@0.37.0", + "type": "library", + "name": "referencing", + "version": "0.37.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "f6fa004340bef5d23995b09dab75ce12e3f367a7" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/referencing@0.37.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/referencing-0.37.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/rpds-py@0.30.0", + "type": "library", + "name": "rpds-py", + "version": "0.30.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9eef46c842a0ca6229680d7bfc1272958efdcc5a" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/rpds-py@0.30.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/rpds_py-0.30.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/s3transfer@0.19.2", + "type": "library", + "name": "s3transfer", + "version": "0.19.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "453d1af3240f56bb5d0bd093eb3d21a250cc5777" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/s3transfer@0.19.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/s3transfer-0.19.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/six@1.17.0", + "type": "library", + "name": "six", + "version": "1.17.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "483a26554261f6c839703c0e1183f3ef33ff97f1" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/six@1.17.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/six-1.17.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/sniffio@1.3.1", + "type": "library", + "name": "sniffio", + "version": "1.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "bc1d7aead770fe23c8d22666b84558edb3686da3" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/sniffio@1.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/sniffio-1.3.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/sse-starlette@3.4.4", + "type": "library", + "name": "sse-starlette", + "version": "3.4.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "58e4ad3946eafb572e0219f89bb599bee2b7cca2" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/sse-starlette@3.4.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/sse_starlette-3.4.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/starlette@1.3.1", + "type": "library", + "name": "starlette", + "version": "1.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "9e43f99dc64bcf4498d65999f9cb36a40fa5e94c" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/starlette@1.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/starlette-1.3.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/tqdm@4.67.3", + "type": "library", + "name": "tqdm", + "version": "4.67.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "0135af1981d2b0f1326020f991192d869693b873" + } + ], + "licenses": [ + { + "expression": "MPL-2.0 AND MIT" + } + ], + "purl": "pkg:pypi/tqdm@4.67.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/tqdm-4.67.3.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/typing-extensions@4.15.0", + "type": "library", + "name": "typing_extensions", + "version": "4.15.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "c5c2ce18351f8f2ae0f4a6f7c84c523f342010ee" + } + ], + "licenses": [ + { + "license": { + "name": "PSF-2.0" + } + } + ], + "purl": "pkg:pypi/typing-extensions@4.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/typing-inspection@0.4.2", + "type": "library", + "name": "typing-inspection", + "version": "0.4.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "455fdb9c8e246ba02c2a28655287401b62028b60" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/typing-inspection@0.4.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/urllib3@2.7.0", + "type": "library", + "name": "urllib3", + "version": "2.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "d20520d0598c114ced8d55ed14209a2a3bbee22c" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/urllib3@2.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/urllib3-2.7.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/uvicorn@0.47.0", + "type": "library", + "name": "uvicorn", + "version": "0.47.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "f55652a6d9d6137b9be4cfc40b4c3d30b63717f8" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/uvicorn@0.47.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/uvicorn-0.47.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2", + "type": "library", + "name": "cyrus-sasl-lib", + "version": "2.1.26-24.el7_9", + "purl": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "cyrus-sasl-lib@2.1.26-24.el7_9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "cyrus-sasl-lib" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "24.el7_9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.1.26" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9", + "type": "library", + "name": "keyutils-libs", + "version": "1.5.8-3.el7", + "purl": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "keyutils-libs@1.5.8-3.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "keyutils-libs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.8" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#5b1da461e2c57feebadb3f96f47736f4b17ad56a83895f95d60a166ced6472b0", + "type": "library", + "name": "krb5-libs", + "version": "1.15.1-55.el7_9", + "purl": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "krb5-libs@1.15.1-55.el7_9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "krb5-libs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "55.el7_9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.15.1" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce", + "type": "library", + "name": "libcom_err", + "version": "1.42.9-19.el7", + "purl": "pkg:rpm/centos/libcom_err@1.42.9-19.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcom_err@1.42.9-19.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcom_err" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.42.9" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed", + "type": "library", + "name": "libselinux", + "version": "2.5-15.el7", + "purl": "pkg:rpm/centos/libselinux@2.5-15.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libselinux@2.5-15.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libselinux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "15.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72", + "type": "library", + "name": "pcre", + "version": "8.32-17.el7", + "purl": "pkg:rpm/centos/pcre@8.32-17.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "pcre@8.32-17.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pcre" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "17.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.32" + } + ] + } + ], + "dependencies": [ + { + "ref": "3f6f6949-37ac-4670-9c65-1265e18f14ff", + "dependsOn": [ + "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2", + "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9", + "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#5b1da461e2c57feebadb3f96f47736f4b17ad56a83895f95d60a166ced6472b0", + "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce", + "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed", + "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72" + ] + }, + { + "ref": "a8a162b2-37e7-4097-9741-311f48a2e27a", + "dependsOn": [ + "3f6f6949-37ac-4670-9c65-1265e18f14ff", + "pkg:pypi/annotated-types@0.7.0", + "pkg:pypi/anthropic@0.104.1", + "pkg:pypi/anyio@4.13.0", + "pkg:pypi/attrs@26.1.0", + "pkg:pypi/boto3@1.43.56", + "pkg:pypi/botocore@1.43.56", + "pkg:pypi/certifi@2026.5.20", + "pkg:pypi/cffi@2.0.0", + "pkg:pypi/click@8.4.1", + "pkg:pypi/cryptography@49.0.0", + "pkg:pypi/distro@1.9.0", + "pkg:pypi/docstring-parser@0.18.0", + "pkg:pypi/h11@0.16.0", + "pkg:pypi/httpcore@1.0.9", + "pkg:pypi/httpx-sse@0.4.3", + "pkg:pypi/httpx@0.28.1", + "pkg:pypi/idna@3.16", + "pkg:pypi/jiter@0.15.0", + "pkg:pypi/jmespath@1.1.0", + "pkg:pypi/jsonschema-specifications@2025.9.1", + "pkg:pypi/jsonschema@4.26.0", + "pkg:pypi/mcp@1.28.1", + "pkg:pypi/openai@2.38.0", + "pkg:pypi/openrath@1.3.0", + "pkg:pypi/opentelemetry-api@1.42.1", + "pkg:pypi/opentelemetry-sdk@1.42.1", + "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "pkg:pypi/pip@25.0.1", + "pkg:pypi/psycopg-binary@3.3.4", + "pkg:pypi/psycopg-pool@3.3.1", + "pkg:pypi/psycopg@3.3.4", + "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "pkg:pypi/pycparser@3.0", + "pkg:pypi/pydantic-core@2.46.4", + "pkg:pypi/pydantic-settings@2.14.1", + "pkg:pypi/pydantic@2.13.4", + "pkg:pypi/pyjwt@2.13.0", + "pkg:pypi/python-dateutil@2.9.0.post0", + "pkg:pypi/python-dotenv@1.2.2", + "pkg:pypi/python-multipart@0.0.32", + "pkg:pypi/redis@6.4.0", + "pkg:pypi/referencing@0.37.0", + "pkg:pypi/rpds-py@0.30.0", + "pkg:pypi/s3transfer@0.19.2", + "pkg:pypi/six@1.17.0", + "pkg:pypi/sniffio@1.3.1", + "pkg:pypi/sse-starlette@3.4.4", + "pkg:pypi/starlette@1.3.1", + "pkg:pypi/tqdm@4.67.3", + "pkg:pypi/typing-extensions@4.15.0", + "pkg:pypi/typing-inspection@0.4.2", + "pkg:pypi/urllib3@2.7.0", + "pkg:pypi/uvicorn@0.47.0" + ] + }, + { + "ref": "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:pypi/annotated-types@0.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/anthropic@0.104.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/anyio@4.13.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/attrs@26.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/boto3@1.43.56", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/botocore@1.43.56", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/certifi@2026.5.20", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/cffi@2.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/click@8.4.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/cryptography@49.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/distro@1.9.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/docstring-parser@0.18.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/h11@0.16.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/httpcore@1.0.9", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/httpx-sse@0.4.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/httpx@0.28.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@3.16", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jiter@0.15.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jmespath@1.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jsonschema-specifications@2025.9.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jsonschema@4.26.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/mcp@1.28.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/openai@2.38.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/openrath@1.3.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/opentelemetry-api@1.42.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/opentelemetry-sdk@1.42.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pip@25.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg-binary@3.3.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg-pool@3.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg@3.3.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pycparser@3.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pydantic-core@2.46.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pydantic-settings@2.14.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pydantic@2.13.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pyjwt@2.13.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/python-dateutil@2.9.0.post0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/python-dotenv@1.2.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/python-multipart@0.0.32", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/redis@6.4.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/referencing@0.37.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/rpds-py@0.30.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/s3transfer@0.19.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/six@1.17.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/sse-starlette@3.4.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/starlette@1.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/tqdm@4.67.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/typing-extensions@4.15.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/typing-inspection@0.4.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@2.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/uvicorn@0.47.0", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#5b1da461e2c57feebadb3f96f47736f4b17ad56a83895f95d60a166ced6472b0", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/review/v2.0.0/soak.json b/review/v2.0.0/soak.json new file mode 100644 index 0000000..96b6af3 --- /dev/null +++ b/review/v2.0.0/soak.json @@ -0,0 +1,19 @@ +{ + "schema": "openrath.v2.soak/1", + "profile": "sqlite-single-worker-one-step", + "duration_seconds": 30.003353699999934, + "completed_runs": 1050, + "failed_runs": 0, + "throughput_runs_per_second": 34.996087787346326, + "resource_delta": { + "threads": 0, + "traced_memory_bytes": 312383, + "peak_traced_memory_bytes": 337934 + }, + "environment": { + "python": "3.10.18", + "platform": "Windows-10-10.0.26200-SP0", + "processor": "Intel64 Family 6 Model 183 Stepping 1, GenuineIntel" + }, + "scope": "Review profile only; repeat for the approved 8h/24h duration on target hardware before production rollout." +} \ No newline at end of file diff --git a/review/v2.0.0/vulnerability-report.json b/review/v2.0.0/vulnerability-report.json new file mode 100644 index 0000000..eb7756e --- /dev/null +++ b/review/v2.0.0/vulnerability-report.json @@ -0,0 +1,8360 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2026-07-27T11:32:22.346970079Z", + "ArtifactName": "openrath:2.0.0-review", + "ArtifactType": "container_image", + "Metadata": { + "Size": 238930944, + "OS": { + "Family": "debian", + "Name": "13.6" + }, + "ImageID": "sha256:92fa787d8b2be51b2248c13628765ad630f0820b757f74d46514ab94f332c7f6", + "DiffIDs": [ + "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f", + "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167", + "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd", + "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f", + "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8", + "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1", + "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5", + "sha256:82d0cec9fb0c30115a70fe1863820ee9af9f2b16e211e6e5233beb4aa7c9386a" + ], + "RepoTags": [ + "openrath:2.0.0-review" + ], + "ImageConfig": { + "architecture": "amd64", + "created": "2026-07-27T11:32:09.941712925Z", + "history": [ + { + "created": "2026-07-13T00:00:00Z", + "created_by": "# debian.sh --arch 'amd64' out/ 'trixie' '@1783900800'", + "comment": "debuerreotype 0.17" + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV LANG=C.UTF-8", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "RUN /bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates \t\tnetbase \t\ttzdata \t; \tapt-get dist-clean # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV PYTHON_VERSION=3.12.13", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV PYTHON_SHA256=c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:11:29Z", + "created_by": "RUN /bin/sh -c set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdpkg-dev \t\tgcc \t\tgnupg \t\tlibbluetooth-dev \t\tlibbz2-dev \t\tlibc6-dev \t\tlibdb-dev \t\tlibffi-dev \t\tlibgdbm-dev \t\tliblzma-dev \t\tlibncursesw5-dev \t\tlibreadline-dev \t\tlibsqlite3-dev \t\tlibssl-dev \t\tmake \t\ttk-dev \t\tuuid-dev \t\twget \t\txz-utils \t\tzlib1g-dev \t; \t\twget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"; \techo \"$PYTHON_SHA256 *python.tar.xz\" | sha256sum -c -; \twget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"; \tGNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME; \tgpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"; \tgpg --batch --verify python.tar.xz.asc python.tar.xz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\" python.tar.xz.asc; \tmkdir -p /usr/src/python; \ttar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \trm python.tar.xz; \t\tcd /usr/src/python; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--enable-loadable-sqlite-extensions \t\t--enable-optimizations \t\t--enable-option-checking=fatal \t\t--enable-shared \t\t$(test \"${gnuArch%%-*}\" != 'riscv64' \u0026\u0026 echo '--with-lto') \t\t--with-ensurepip \t; \tnproc=\"$(nproc)\"; \tEXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"; \tLDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"; \tLDFLAGS=\"${LDFLAGS:-} -Wl,--strip-all\"; \tarch=\"$(dpkg --print-architecture)\"; arch=\"${arch##*-}\"; \tcase \"$arch\" in \t\tamd64|arm64) \t\t\tEXTRA_CFLAGS=\"${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer\"; \t\t\t;; \t\ti386) \t\t\t;; \t\t*) \t\t\tEXTRA_CFLAGS=\"${EXTRA_CFLAGS:-} -fno-omit-frame-pointer\"; \t\t\t;; \tesac; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-}\" \t; \trm python; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-} -Wl,-rpath='\\$\\$ORIGIN/../lib'\" \t\tpython \t; \tmake install; \t\tcd /; \trm -rf /usr/src/python; \t\tfind /usr/local -depth \t\t\\( \t\t\t\\( -type d -a \\( -name test -o -name tests -o -name idle_test \\) \\) \t\t\t-o \\( -type f -a \\( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \\) \\) \t\t\\) -exec rm -rf '{}' + \t; \t\tldconfig; \t\tapt-mark auto '.*' \u003e /dev/null; \tapt-mark manual $savedAptMark; \tfind /usr/local -type f -executable -not \\( -name '*tkinter*' \\) -exec ldd '{}' ';' \t\t| awk '/=\u003e/ { so = $(NF-1); if (index(so, \"/usr/local/\") == 1) { next }; gsub(\"^/(usr/)?\", \"\", so); printf \"*%s\\n\", so }' \t\t| sort -u \t\t| xargs -rt dpkg-query --search \t\t| awk 'sub(\":$\", \"\", $1) { print $1 }' \t\t| sort -u \t\t| xargs -r apt-mark manual \t; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \tapt-get dist-clean; \t\texport PYTHONDONTWRITEBYTECODE=1; \tpython3 --version; \tpip3 --version # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-14T02:11:29Z", + "created_by": "RUN /bin/sh -c set -eux; \tfor src in idle3 pip3 pydoc3 python3 python3-config; do \t\tdst=\"$(echo \"$src\" | tr -d 3)\"; \t\t[ -s \"/usr/local/bin/$src\" ]; \t\t[ ! -e \"/usr/local/bin/$dst\" ]; \t\tln -svT \"$src\" \"/usr/local/bin/$dst\"; \tdone # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-14T02:11:29Z", + "created_by": "CMD [\"python3\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-27T10:45:44Z", + "created_by": "ENV PATH=/opt/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PYTHONPATH=/app PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 OPENRATH_HOST=0.0.0.0 OPENRATH_PORT=8000", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-27T10:45:44Z", + "created_by": "RUN /bin/sh -c groupadd --system --gid 10001 openrath \u0026\u0026 useradd --system --uid 10001 --gid openrath --home /app openrath # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-27T10:45:44Z", + "created_by": "WORKDIR /app", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-27T11:32:09Z", + "created_by": "COPY /opt/venv /opt/venv # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-27T11:32:09Z", + "created_by": "COPY examples ./examples # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-27T11:32:09Z", + "created_by": "USER 10001:10001", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-27T11:32:09Z", + "created_by": "EXPOSE map[8000/tcp:{}]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-27T11:32:09Z", + "created_by": "ENTRYPOINT [\"openrath-server\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f", + "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167", + "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd", + "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f", + "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8", + "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1", + "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5", + "sha256:82d0cec9fb0c30115a70fe1863820ee9af9f2b16e211e6e5233beb4aa7c9386a" + ] + }, + "config": { + "Entrypoint": [ + "openrath-server" + ], + "Env": [ + "PATH=/opt/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "LANG=C.UTF-8", + "GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305", + "PYTHON_VERSION=3.12.13", + "PYTHON_SHA256=c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684", + "PYTHONPATH=/app", + "PYTHONDONTWRITEBYTECODE=1", + "PYTHONUNBUFFERED=1", + "OPENRATH_HOST=0.0.0.0", + "OPENRATH_PORT=8000" + ], + "User": "10001:10001", + "WorkingDir": "/app", + "ExposedPorts": { + "8000/tcp": {} + } + } + }, + "Layers": [ + { + "Size": 81049600, + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "Size": 4127232, + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "Size": 38094848, + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "Size": 5120, + "DiffID": "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f" + }, + { + "Size": 11264, + "DiffID": "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8" + }, + { + "Size": 1536, + "DiffID": "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1" + }, + { + "Size": 115636224, + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + { + "Size": 5120, + "DiffID": "sha256:82d0cec9fb0c30115a70fe1863820ee9af9f2b16e211e6e5233beb4aa7c9386a" + } + ] + }, + "Results": [ + { + "Target": "openrath:2.0.0-review (debian 13.6)", + "Class": "os-pkgs", + "Type": "debian", + "Packages": [ + { + "ID": "adduser@3.152", + "Name": "adduser", + "Identifier": { + "PURL": "pkg:deb/debian/adduser@3.152?arch=all\u0026distro=debian-13.6", + "UID": "a26e3466c18314ad" + }, + "Version": "3.152", + "Arch": "all", + "SrcName": "adduser", + "SrcVersion": "3.152", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian Adduser Developers \u003cadduser@packages.debian.org\u003e", + "DependsOn": [ + "passwd@1:4.17.4-2" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/sbin/adduser", + "/usr/sbin/deluser", + "/usr/share/doc/adduser/NEWS.Debian.gz", + "/usr/share/doc/adduser/README.gz", + "/usr/share/doc/adduser/TODO", + "/usr/share/doc/adduser/changelog.gz", + "/usr/share/doc/adduser/copyright", + "/usr/share/doc/adduser/examples/INSTALL", + "/usr/share/doc/adduser/examples/README", + "/usr/share/doc/adduser/examples/adduser.conf", + "/usr/share/doc/adduser/examples/adduser.local", + "/usr/share/doc/adduser/examples/adduser.local.conf", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/bash.bashrc", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/profile", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel.other/index.html", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel/dot.bash_logout", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel/dot.bash_profile", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel/dot.bashrc", + "/usr/share/doc/adduser/examples/deluser.conf", + "/usr/share/man/da/man5/deluser.conf.5.gz", + "/usr/share/man/de/man5/adduser.conf.5.gz", + "/usr/share/man/de/man5/deluser.conf.5.gz", + "/usr/share/man/de/man8/adduser.8.gz", + "/usr/share/man/de/man8/adduser.local.8.gz", + "/usr/share/man/de/man8/deluser.8.gz", + "/usr/share/man/es/man5/deluser.conf.5.gz", + "/usr/share/man/fr/man5/adduser.conf.5.gz", + "/usr/share/man/fr/man5/deluser.conf.5.gz", + "/usr/share/man/fr/man8/adduser.8.gz", + "/usr/share/man/fr/man8/deluser.8.gz", + "/usr/share/man/it/man5/deluser.conf.5.gz", + "/usr/share/man/man5/adduser.conf.5.gz", + "/usr/share/man/man5/deluser.conf.5.gz", + "/usr/share/man/man8/adduser.8.gz", + "/usr/share/man/man8/adduser.local.8.gz", + "/usr/share/man/man8/deluser.8.gz", + "/usr/share/man/nl/man5/adduser.conf.5.gz", + "/usr/share/man/nl/man5/deluser.conf.5.gz", + "/usr/share/man/nl/man8/adduser.8.gz", + "/usr/share/man/nl/man8/adduser.local.8.gz", + "/usr/share/man/nl/man8/deluser.8.gz", + "/usr/share/man/pl/man5/deluser.conf.5.gz", + "/usr/share/man/pt/man5/adduser.conf.5.gz", + "/usr/share/man/pt/man5/deluser.conf.5.gz", + "/usr/share/man/pt/man8/adduser.8.gz", + "/usr/share/man/pt/man8/adduser.local.8.gz", + "/usr/share/man/pt/man8/deluser.8.gz", + "/usr/share/man/ro/man5/adduser.conf.5.gz", + "/usr/share/man/ro/man5/deluser.conf.5.gz", + "/usr/share/man/ro/man8/adduser.8.gz", + "/usr/share/man/ro/man8/adduser.local.8.gz", + "/usr/share/man/ro/man8/deluser.8.gz", + "/usr/share/man/ru/man5/deluser.conf.5.gz", + "/usr/share/man/sv/man5/deluser.conf.5.gz", + "/usr/share/perl5/Debian/AdduserCommon.pm", + "/usr/share/perl5/Debian/AdduserLogging.pm", + "/usr/share/perl5/Debian/AdduserRetvalues.pm" + ] + }, + { + "ID": "apt@3.0.3", + "Name": "apt", + "Identifier": { + "PURL": "pkg:deb/debian/apt@3.0.3?arch=amd64\u0026distro=debian-13.6", + "UID": "3c1822d549195c1f" + }, + "Version": "3.0.3", + "Arch": "amd64", + "SrcName": "apt", + "SrcVersion": "3.0.3", + "Licenses": [ + "GPL-2.0-or-later", + "curl", + "BSD-3-Clause", + "MIT", + "GPL-2.0-only" + ], + "Maintainer": "APT Development Team \u003cdeity@lists.debian.org\u003e", + "DependsOn": [ + "adduser@3.152", + "base-passwd@3.6.7", + "debian-archive-keyring@2025.1", + "libapt-pkg7.0@3.0.3", + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19", + "libseccomp2@2.6.0-2", + "libssl3t64@3.5.6-1~deb13u2", + "libstdc++6@14.2.0-19", + "libsystemd0@257.13-1~deb13u1", + "sqv@1.3.0-3+b2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/apt", + "/usr/bin/apt-cache", + "/usr/bin/apt-cdrom", + "/usr/bin/apt-config", + "/usr/bin/apt-get", + "/usr/bin/apt-mark", + "/usr/lib/apt/apt-extracttemplates", + "/usr/lib/apt/apt-helper", + "/usr/lib/apt/apt.systemd.daily", + "/usr/lib/apt/methods/cdrom", + "/usr/lib/apt/methods/copy", + "/usr/lib/apt/methods/file", + "/usr/lib/apt/methods/gpgv", + "/usr/lib/apt/methods/http", + "/usr/lib/apt/methods/mirror", + "/usr/lib/apt/methods/rred", + "/usr/lib/apt/methods/sqv", + "/usr/lib/apt/methods/store", + "/usr/lib/apt/solvers/dump", + "/usr/lib/dpkg/methods/apt/desc.apt", + "/usr/lib/dpkg/methods/apt/install", + "/usr/lib/dpkg/methods/apt/names", + "/usr/lib/dpkg/methods/apt/setup", + "/usr/lib/dpkg/methods/apt/update", + "/usr/lib/systemd/system/apt-daily-upgrade.service", + "/usr/lib/systemd/system/apt-daily-upgrade.timer", + "/usr/lib/systemd/system/apt-daily.service", + "/usr/lib/systemd/system/apt-daily.timer", + "/usr/lib/x86_64-linux-gnu/libapt-private.so.0.0.0", + "/usr/share/apt/default-sequoia.config", + "/usr/share/bash-completion/completions/apt", + "/usr/share/bug/apt/script", + "/usr/share/doc/apt/NEWS.Debian.gz", + "/usr/share/doc/apt/README.md.gz", + "/usr/share/doc/apt/changelog.gz", + "/usr/share/doc/apt/copyright", + "/usr/share/doc/apt/examples/apt.conf", + "/usr/share/doc/apt/examples/configure-index", + "/usr/share/doc/apt/examples/debian.sources", + "/usr/share/doc/apt/examples/preferences", + "/usr/share/lintian/overrides/apt", + "/usr/share/locale/ar/LC_MESSAGES/apt.mo", + "/usr/share/locale/ast/LC_MESSAGES/apt.mo", + "/usr/share/locale/bg/LC_MESSAGES/apt.mo", + "/usr/share/locale/bs/LC_MESSAGES/apt.mo", + "/usr/share/locale/ca/LC_MESSAGES/apt.mo", + "/usr/share/locale/cs/LC_MESSAGES/apt.mo", + "/usr/share/locale/cy/LC_MESSAGES/apt.mo", + "/usr/share/locale/da/LC_MESSAGES/apt.mo", + "/usr/share/locale/de/LC_MESSAGES/apt.mo", + "/usr/share/locale/dz/LC_MESSAGES/apt.mo", + "/usr/share/locale/el/LC_MESSAGES/apt.mo", + "/usr/share/locale/es/LC_MESSAGES/apt.mo", + "/usr/share/locale/eu/LC_MESSAGES/apt.mo", + "/usr/share/locale/fi/LC_MESSAGES/apt.mo", + "/usr/share/locale/fr/LC_MESSAGES/apt.mo", + "/usr/share/locale/gl/LC_MESSAGES/apt.mo", + "/usr/share/locale/hu/LC_MESSAGES/apt.mo", + "/usr/share/locale/it/LC_MESSAGES/apt.mo", + "/usr/share/locale/ja/LC_MESSAGES/apt.mo", + "/usr/share/locale/km/LC_MESSAGES/apt.mo", + "/usr/share/locale/ko/LC_MESSAGES/apt.mo", + "/usr/share/locale/ku/LC_MESSAGES/apt.mo", + "/usr/share/locale/lt/LC_MESSAGES/apt.mo", + "/usr/share/locale/mr/LC_MESSAGES/apt.mo", + "/usr/share/locale/nb/LC_MESSAGES/apt.mo", + "/usr/share/locale/ne/LC_MESSAGES/apt.mo", + "/usr/share/locale/nl/LC_MESSAGES/apt.mo", + "/usr/share/locale/nn/LC_MESSAGES/apt.mo", + "/usr/share/locale/pl/LC_MESSAGES/apt.mo", + "/usr/share/locale/pt/LC_MESSAGES/apt.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/apt.mo", + "/usr/share/locale/ro/LC_MESSAGES/apt.mo", + "/usr/share/locale/ru/LC_MESSAGES/apt.mo", + "/usr/share/locale/sk/LC_MESSAGES/apt.mo", + "/usr/share/locale/sl/LC_MESSAGES/apt.mo", + "/usr/share/locale/sv/LC_MESSAGES/apt.mo", + "/usr/share/locale/th/LC_MESSAGES/apt.mo", + "/usr/share/locale/tl/LC_MESSAGES/apt.mo", + "/usr/share/locale/tr/LC_MESSAGES/apt.mo", + "/usr/share/locale/uk/LC_MESSAGES/apt.mo", + "/usr/share/locale/vi/LC_MESSAGES/apt.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/apt.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/apt.mo", + "/usr/share/man/de/man1/apt-transport-http.1.gz", + "/usr/share/man/de/man1/apt-transport-https.1.gz", + "/usr/share/man/de/man1/apt-transport-mirror.1.gz", + "/usr/share/man/de/man5/apt.conf.5.gz", + "/usr/share/man/de/man5/apt_auth.conf.5.gz", + "/usr/share/man/de/man5/apt_preferences.5.gz", + "/usr/share/man/de/man5/sources.list.5.gz", + "/usr/share/man/de/man7/apt-patterns.7.gz", + "/usr/share/man/de/man8/apt-cache.8.gz", + "/usr/share/man/de/man8/apt-cdrom.8.gz", + "/usr/share/man/de/man8/apt-config.8.gz", + "/usr/share/man/de/man8/apt-get.8.gz", + "/usr/share/man/de/man8/apt-mark.8.gz", + "/usr/share/man/de/man8/apt-secure.8.gz", + "/usr/share/man/de/man8/apt.8.gz", + "/usr/share/man/es/man5/apt_preferences.5.gz", + "/usr/share/man/es/man8/apt-cache.8.gz", + "/usr/share/man/es/man8/apt-cdrom.8.gz", + "/usr/share/man/es/man8/apt-config.8.gz", + "/usr/share/man/fr/man1/apt-transport-http.1.gz", + "/usr/share/man/fr/man1/apt-transport-https.1.gz", + "/usr/share/man/fr/man1/apt-transport-mirror.1.gz", + "/usr/share/man/fr/man5/apt.conf.5.gz", + "/usr/share/man/fr/man5/apt_auth.conf.5.gz", + "/usr/share/man/fr/man5/apt_preferences.5.gz", + "/usr/share/man/fr/man5/sources.list.5.gz", + "/usr/share/man/fr/man7/apt-patterns.7.gz", + "/usr/share/man/fr/man8/apt-cache.8.gz", + "/usr/share/man/fr/man8/apt-cdrom.8.gz", + "/usr/share/man/fr/man8/apt-config.8.gz", + "/usr/share/man/fr/man8/apt-get.8.gz", + "/usr/share/man/fr/man8/apt-mark.8.gz", + "/usr/share/man/fr/man8/apt-secure.8.gz", + "/usr/share/man/fr/man8/apt.8.gz", + "/usr/share/man/it/man5/apt.conf.5.gz", + "/usr/share/man/it/man5/apt_preferences.5.gz", + "/usr/share/man/it/man8/apt-cache.8.gz", + "/usr/share/man/it/man8/apt-cdrom.8.gz", + "/usr/share/man/it/man8/apt-config.8.gz", + "/usr/share/man/it/man8/apt-mark.8.gz", + "/usr/share/man/it/man8/apt.8.gz", + "/usr/share/man/ja/man5/apt.conf.5.gz", + "/usr/share/man/ja/man5/apt_preferences.5.gz", + "/usr/share/man/ja/man8/apt-cache.8.gz", + "/usr/share/man/ja/man8/apt-cdrom.8.gz", + "/usr/share/man/ja/man8/apt-config.8.gz", + "/usr/share/man/ja/man8/apt-mark.8.gz", + "/usr/share/man/ja/man8/apt.8.gz", + "/usr/share/man/man1/apt-transport-http.1.gz", + "/usr/share/man/man1/apt-transport-https.1.gz", + "/usr/share/man/man1/apt-transport-mirror.1.gz", + "/usr/share/man/man5/apt.conf.5.gz", + "/usr/share/man/man5/apt_auth.conf.5.gz", + "/usr/share/man/man5/apt_preferences.5.gz", + "/usr/share/man/man5/sources.list.5.gz", + "/usr/share/man/man7/apt-patterns.7.gz", + "/usr/share/man/man8/apt-cache.8.gz", + "/usr/share/man/man8/apt-cdrom.8.gz", + "/usr/share/man/man8/apt-config.8.gz", + "/usr/share/man/man8/apt-get.8.gz", + "/usr/share/man/man8/apt-mark.8.gz", + "/usr/share/man/man8/apt-secure.8.gz", + "/usr/share/man/man8/apt.8.gz", + "/usr/share/man/nl/man1/apt-transport-http.1.gz", + "/usr/share/man/nl/man1/apt-transport-https.1.gz", + "/usr/share/man/nl/man1/apt-transport-mirror.1.gz", + "/usr/share/man/nl/man5/apt.conf.5.gz", + "/usr/share/man/nl/man5/apt_auth.conf.5.gz", + "/usr/share/man/nl/man5/apt_preferences.5.gz", + "/usr/share/man/nl/man5/sources.list.5.gz", + "/usr/share/man/nl/man7/apt-patterns.7.gz", + "/usr/share/man/nl/man8/apt-cache.8.gz", + "/usr/share/man/nl/man8/apt-cdrom.8.gz", + "/usr/share/man/nl/man8/apt-config.8.gz", + "/usr/share/man/nl/man8/apt-get.8.gz", + "/usr/share/man/nl/man8/apt-mark.8.gz", + "/usr/share/man/nl/man8/apt-secure.8.gz", + "/usr/share/man/nl/man8/apt.8.gz", + "/usr/share/man/pl/man5/apt_preferences.5.gz", + "/usr/share/man/pl/man8/apt-cache.8.gz", + "/usr/share/man/pl/man8/apt-cdrom.8.gz", + "/usr/share/man/pl/man8/apt-config.8.gz", + "/usr/share/man/pt/man1/apt-transport-http.1.gz", + "/usr/share/man/pt/man1/apt-transport-https.1.gz", + "/usr/share/man/pt/man1/apt-transport-mirror.1.gz", + "/usr/share/man/pt/man5/apt.conf.5.gz", + "/usr/share/man/pt/man5/apt_auth.conf.5.gz", + "/usr/share/man/pt/man5/apt_preferences.5.gz", + "/usr/share/man/pt/man5/sources.list.5.gz", + "/usr/share/man/pt/man7/apt-patterns.7.gz", + "/usr/share/man/pt/man8/apt-cache.8.gz", + "/usr/share/man/pt/man8/apt-cdrom.8.gz", + "/usr/share/man/pt/man8/apt-config.8.gz", + "/usr/share/man/pt/man8/apt-get.8.gz", + "/usr/share/man/pt/man8/apt-mark.8.gz", + "/usr/share/man/pt/man8/apt-secure.8.gz", + "/usr/share/man/pt/man8/apt.8.gz" + ] + }, + { + "ID": "base-files@13.8+deb13u6", + "Name": "base-files", + "Identifier": { + "PURL": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64\u0026distro=debian-13.6", + "UID": "4bb4c2ef5a12c64b" + }, + "Version": "13.8+deb13u6", + "Arch": "amd64", + "SrcName": "base-files", + "SrcVersion": "13.8+deb13u6", + "Licenses": [ + "GPL-2.0-or-later", + "verbatim" + ], + "Maintainer": "Santiago Vila \u003csanvila@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/os-release", + "/usr/share/base-files/dot.bashrc", + "/usr/share/base-files/dot.profile", + "/usr/share/base-files/dot.profile.md5sums", + "/usr/share/base-files/info.dir", + "/usr/share/base-files/motd", + "/usr/share/base-files/profile", + "/usr/share/base-files/profile.md5sums", + "/usr/share/base-files/staff-group-for-usr-local", + "/usr/share/common-licenses/Apache-2.0", + "/usr/share/common-licenses/Artistic", + "/usr/share/common-licenses/BSD", + "/usr/share/common-licenses/CC0-1.0", + "/usr/share/common-licenses/GFDL-1.2", + "/usr/share/common-licenses/GFDL-1.3", + "/usr/share/common-licenses/GPL-1", + "/usr/share/common-licenses/GPL-2", + "/usr/share/common-licenses/GPL-3", + "/usr/share/common-licenses/LGPL-2", + "/usr/share/common-licenses/LGPL-2.1", + "/usr/share/common-licenses/LGPL-3", + "/usr/share/common-licenses/MPL-1.1", + "/usr/share/common-licenses/MPL-2.0", + "/usr/share/doc/base-files/NEWS.Debian.gz", + "/usr/share/doc/base-files/README", + "/usr/share/doc/base-files/README.FHS", + "/usr/share/doc/base-files/changelog.gz", + "/usr/share/doc/base-files/copyright", + "/usr/share/lintian/overrides/base-files" + ] + }, + { + "ID": "base-passwd@3.6.7", + "Name": "base-passwd", + "Identifier": { + "PURL": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64\u0026distro=debian-13.6", + "UID": "bc0cc430715927e6" + }, + "Version": "3.6.7", + "Arch": "amd64", + "SrcName": "base-passwd", + "SrcVersion": "3.6.7", + "Licenses": [ + "GPL-2.0-only", + "public-domain" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libdebconfclient0@0.280", + "libselinux1@3.8.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/sbin/update-passwd", + "/usr/share/base-passwd/group.master", + "/usr/share/base-passwd/passwd.master", + "/usr/share/doc-base/base-passwd.users-and-groups", + "/usr/share/doc/base-passwd/README", + "/usr/share/doc/base-passwd/changelog.gz", + "/usr/share/doc/base-passwd/copyright", + "/usr/share/doc/base-passwd/users-and-groups.html", + "/usr/share/doc/base-passwd/users-and-groups.txt.gz", + "/usr/share/lintian/overrides/base-passwd", + "/usr/share/man/de/man8/update-passwd.8.gz", + "/usr/share/man/es/man8/update-passwd.8.gz", + "/usr/share/man/fr/man8/update-passwd.8.gz", + "/usr/share/man/ja/man8/update-passwd.8.gz", + "/usr/share/man/man8/update-passwd.8.gz", + "/usr/share/man/pl/man8/update-passwd.8.gz", + "/usr/share/man/ro/man8/update-passwd.8.gz", + "/usr/share/man/ru/man8/update-passwd.8.gz" + ] + }, + { + "ID": "bash@5.2.37-2+b9", + "Name": "bash", + "Identifier": { + "PURL": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64\u0026distro=debian-13.6", + "UID": "2c8d7060f3972831" + }, + "Version": "5.2.37", + "Release": "2+b9", + "Arch": "amd64", + "SrcName": "bash", + "SrcVersion": "5.2.37", + "SrcRelease": "2", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "GPL-2.0-or-later", + "GPL-2.0-only", + "GFDL-1.3-no-invariants-only", + "GFDL-1.3-only", + "Latex2e", + "BSD-4-Clause-UC", + "MIT", + "permissive" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "DependsOn": [ + "base-files@13.8+deb13u6", + "debianutils@5.23.2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/bash", + "/usr/bin/bashbug", + "/usr/bin/clear_console", + "/usr/share/debianutils/shells.d/bash", + "/usr/share/doc/bash/CHANGES.gz", + "/usr/share/doc/bash/COMPAT.gz", + "/usr/share/doc/bash/INTRO.gz", + "/usr/share/doc/bash/NEWS.gz", + "/usr/share/doc/bash/POSIX.gz", + "/usr/share/doc/bash/RBASH", + "/usr/share/doc/bash/README.Debian.gz", + "/usr/share/doc/bash/README.abs-guide", + "/usr/share/doc/bash/README.commands.gz", + "/usr/share/doc/bash/README.gz", + "/usr/share/doc/bash/changelog.Debian.amd64.gz", + "/usr/share/doc/bash/changelog.Debian.gz", + "/usr/share/doc/bash/changelog.gz", + "/usr/share/doc/bash/copyright", + "/usr/share/doc/bash/inputrc.arrows", + "/usr/share/lintian/overrides/bash", + "/usr/share/locale/af/LC_MESSAGES/bash.mo", + "/usr/share/locale/bg/LC_MESSAGES/bash.mo", + "/usr/share/locale/ca/LC_MESSAGES/bash.mo", + "/usr/share/locale/cs/LC_MESSAGES/bash.mo", + "/usr/share/locale/da/LC_MESSAGES/bash.mo", + "/usr/share/locale/de/LC_MESSAGES/bash.mo", + "/usr/share/locale/el/LC_MESSAGES/bash.mo", + "/usr/share/locale/en@boldquot/LC_MESSAGES/bash.mo", + "/usr/share/locale/en@quot/LC_MESSAGES/bash.mo", + "/usr/share/locale/eo/LC_MESSAGES/bash.mo", + "/usr/share/locale/es/LC_MESSAGES/bash.mo", + "/usr/share/locale/et/LC_MESSAGES/bash.mo", + "/usr/share/locale/fi/LC_MESSAGES/bash.mo", + "/usr/share/locale/fr/LC_MESSAGES/bash.mo", + "/usr/share/locale/ga/LC_MESSAGES/bash.mo", + "/usr/share/locale/gl/LC_MESSAGES/bash.mo", + "/usr/share/locale/hr/LC_MESSAGES/bash.mo", + "/usr/share/locale/hu/LC_MESSAGES/bash.mo", + "/usr/share/locale/id/LC_MESSAGES/bash.mo", + "/usr/share/locale/it/LC_MESSAGES/bash.mo", + "/usr/share/locale/ja/LC_MESSAGES/bash.mo", + "/usr/share/locale/ko/LC_MESSAGES/bash.mo", + "/usr/share/locale/lt/LC_MESSAGES/bash.mo", + "/usr/share/locale/nb/LC_MESSAGES/bash.mo", + "/usr/share/locale/nl/LC_MESSAGES/bash.mo", + "/usr/share/locale/pl/LC_MESSAGES/bash.mo", + "/usr/share/locale/pt/LC_MESSAGES/bash.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/bash.mo", + "/usr/share/locale/ro/LC_MESSAGES/bash.mo", + "/usr/share/locale/ru/LC_MESSAGES/bash.mo", + "/usr/share/locale/sk/LC_MESSAGES/bash.mo", + "/usr/share/locale/sl/LC_MESSAGES/bash.mo", + "/usr/share/locale/sr/LC_MESSAGES/bash.mo", + "/usr/share/locale/sv/LC_MESSAGES/bash.mo", + "/usr/share/locale/tr/LC_MESSAGES/bash.mo", + "/usr/share/locale/uk/LC_MESSAGES/bash.mo", + "/usr/share/locale/vi/LC_MESSAGES/bash.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/bash.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/bash.mo", + "/usr/share/man/man1/bash.1.gz", + "/usr/share/man/man1/bashbug.1.gz", + "/usr/share/man/man1/clear_console.1.gz", + "/usr/share/man/man1/rbash.1.gz", + "/usr/share/man/man7/bash-builtins.7.gz", + "/usr/share/menu/bash" + ] + }, + { + "ID": "bsdutils@1:2.41-5", + "Name": "bsdutils", + "Identifier": { + "PURL": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "c9de60be80a96a27" + }, + "Version": "2.41", + "Release": "5", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/logger", + "/usr/bin/renice", + "/usr/bin/script", + "/usr/bin/scriptlive", + "/usr/bin/scriptreplay", + "/usr/bin/wall", + "/usr/share/bash-completion/completions/logger", + "/usr/share/bash-completion/completions/renice", + "/usr/share/bash-completion/completions/script", + "/usr/share/bash-completion/completions/scriptlive", + "/usr/share/bash-completion/completions/scriptreplay", + "/usr/share/bash-completion/completions/wall", + "/usr/share/doc/bsdutils/NEWS.Debian.gz", + "/usr/share/doc/bsdutils/changelog.Debian.gz", + "/usr/share/doc/bsdutils/changelog.gz", + "/usr/share/doc/bsdutils/copyright", + "/usr/share/lintian/overrides/bsdutils", + "/usr/share/man/man1/logger.1.gz", + "/usr/share/man/man1/renice.1.gz", + "/usr/share/man/man1/script.1.gz", + "/usr/share/man/man1/scriptlive.1.gz", + "/usr/share/man/man1/scriptreplay.1.gz", + "/usr/share/man/man1/wall.1.gz" + ] + }, + { + "ID": "ca-certificates@20250419", + "Name": "ca-certificates", + "Identifier": { + "PURL": "pkg:deb/debian/ca-certificates@20250419?arch=all\u0026distro=debian-13.6", + "UID": "6365e842529686ef" + }, + "Version": "20250419", + "Arch": "all", + "SrcName": "ca-certificates", + "SrcVersion": "20250419", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "MPL-2.0" + ], + "Maintainer": "Julien Cristau \u003cjcristau@debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91", + "openssl@3.5.6-1~deb13u2" + ], + "Layer": { + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + "InstalledFiles": [ + "/usr/sbin/update-ca-certificates", + "/usr/share/ca-certificates/mozilla/ACCVRAIZ1.crt", + "/usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM.crt", + "/usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM_SERVIDORES_SEGUROS.crt", + "/usr/share/ca-certificates/mozilla/ANF_Secure_Server_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/Actalis_Authentication_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Commercial.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Networking.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Premium.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Premium_ECC.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_1.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_2.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_3.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_4.crt", + "/usr/share/ca-certificates/mozilla/Atos_TrustedRoot_2011.crt", + "/usr/share/ca-certificates/mozilla/Atos_TrustedRoot_Root_CA_ECC_TLS_2021.crt", + "/usr/share/ca-certificates/mozilla/Atos_TrustedRoot_Root_CA_RSA_TLS_2021.crt", + "/usr/share/ca-certificates/mozilla/Autoridad_de_Certificacion_Firmaprofesional_CIF_A62634068.crt", + "/usr/share/ca-certificates/mozilla/BJCA_Global_Root_CA1.crt", + "/usr/share/ca-certificates/mozilla/BJCA_Global_Root_CA2.crt", + "/usr/share/ca-certificates/mozilla/Baltimore_CyberTrust_Root.crt", + "/usr/share/ca-certificates/mozilla/Buypass_Class_2_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/Buypass_Class_3_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/CA_Disig_Root_R2.crt", + "/usr/share/ca-certificates/mozilla/CFCA_EV_ROOT.crt", + "/usr/share/ca-certificates/mozilla/COMODO_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/COMODO_ECC_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/COMODO_RSA_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Certainly_Root_E1.crt", + "/usr/share/ca-certificates/mozilla/Certainly_Root_R1.crt", + "/usr/share/ca-certificates/mozilla/Certigna.crt", + "/usr/share/ca-certificates/mozilla/Certigna_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/Certum_EC-384_CA.crt", + "/usr/share/ca-certificates/mozilla/Certum_Trusted_Network_CA.crt", + "/usr/share/ca-certificates/mozilla/Certum_Trusted_Network_CA_2.crt", + "/usr/share/ca-certificates/mozilla/Certum_Trusted_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_ECC_Root-01.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_ECC_Root-02.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_RSA_Root-01.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_RSA_Root-02.crt", + "/usr/share/ca-certificates/mozilla/Comodo_AAA_Services_root.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_BR_Root_CA_1_2020.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_BR_Root_CA_2_2023.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_EV_Root_CA_1_2020.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_EV_Root_CA_2_2023.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_Root_Class_3_CA_2_2009.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_Root_Class_3_CA_2_EV_2009.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Assured_ID_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Assured_ID_Root_G2.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Assured_ID_Root_G3.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_G2.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_G3.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_High_Assurance_EV_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_TLS_ECC_P384_Root_G5.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_TLS_RSA4096_Root_G5.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Trusted_Root_G4.crt", + "/usr/share/ca-certificates/mozilla/Entrust.net_Premium_2048_Secure_Server_CA.crt", + "/usr/share/ca-certificates/mozilla/Entrust_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Entrust_Root_Certification_Authority_-_EC1.crt", + "/usr/share/ca-certificates/mozilla/Entrust_Root_Certification_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/FIRMAPROFESIONAL_CA_ROOT-A_WEB.crt", + "/usr/share/ca-certificates/mozilla/GDCA_TrustAUTH_R5_ROOT.crt", + "/usr/share/ca-certificates/mozilla/GLOBALTRUST_2020.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R1.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R2.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R3.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R4.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_ECC_Root_CA_-_R4.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_ECC_Root_CA_-_R5.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R3.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R6.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_E46.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_R46.crt", + "/usr/share/ca-certificates/mozilla/Go_Daddy_Class_2_CA.crt", + "/usr/share/ca-certificates/mozilla/Go_Daddy_Root_Certificate_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/HARICA_TLS_ECC_Root_CA_2021.crt", + "/usr/share/ca-certificates/mozilla/HARICA_TLS_RSA_Root_CA_2021.crt", + "/usr/share/ca-certificates/mozilla/Hellenic_Academic_and_Research_Institutions_ECC_RootCA_2015.crt", + "/usr/share/ca-certificates/mozilla/Hellenic_Academic_and_Research_Institutions_RootCA_2015.crt", + "/usr/share/ca-certificates/mozilla/HiPKI_Root_CA_-_G1.crt", + "/usr/share/ca-certificates/mozilla/Hongkong_Post_Root_CA_3.crt", + "/usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt", + "/usr/share/ca-certificates/mozilla/ISRG_Root_X2.crt", + "/usr/share/ca-certificates/mozilla/IdenTrust_Commercial_Root_CA_1.crt", + "/usr/share/ca-certificates/mozilla/IdenTrust_Public_Sector_Root_CA_1.crt", + "/usr/share/ca-certificates/mozilla/Izenpe.com.crt", + "/usr/share/ca-certificates/mozilla/Microsec_e-Szigno_Root_CA_2009.crt", + "/usr/share/ca-certificates/mozilla/Microsoft_ECC_Root_Certificate_Authority_2017.crt", + "/usr/share/ca-certificates/mozilla/Microsoft_RSA_Root_Certificate_Authority_2017.crt", + "/usr/share/ca-certificates/mozilla/NAVER_Global_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/NetLock_Arany_=Class_Gold=_Főtanúsítvány.crt", + "/usr/share/ca-certificates/mozilla/OISTE_WISeKey_Global_Root_GB_CA.crt", + "/usr/share/ca-certificates/mozilla/OISTE_WISeKey_Global_Root_GC_CA.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_1_G3.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_2.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_2_G3.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_3.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_3_G3.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_EV_Root_Certification_Authority_ECC.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_EV_Root_Certification_Authority_RSA_R2.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_Root_Certification_Authority_ECC.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_Root_Certification_Authority_RSA.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_TLS_ECC_Root_CA_2022.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_TLS_RSA_Root_CA_2022.crt", + "/usr/share/ca-certificates/mozilla/SZAFIR_ROOT_CA2.crt", + "/usr/share/ca-certificates/mozilla/Sectigo_Public_Server_Authentication_Root_E46.crt", + "/usr/share/ca-certificates/mozilla/Sectigo_Public_Server_Authentication_Root_R46.crt", + "/usr/share/ca-certificates/mozilla/SecureSign_Root_CA12.crt", + "/usr/share/ca-certificates/mozilla/SecureSign_Root_CA14.crt", + "/usr/share/ca-certificates/mozilla/SecureSign_Root_CA15.crt", + "/usr/share/ca-certificates/mozilla/SecureTrust_CA.crt", + "/usr/share/ca-certificates/mozilla/Secure_Global_CA.crt", + "/usr/share/ca-certificates/mozilla/Security_Communication_ECC_RootCA1.crt", + "/usr/share/ca-certificates/mozilla/Security_Communication_RootCA2.crt", + "/usr/share/ca-certificates/mozilla/Starfield_Class_2_CA.crt", + "/usr/share/ca-certificates/mozilla/Starfield_Root_Certificate_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/Starfield_Services_Root_Certificate_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/SwissSign_Gold_CA_-_G2.crt", + "/usr/share/ca-certificates/mozilla/T-TeleSec_GlobalRoot_Class_2.crt", + "/usr/share/ca-certificates/mozilla/T-TeleSec_GlobalRoot_Class_3.crt", + "/usr/share/ca-certificates/mozilla/TUBITAK_Kamu_SM_SSL_Kok_Sertifikasi_-_Surum_1.crt", + "/usr/share/ca-certificates/mozilla/TWCA_CYBER_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/TWCA_Global_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/TWCA_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Telekom_Security_TLS_ECC_Root_2020.crt", + "/usr/share/ca-certificates/mozilla/Telekom_Security_TLS_RSA_Root_2023.crt", + "/usr/share/ca-certificates/mozilla/TeliaSonera_Root_CA_v1.crt", + "/usr/share/ca-certificates/mozilla/Telia_Root_CA_v2.crt", + "/usr/share/ca-certificates/mozilla/TrustAsia_Global_Root_CA_G3.crt", + "/usr/share/ca-certificates/mozilla/TrustAsia_Global_Root_CA_G4.crt", + "/usr/share/ca-certificates/mozilla/Trustwave_Global_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Trustwave_Global_ECC_P256_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Trustwave_Global_ECC_P384_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/TunTrust_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/UCA_Extended_Validation_Root.crt", + "/usr/share/ca-certificates/mozilla/UCA_Global_G2_Root.crt", + "/usr/share/ca-certificates/mozilla/USERTrust_ECC_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/USERTrust_RSA_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/XRamp_Global_CA_Root.crt", + "/usr/share/ca-certificates/mozilla/certSIGN_ROOT_CA.crt", + "/usr/share/ca-certificates/mozilla/certSIGN_Root_CA_G2.crt", + "/usr/share/ca-certificates/mozilla/e-Szigno_Root_CA_2017.crt", + "/usr/share/ca-certificates/mozilla/ePKI_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/emSign_ECC_Root_CA_-_C3.crt", + "/usr/share/ca-certificates/mozilla/emSign_ECC_Root_CA_-_G3.crt", + "/usr/share/ca-certificates/mozilla/emSign_Root_CA_-_C1.crt", + "/usr/share/ca-certificates/mozilla/emSign_Root_CA_-_G1.crt", + "/usr/share/ca-certificates/mozilla/vTrus_ECC_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/vTrus_Root_CA.crt", + "/usr/share/doc/ca-certificates/README.Debian", + "/usr/share/doc/ca-certificates/changelog.gz", + "/usr/share/doc/ca-certificates/copyright", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/Makefile", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/README", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/ca-certificates-local.triggers", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/changelog", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/compat", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/control", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/copyright", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/postrm", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/rules", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/source/format", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/local/Local_Root_CA.crt", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/local/Makefile", + "/usr/share/man/man8/update-ca-certificates.8.gz" + ] + }, + { + "ID": "coreutils@9.7-3", + "Name": "coreutils", + "Identifier": { + "PURL": "pkg:deb/debian/coreutils@9.7-3?arch=amd64\u0026distro=debian-13.6", + "UID": "a90cbdbcbab1768e" + }, + "Version": "9.7", + "Release": "3", + "Arch": "amd64", + "SrcName": "coreutils", + "SrcVersion": "9.7", + "SrcRelease": "3", + "Licenses": [ + "GPL-3.0-or-later", + "BSD-4-Clause-UC", + "GPL-3.0-only", + "ISC", + "FSFULLR", + "GFDL-1.3-no-invariants-only", + "GFDL-1.3-only" + ], + "Maintainer": "Michael Stone \u003cmstone@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/[", + "/usr/bin/arch", + "/usr/bin/b2sum", + "/usr/bin/base32", + "/usr/bin/base64", + "/usr/bin/basename", + "/usr/bin/basenc", + "/usr/bin/cat", + "/usr/bin/chcon", + "/usr/bin/chgrp", + "/usr/bin/chmod", + "/usr/bin/chown", + "/usr/bin/cksum", + "/usr/bin/comm", + "/usr/bin/cp", + "/usr/bin/csplit", + "/usr/bin/cut", + "/usr/bin/date", + "/usr/bin/dd", + "/usr/bin/df", + "/usr/bin/dir", + "/usr/bin/dircolors", + "/usr/bin/dirname", + "/usr/bin/du", + "/usr/bin/echo", + "/usr/bin/env", + "/usr/bin/expand", + "/usr/bin/expr", + "/usr/bin/factor", + "/usr/bin/false", + "/usr/bin/fmt", + "/usr/bin/fold", + "/usr/bin/groups", + "/usr/bin/head", + "/usr/bin/hostid", + "/usr/bin/id", + "/usr/bin/install", + "/usr/bin/join", + "/usr/bin/link", + "/usr/bin/ln", + "/usr/bin/logname", + "/usr/bin/ls", + "/usr/bin/md5sum", + "/usr/bin/mkdir", + "/usr/bin/mkfifo", + "/usr/bin/mknod", + "/usr/bin/mktemp", + "/usr/bin/mv", + "/usr/bin/nice", + "/usr/bin/nl", + "/usr/bin/nohup", + "/usr/bin/nproc", + "/usr/bin/numfmt", + "/usr/bin/od", + "/usr/bin/paste", + "/usr/bin/pathchk", + "/usr/bin/pinky", + "/usr/bin/pr", + "/usr/bin/printenv", + "/usr/bin/printf", + "/usr/bin/ptx", + "/usr/bin/pwd", + "/usr/bin/readlink", + "/usr/bin/realpath", + "/usr/bin/rm", + "/usr/bin/rmdir", + "/usr/bin/runcon", + "/usr/bin/seq", + "/usr/bin/sha1sum", + "/usr/bin/sha224sum", + "/usr/bin/sha256sum", + "/usr/bin/sha384sum", + "/usr/bin/sha512sum", + "/usr/bin/shred", + "/usr/bin/shuf", + "/usr/bin/sleep", + "/usr/bin/sort", + "/usr/bin/split", + "/usr/bin/stat", + "/usr/bin/stdbuf", + "/usr/bin/stty", + "/usr/bin/sum", + "/usr/bin/sync", + "/usr/bin/tac", + "/usr/bin/tail", + "/usr/bin/tee", + "/usr/bin/test", + "/usr/bin/timeout", + "/usr/bin/touch", + "/usr/bin/tr", + "/usr/bin/true", + "/usr/bin/truncate", + "/usr/bin/tsort", + "/usr/bin/tty", + "/usr/bin/uname", + "/usr/bin/unexpand", + "/usr/bin/uniq", + "/usr/bin/unlink", + "/usr/bin/users", + "/usr/bin/vdir", + "/usr/bin/wc", + "/usr/bin/who", + "/usr/bin/whoami", + "/usr/bin/yes", + "/usr/libexec/coreutils/libstdbuf.so", + "/usr/sbin/chroot", + "/usr/share/doc/coreutils/AUTHORS", + "/usr/share/doc/coreutils/NEWS.gz", + "/usr/share/doc/coreutils/README.Debian", + "/usr/share/doc/coreutils/README.gz", + "/usr/share/doc/coreutils/THANKS.gz", + "/usr/share/doc/coreutils/TODO.gz", + "/usr/share/doc/coreutils/changelog.Debian.gz", + "/usr/share/doc/coreutils/changelog.gz", + "/usr/share/doc/coreutils/copyright", + "/usr/share/info/coreutils.info.gz", + "/usr/share/lintian/overrides/coreutils", + "/usr/share/locale/af/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/be/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/bg/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ca/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/cs/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/da/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/de/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/el/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/eo/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/es/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/et/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/eu/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/fi/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/fr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ga/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/gl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/hr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/hu/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ia/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/id/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/it/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ja/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ka/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/kk/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ko/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/lg/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/lt/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ms/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/nb/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/nl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/pl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/pt/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ro/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ru/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sk/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sv/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ta/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/tr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/uk/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/vi/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/coreutils.mo", + "/usr/share/man/man1/arch.1.gz", + "/usr/share/man/man1/b2sum.1.gz", + "/usr/share/man/man1/base32.1.gz", + "/usr/share/man/man1/base64.1.gz", + "/usr/share/man/man1/basename.1.gz", + "/usr/share/man/man1/basenc.1.gz", + "/usr/share/man/man1/cat.1.gz", + "/usr/share/man/man1/chcon.1.gz", + "/usr/share/man/man1/chgrp.1.gz", + "/usr/share/man/man1/chmod.1.gz", + "/usr/share/man/man1/chown.1.gz", + "/usr/share/man/man1/cksum.1.gz", + "/usr/share/man/man1/comm.1.gz", + "/usr/share/man/man1/cp.1.gz", + "/usr/share/man/man1/csplit.1.gz", + "/usr/share/man/man1/cut.1.gz", + "/usr/share/man/man1/date.1.gz", + "/usr/share/man/man1/dd.1.gz", + "/usr/share/man/man1/df.1.gz", + "/usr/share/man/man1/dir.1.gz", + "/usr/share/man/man1/dircolors.1.gz", + "/usr/share/man/man1/dirname.1.gz", + "/usr/share/man/man1/du.1.gz", + "/usr/share/man/man1/echo.1.gz", + "/usr/share/man/man1/env.1.gz", + "/usr/share/man/man1/expand.1.gz", + "/usr/share/man/man1/expr.1.gz", + "/usr/share/man/man1/factor.1.gz", + "/usr/share/man/man1/false.1.gz", + "/usr/share/man/man1/fmt.1.gz", + "/usr/share/man/man1/fold.1.gz", + "/usr/share/man/man1/groups.1.gz", + "/usr/share/man/man1/head.1.gz", + "/usr/share/man/man1/hostid.1.gz", + "/usr/share/man/man1/id.1.gz", + "/usr/share/man/man1/install.1.gz", + "/usr/share/man/man1/join.1.gz", + "/usr/share/man/man1/link.1.gz", + "/usr/share/man/man1/ln.1.gz", + "/usr/share/man/man1/logname.1.gz", + "/usr/share/man/man1/ls.1.gz", + "/usr/share/man/man1/md5sum.1.gz", + "/usr/share/man/man1/mkdir.1.gz", + "/usr/share/man/man1/mkfifo.1.gz", + "/usr/share/man/man1/mknod.1.gz", + "/usr/share/man/man1/mktemp.1.gz", + "/usr/share/man/man1/mv.1.gz", + "/usr/share/man/man1/nice.1.gz", + "/usr/share/man/man1/nl.1.gz", + "/usr/share/man/man1/nohup.1.gz", + "/usr/share/man/man1/nproc.1.gz", + "/usr/share/man/man1/numfmt.1.gz", + "/usr/share/man/man1/od.1.gz", + "/usr/share/man/man1/paste.1.gz", + "/usr/share/man/man1/pathchk.1.gz", + "/usr/share/man/man1/pinky.1.gz", + "/usr/share/man/man1/pr.1.gz", + "/usr/share/man/man1/printenv.1.gz", + "/usr/share/man/man1/printf.1.gz", + "/usr/share/man/man1/ptx.1.gz", + "/usr/share/man/man1/pwd.1.gz", + "/usr/share/man/man1/readlink.1.gz", + "/usr/share/man/man1/realpath.1.gz", + "/usr/share/man/man1/rm.1.gz", + "/usr/share/man/man1/rmdir.1.gz", + "/usr/share/man/man1/runcon.1.gz", + "/usr/share/man/man1/seq.1.gz", + "/usr/share/man/man1/sha1sum.1.gz", + "/usr/share/man/man1/sha224sum.1.gz", + "/usr/share/man/man1/sha256sum.1.gz", + "/usr/share/man/man1/sha384sum.1.gz", + "/usr/share/man/man1/sha512sum.1.gz", + "/usr/share/man/man1/shred.1.gz", + "/usr/share/man/man1/shuf.1.gz", + "/usr/share/man/man1/sleep.1.gz", + "/usr/share/man/man1/sort.1.gz", + "/usr/share/man/man1/split.1.gz", + "/usr/share/man/man1/stat.1.gz", + "/usr/share/man/man1/stdbuf.1.gz", + "/usr/share/man/man1/stty.1.gz", + "/usr/share/man/man1/sum.1.gz", + "/usr/share/man/man1/sync.1.gz", + "/usr/share/man/man1/tac.1.gz", + "/usr/share/man/man1/tail.1.gz", + "/usr/share/man/man1/tee.1.gz", + "/usr/share/man/man1/test.1.gz", + "/usr/share/man/man1/timeout.1.gz", + "/usr/share/man/man1/touch.1.gz", + "/usr/share/man/man1/tr.1.gz", + "/usr/share/man/man1/true.1.gz", + "/usr/share/man/man1/truncate.1.gz", + "/usr/share/man/man1/tsort.1.gz", + "/usr/share/man/man1/tty.1.gz", + "/usr/share/man/man1/uname.1.gz", + "/usr/share/man/man1/unexpand.1.gz", + "/usr/share/man/man1/uniq.1.gz", + "/usr/share/man/man1/unlink.1.gz", + "/usr/share/man/man1/users.1.gz", + "/usr/share/man/man1/vdir.1.gz", + "/usr/share/man/man1/wc.1.gz", + "/usr/share/man/man1/who.1.gz", + "/usr/share/man/man1/whoami.1.gz", + "/usr/share/man/man1/yes.1.gz", + "/usr/share/man/man8/chroot.8.gz" + ] + }, + { + "ID": "cyrus-sasl-lib@2.1.26-24.el7_9", + "Name": "cyrus-sasl-lib", + "Identifier": { + "PURL": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9", + "UID": "b814395bf7062155", + "BOMRef": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2" + }, + "Version": "2.1.26", + "Release": "24.el7_9", + "SrcName": "cyrus-sasl-lib", + "SrcVersion": "2.1.26", + "SrcRelease": "24.el7_9", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "dash@0.5.12-12", + "Name": "dash", + "Identifier": { + "PURL": "pkg:deb/debian/dash@0.5.12-12?arch=amd64\u0026distro=debian-13.6", + "UID": "89c835b0985cdc5c" + }, + "Version": "0.5.12", + "Release": "12", + "Arch": "amd64", + "SrcName": "dash", + "SrcVersion": "0.5.12", + "SrcRelease": "12", + "Licenses": [ + "BSD-3-Clause", + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Andrej Shadura \u003candrewsh@debian.org\u003e", + "DependsOn": [ + "debianutils@5.23.2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/dash", + "/usr/share/debianutils/shells.d/dash", + "/usr/share/doc/dash/README.Debian.diet", + "/usr/share/doc/dash/README.source", + "/usr/share/doc/dash/changelog.Debian.gz", + "/usr/share/doc/dash/changelog.gz", + "/usr/share/doc/dash/copyright", + "/usr/share/lintian/overrides/dash", + "/usr/share/man/man1/dash.1.gz", + "/usr/share/menu/dash" + ] + }, + { + "ID": "debconf@1.5.91", + "Name": "debconf", + "Identifier": { + "PURL": "pkg:deb/debian/debconf@1.5.91?arch=all\u0026distro=debian-13.6", + "UID": "dbd74d1c32616a65" + }, + "Version": "1.5.91", + "Arch": "all", + "SrcName": "debconf", + "SrcVersion": "1.5.91", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "Debconf Developers \u003cdebconf-devel@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/debconf", + "/usr/bin/debconf-apt-progress", + "/usr/bin/debconf-communicate", + "/usr/bin/debconf-copydb", + "/usr/bin/debconf-escape", + "/usr/bin/debconf-set-selections", + "/usr/bin/debconf-show", + "/usr/sbin/dpkg-preconfigure", + "/usr/sbin/dpkg-reconfigure", + "/usr/share/bash-completion/completions/debconf", + "/usr/share/debconf/confmodule", + "/usr/share/debconf/confmodule.sh", + "/usr/share/debconf/debconf.conf", + "/usr/share/debconf/fix_db.pl", + "/usr/share/debconf/frontend", + "/usr/share/doc/debconf/README.Debian", + "/usr/share/doc/debconf/changelog.gz", + "/usr/share/doc/debconf/copyright", + "/usr/share/lintian/overrides/debconf", + "/usr/share/man/man1/debconf-apt-progress.1.gz", + "/usr/share/man/man1/debconf-communicate.1.gz", + "/usr/share/man/man1/debconf-copydb.1.gz", + "/usr/share/man/man1/debconf-escape.1.gz", + "/usr/share/man/man1/debconf-set-selections.1.gz", + "/usr/share/man/man1/debconf-show.1.gz", + "/usr/share/man/man1/debconf.1.gz", + "/usr/share/man/man8/dpkg-preconfigure.8.gz", + "/usr/share/man/man8/dpkg-reconfigure.8.gz", + "/usr/share/perl5/Debconf/AutoSelect.pm", + "/usr/share/perl5/Debconf/Base.pm", + "/usr/share/perl5/Debconf/Client/ConfModule.pm", + "/usr/share/perl5/Debconf/ConfModule.pm", + "/usr/share/perl5/Debconf/Config.pm", + "/usr/share/perl5/Debconf/Db.pm", + "/usr/share/perl5/Debconf/DbDriver.pm", + "/usr/share/perl5/Debconf/DbDriver/Backup.pm", + "/usr/share/perl5/Debconf/DbDriver/Cache.pm", + "/usr/share/perl5/Debconf/DbDriver/Copy.pm", + "/usr/share/perl5/Debconf/DbDriver/Debug.pm", + "/usr/share/perl5/Debconf/DbDriver/DirTree.pm", + "/usr/share/perl5/Debconf/DbDriver/Directory.pm", + "/usr/share/perl5/Debconf/DbDriver/File.pm", + "/usr/share/perl5/Debconf/DbDriver/LDAP.pm", + "/usr/share/perl5/Debconf/DbDriver/PackageDir.pm", + "/usr/share/perl5/Debconf/DbDriver/Pipe.pm", + "/usr/share/perl5/Debconf/DbDriver/Stack.pm", + "/usr/share/perl5/Debconf/Element.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Error.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Note.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Password.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Progress.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Select.pm", + "/usr/share/perl5/Debconf/Element/Dialog/String.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Text.pm", + "/usr/share/perl5/Debconf/Element/Editor/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Editor/Error.pm", + "/usr/share/perl5/Debconf/Element/Editor/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Editor/Note.pm", + "/usr/share/perl5/Debconf/Element/Editor/Password.pm", + "/usr/share/perl5/Debconf/Element/Editor/Progress.pm", + "/usr/share/perl5/Debconf/Element/Editor/Select.pm", + "/usr/share/perl5/Debconf/Element/Editor/String.pm", + "/usr/share/perl5/Debconf/Element/Editor/Text.pm", + "/usr/share/perl5/Debconf/Element/Gnome.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Error.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Note.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Password.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Progress.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Select.pm", + "/usr/share/perl5/Debconf/Element/Gnome/String.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Text.pm", + "/usr/share/perl5/Debconf/Element/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Error.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Note.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Password.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Progress.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Select.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/String.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Text.pm", + "/usr/share/perl5/Debconf/Element/Select.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Error.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Note.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Password.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Progress.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Select.pm", + "/usr/share/perl5/Debconf/Element/Teletype/String.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Text.pm", + "/usr/share/perl5/Debconf/Element/Web/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Web/Error.pm", + "/usr/share/perl5/Debconf/Element/Web/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Web/Note.pm", + "/usr/share/perl5/Debconf/Element/Web/Password.pm", + "/usr/share/perl5/Debconf/Element/Web/Progress.pm", + "/usr/share/perl5/Debconf/Element/Web/Select.pm", + "/usr/share/perl5/Debconf/Element/Web/String.pm", + "/usr/share/perl5/Debconf/Element/Web/Text.pm", + "/usr/share/perl5/Debconf/Encoding.pm", + "/usr/share/perl5/Debconf/Format.pm", + "/usr/share/perl5/Debconf/Format/822.pm", + "/usr/share/perl5/Debconf/FrontEnd.pm", + "/usr/share/perl5/Debconf/FrontEnd/Dialog.pm", + "/usr/share/perl5/Debconf/FrontEnd/Editor.pm", + "/usr/share/perl5/Debconf/FrontEnd/Gnome.pm", + "/usr/share/perl5/Debconf/FrontEnd/Kde.pm", + "/usr/share/perl5/Debconf/FrontEnd/Noninteractive.pm", + "/usr/share/perl5/Debconf/FrontEnd/Passthrough.pm", + "/usr/share/perl5/Debconf/FrontEnd/Readline.pm", + "/usr/share/perl5/Debconf/FrontEnd/ScreenSize.pm", + "/usr/share/perl5/Debconf/FrontEnd/Teletype.pm", + "/usr/share/perl5/Debconf/FrontEnd/Text.pm", + "/usr/share/perl5/Debconf/FrontEnd/Web.pm", + "/usr/share/perl5/Debconf/Gettext.pm", + "/usr/share/perl5/Debconf/Iterator.pm", + "/usr/share/perl5/Debconf/Log.pm", + "/usr/share/perl5/Debconf/Path.pm", + "/usr/share/perl5/Debconf/Priority.pm", + "/usr/share/perl5/Debconf/Question.pm", + "/usr/share/perl5/Debconf/Template.pm", + "/usr/share/perl5/Debconf/Template/Transient.pm", + "/usr/share/perl5/Debconf/TmpFile.pm", + "/usr/share/perl5/Debian/DebConf/Client/ConfModule.pm", + "/usr/share/pixmaps/debian-logo.png" + ] + }, + { + "ID": "debian-archive-keyring@2025.1", + "Name": "debian-archive-keyring", + "Identifier": { + "PURL": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all\u0026distro=debian-13.6", + "UID": "3d23f3bc34b84a13" + }, + "Version": "2025.1", + "Arch": "all", + "SrcName": "debian-archive-keyring", + "SrcVersion": "2025.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Debian Release Team \u003cpackages@release.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/debian-archive-keyring/NEWS.Debian.gz", + "/usr/share/doc/debian-archive-keyring/README", + "/usr/share/doc/debian-archive-keyring/changelog.gz", + "/usr/share/doc/debian-archive-keyring/copyright", + "/usr/share/keyrings/debian-archive-bookworm-automatic.pgp", + "/usr/share/keyrings/debian-archive-bookworm-security-automatic.pgp", + "/usr/share/keyrings/debian-archive-bookworm-stable.pgp", + "/usr/share/keyrings/debian-archive-bullseye-automatic.pgp", + "/usr/share/keyrings/debian-archive-bullseye-security-automatic.pgp", + "/usr/share/keyrings/debian-archive-bullseye-stable.pgp", + "/usr/share/keyrings/debian-archive-keyring.pgp", + "/usr/share/keyrings/debian-archive-removed-keys.pgp", + "/usr/share/keyrings/debian-archive-trixie-automatic.pgp", + "/usr/share/keyrings/debian-archive-trixie-security-automatic.pgp", + "/usr/share/keyrings/debian-archive-trixie-stable.pgp" + ] + }, + { + "ID": "debianutils@5.23.2", + "Name": "debianutils", + "Identifier": { + "PURL": "pkg:deb/debian/debianutils@5.23.2?arch=amd64\u0026distro=debian-13.6", + "UID": "1faaee83f4beb2af" + }, + "Version": "5.23.2", + "Arch": "amd64", + "SrcName": "debianutils", + "SrcVersion": "5.23.2", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "SMAIL-GPL" + ], + "Maintainer": "Ileana Dumitrescu \u003cileanadumitrescu95@gmail.com\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/ischroot", + "/usr/bin/run-parts", + "/usr/bin/savelog", + "/usr/bin/tempfile", + "/usr/bin/which.debianutils", + "/usr/sbin/add-shell", + "/usr/sbin/installkernel", + "/usr/sbin/remove-shell", + "/usr/sbin/update-shells", + "/usr/share/debianutils/shells", + "/usr/share/doc/debianutils/README.shells", + "/usr/share/doc/debianutils/changelog.gz", + "/usr/share/doc/debianutils/copyright", + "/usr/share/man/de/man1/which.debianutils.1.gz", + "/usr/share/man/de/man8/add-shell.8.gz", + "/usr/share/man/de/man8/installkernel.8.gz", + "/usr/share/man/de/man8/remove-shell.8.gz", + "/usr/share/man/de/man8/run-parts.8.gz", + "/usr/share/man/de/man8/savelog.8.gz", + "/usr/share/man/es/man1/which.debianutils.1.gz", + "/usr/share/man/es/man8/add-shell.8.gz", + "/usr/share/man/es/man8/installkernel.8.gz", + "/usr/share/man/es/man8/remove-shell.8.gz", + "/usr/share/man/es/man8/run-parts.8.gz", + "/usr/share/man/es/man8/savelog.8.gz", + "/usr/share/man/fr/man1/which.debianutils.1.gz", + "/usr/share/man/fr/man8/add-shell.8.gz", + "/usr/share/man/fr/man8/installkernel.8.gz", + "/usr/share/man/fr/man8/remove-shell.8.gz", + "/usr/share/man/fr/man8/run-parts.8.gz", + "/usr/share/man/fr/man8/savelog.8.gz", + "/usr/share/man/it/man1/which.debianutils.1.gz", + "/usr/share/man/it/man8/add-shell.8.gz", + "/usr/share/man/it/man8/installkernel.8.gz", + "/usr/share/man/it/man8/remove-shell.8.gz", + "/usr/share/man/it/man8/run-parts.8.gz", + "/usr/share/man/it/man8/savelog.8.gz", + "/usr/share/man/ja/man1/which.debianutils.1.gz", + "/usr/share/man/ja/man8/add-shell.8.gz", + "/usr/share/man/ja/man8/installkernel.8.gz", + "/usr/share/man/ja/man8/remove-shell.8.gz", + "/usr/share/man/ja/man8/run-parts.8.gz", + "/usr/share/man/ja/man8/savelog.8.gz", + "/usr/share/man/man1/ischroot.1.gz", + "/usr/share/man/man1/tempfile.1.gz", + "/usr/share/man/man1/which.debianutils.1.gz", + "/usr/share/man/man8/add-shell.8.gz", + "/usr/share/man/man8/installkernel.8.gz", + "/usr/share/man/man8/remove-shell.8.gz", + "/usr/share/man/man8/run-parts.8.gz", + "/usr/share/man/man8/savelog.8.gz", + "/usr/share/man/man8/update-shells.8.gz", + "/usr/share/man/pl/man1/which.debianutils.1.gz", + "/usr/share/man/pl/man8/add-shell.8.gz", + "/usr/share/man/pl/man8/installkernel.8.gz", + "/usr/share/man/pl/man8/remove-shell.8.gz", + "/usr/share/man/pl/man8/run-parts.8.gz", + "/usr/share/man/pl/man8/savelog.8.gz", + "/usr/share/man/pt/man1/which.debianutils.1.gz", + "/usr/share/man/pt/man8/add-shell.8.gz", + "/usr/share/man/pt/man8/installkernel.8.gz", + "/usr/share/man/pt/man8/remove-shell.8.gz", + "/usr/share/man/pt/man8/run-parts.8.gz", + "/usr/share/man/pt/man8/savelog.8.gz", + "/usr/share/man/sl/man1/which.debianutils.1.gz", + "/usr/share/man/sl/man8/add-shell.8.gz", + "/usr/share/man/sl/man8/installkernel.8.gz", + "/usr/share/man/sl/man8/remove-shell.8.gz", + "/usr/share/man/sl/man8/run-parts.8.gz", + "/usr/share/man/sl/man8/savelog.8.gz" + ] + }, + { + "ID": "diffutils@1:3.10-4", + "Name": "diffutils", + "Identifier": { + "PURL": "pkg:deb/debian/diffutils@3.10-4?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "6ae1b70a720e3ebb" + }, + "Version": "3.10", + "Release": "4", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "diffutils", + "SrcVersion": "3.10", + "SrcRelease": "4", + "SrcEpoch": 1, + "Licenses": [ + "GPL-3.0-or-later", + "FSFULLR", + "LGPL-2.1-or-later", + "GPL-3.0-with-autoconf-exception+", + "GPL-3.0-only", + "GPL-3+ with texinfo exception", + "LGPL-2.0-or-later", + "GPL-2.0-or-later", + "X11", + "FSFAP", + "GFDL-1.3-no-invariants-only", + "LGPL-3.0-or-later", + "LGPL-3.0-only", + "public-domain", + "LGPL-2.0-only", + "LGPL-2.1-only", + "GPL-2.0-only", + "GFDL-1.3-only" + ], + "Maintainer": "Santiago Vila \u003csanvila@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/cmp", + "/usr/bin/diff", + "/usr/bin/diff3", + "/usr/bin/sdiff", + "/usr/share/doc/diffutils/NEWS.gz", + "/usr/share/doc/diffutils/changelog.Debian.gz", + "/usr/share/doc/diffutils/changelog.gz", + "/usr/share/doc/diffutils/copyright", + "/usr/share/info/diffutils.info.gz", + "/usr/share/locale/bg/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ca/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/cs/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/da/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/de/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/el/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/eo/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/es/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/fi/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/fr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ga/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/gl/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/he/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/hr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/hu/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/id/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/it/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ja/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ka/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ko/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/lv/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ms/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/nb/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/nl/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/pl/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/pt/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ro/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ru/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/sr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/sv/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/tr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/uk/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/vi/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/diffutils.mo", + "/usr/share/man/man1/cmp.1.gz", + "/usr/share/man/man1/diff.1.gz", + "/usr/share/man/man1/diff3.1.gz", + "/usr/share/man/man1/sdiff.1.gz" + ] + }, + { + "ID": "dpkg@1.22.22", + "Name": "dpkg", + "Identifier": { + "PURL": "pkg:deb/debian/dpkg@1.22.22?arch=amd64\u0026distro=debian-13.6", + "UID": "d88bc872d04d38e8" + }, + "Version": "1.22.22", + "Arch": "amd64", + "SrcName": "dpkg", + "SrcVersion": "1.22.22", + "Licenses": [ + "GPL-2.0-or-later", + "public-domain-s-s-d", + "GPL-2.0-only" + ], + "Maintainer": "Dpkg Developers \u003cdebian-dpkg@lists.debian.org\u003e", + "DependsOn": [ + "tar@1.35+dfsg-3.1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/dpkg", + "/usr/bin/dpkg-deb", + "/usr/bin/dpkg-divert", + "/usr/bin/dpkg-maintscript-helper", + "/usr/bin/dpkg-query", + "/usr/bin/dpkg-realpath", + "/usr/bin/dpkg-split", + "/usr/bin/dpkg-statoverride", + "/usr/bin/dpkg-trigger", + "/usr/bin/update-alternatives", + "/usr/lib/systemd/system/dpkg-db-backup.service", + "/usr/lib/systemd/system/dpkg-db-backup.timer", + "/usr/libexec/dpkg/dpkg-db-backup", + "/usr/libexec/dpkg/dpkg-db-keeper", + "/usr/sbin/start-stop-daemon", + "/usr/share/doc/dpkg/AUTHORS", + "/usr/share/doc/dpkg/README.api", + "/usr/share/doc/dpkg/README.bug-usertags.gz", + "/usr/share/doc/dpkg/README.feature-removal-schedule.gz", + "/usr/share/doc/dpkg/THANKS.gz", + "/usr/share/doc/dpkg/changelog.gz", + "/usr/share/doc/dpkg/copyright", + "/usr/share/dpkg/abitable", + "/usr/share/dpkg/cputable", + "/usr/share/dpkg/ostable", + "/usr/share/dpkg/sh/dpkg-error.sh", + "/usr/share/dpkg/tupletable", + "/usr/share/lintian/overrides/dpkg", + "/usr/share/lintian/profiles/dpkg/main.profile", + "/usr/share/locale/ast/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/bs/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ca/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/cs/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/da/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/de/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/dz/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/el/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/eo/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/es/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/et/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/eu/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/fr/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/gl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/hu/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/id/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/it/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ja/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/km/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ko/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ku/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/lt/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/mr/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/nb/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ne/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/nl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/nn/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/oc/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pa/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pt/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ro/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ru/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/sk/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/sv/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/th/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/tl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/tr/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/vi/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/dpkg.mo", + "/usr/share/man/de/man1/dpkg-deb.1.gz", + "/usr/share/man/de/man1/dpkg-divert.1.gz", + "/usr/share/man/de/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/de/man1/dpkg-query.1.gz", + "/usr/share/man/de/man1/dpkg-realpath.1.gz", + "/usr/share/man/de/man1/dpkg-split.1.gz", + "/usr/share/man/de/man1/dpkg-statoverride.1.gz", + "/usr/share/man/de/man1/dpkg-trigger.1.gz", + "/usr/share/man/de/man1/dpkg.1.gz", + "/usr/share/man/de/man1/update-alternatives.1.gz", + "/usr/share/man/de/man5/dpkg.cfg.5.gz", + "/usr/share/man/de/man8/start-stop-daemon.8.gz", + "/usr/share/man/es/man5/dpkg.cfg.5.gz", + "/usr/share/man/fr/man1/dpkg-divert.1.gz", + "/usr/share/man/fr/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/fr/man1/dpkg-query.1.gz", + "/usr/share/man/fr/man1/dpkg-realpath.1.gz", + "/usr/share/man/fr/man1/dpkg-split.1.gz", + "/usr/share/man/fr/man1/dpkg-trigger.1.gz", + "/usr/share/man/fr/man1/update-alternatives.1.gz", + "/usr/share/man/fr/man5/dpkg.cfg.5.gz", + "/usr/share/man/fr/man8/start-stop-daemon.8.gz", + "/usr/share/man/it/man5/dpkg.cfg.5.gz", + "/usr/share/man/ja/man5/dpkg.cfg.5.gz", + "/usr/share/man/man1/dpkg-deb.1.gz", + "/usr/share/man/man1/dpkg-divert.1.gz", + "/usr/share/man/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/man1/dpkg-query.1.gz", + "/usr/share/man/man1/dpkg-realpath.1.gz", + "/usr/share/man/man1/dpkg-split.1.gz", + "/usr/share/man/man1/dpkg-statoverride.1.gz", + "/usr/share/man/man1/dpkg-trigger.1.gz", + "/usr/share/man/man1/dpkg.1.gz", + "/usr/share/man/man1/update-alternatives.1.gz", + "/usr/share/man/man5/dpkg.cfg.5.gz", + "/usr/share/man/man8/start-stop-daemon.8.gz", + "/usr/share/man/nl/man1/dpkg-deb.1.gz", + "/usr/share/man/nl/man1/dpkg-divert.1.gz", + "/usr/share/man/nl/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/nl/man1/dpkg-query.1.gz", + "/usr/share/man/nl/man1/dpkg-realpath.1.gz", + "/usr/share/man/nl/man1/dpkg-split.1.gz", + "/usr/share/man/nl/man1/dpkg-statoverride.1.gz", + "/usr/share/man/nl/man1/dpkg-trigger.1.gz", + "/usr/share/man/nl/man1/dpkg.1.gz", + "/usr/share/man/nl/man1/update-alternatives.1.gz", + "/usr/share/man/nl/man5/dpkg.cfg.5.gz", + "/usr/share/man/nl/man8/start-stop-daemon.8.gz", + "/usr/share/man/pl/man5/dpkg.cfg.5.gz", + "/usr/share/man/pt/man1/dpkg-deb.1.gz", + "/usr/share/man/pt/man1/dpkg-divert.1.gz", + "/usr/share/man/pt/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/pt/man1/dpkg-query.1.gz", + "/usr/share/man/pt/man1/dpkg-realpath.1.gz", + "/usr/share/man/pt/man1/dpkg-split.1.gz", + "/usr/share/man/pt/man1/dpkg-statoverride.1.gz", + "/usr/share/man/pt/man1/dpkg-trigger.1.gz", + "/usr/share/man/pt/man1/dpkg.1.gz", + "/usr/share/man/pt/man1/update-alternatives.1.gz", + "/usr/share/man/pt/man5/dpkg.cfg.5.gz", + "/usr/share/man/pt/man8/start-stop-daemon.8.gz", + "/usr/share/man/sv/man1/dpkg-deb.1.gz", + "/usr/share/man/sv/man1/dpkg-divert.1.gz", + "/usr/share/man/sv/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/sv/man1/dpkg-query.1.gz", + "/usr/share/man/sv/man1/dpkg-realpath.1.gz", + "/usr/share/man/sv/man1/dpkg-split.1.gz", + "/usr/share/man/sv/man1/dpkg-statoverride.1.gz", + "/usr/share/man/sv/man1/dpkg-trigger.1.gz", + "/usr/share/man/sv/man1/dpkg.1.gz", + "/usr/share/man/sv/man1/update-alternatives.1.gz", + "/usr/share/man/sv/man5/dpkg.cfg.5.gz", + "/usr/share/man/sv/man8/start-stop-daemon.8.gz", + "/usr/share/polkit-1/actions/org.dpkg.pkexec.update-alternatives.policy" + ] + }, + { + "ID": "findutils@4.10.0-3", + "Name": "findutils", + "Identifier": { + "PURL": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64\u0026distro=debian-13.6", + "UID": "111949a4800741f1" + }, + "Version": "4.10.0", + "Release": "3", + "Arch": "amd64", + "SrcName": "findutils", + "SrcVersion": "4.10.0", + "SrcRelease": "3", + "Licenses": [ + "GFDL-1.3-no-invariants-or-later", + "GPL-3.0-or-later", + "FSFAP", + "GPL-2+ with Autoconf-data exception", + "GPL-3+ with Autoconf-data exception", + "FSFULLR", + "GPL-2.0-or-later", + "X11", + "public-domain", + "LGPL-2.1-or-later", + "GPL with automake exception", + "LGPL-2.0-or-later", + "LGPL-3.0-or-later", + "BSD-3-Clause", + "GPL-3+ with Bison-2.2 exception", + "LGPL-3.0-only", + "ISC", + "GFDL-1.3-only", + "GPL-2.0-only", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Andreas Metzler \u003cametzler@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/find", + "/usr/bin/xargs", + "/usr/share/doc-base/findutils.findutils", + "/usr/share/doc/findutils/NEWS.gz", + "/usr/share/doc/findutils/README.gz", + "/usr/share/doc/findutils/TODO", + "/usr/share/doc/findutils/changelog.Debian.gz", + "/usr/share/doc/findutils/changelog.gz", + "/usr/share/doc/findutils/copyright", + "/usr/share/info/find-maint.info.gz", + "/usr/share/info/find.info.gz", + "/usr/share/locale/be/LC_MESSAGES/findutils.mo", + "/usr/share/locale/bg/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ca/LC_MESSAGES/findutils.mo", + "/usr/share/locale/cs/LC_MESSAGES/findutils.mo", + "/usr/share/locale/da/LC_MESSAGES/findutils.mo", + "/usr/share/locale/de/LC_MESSAGES/findutils.mo", + "/usr/share/locale/el/LC_MESSAGES/findutils.mo", + "/usr/share/locale/eo/LC_MESSAGES/findutils.mo", + "/usr/share/locale/es/LC_MESSAGES/findutils.mo", + "/usr/share/locale/et/LC_MESSAGES/findutils.mo", + "/usr/share/locale/fi/LC_MESSAGES/findutils.mo", + "/usr/share/locale/fr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ga/LC_MESSAGES/findutils.mo", + "/usr/share/locale/gl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/hr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/hu/LC_MESSAGES/findutils.mo", + "/usr/share/locale/id/LC_MESSAGES/findutils.mo", + "/usr/share/locale/it/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ja/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ka/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ko/LC_MESSAGES/findutils.mo", + "/usr/share/locale/lg/LC_MESSAGES/findutils.mo", + "/usr/share/locale/lt/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ms/LC_MESSAGES/findutils.mo", + "/usr/share/locale/nb/LC_MESSAGES/findutils.mo", + "/usr/share/locale/nl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/pl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/pt/LC_MESSAGES/findutils.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ro/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ru/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sk/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sv/LC_MESSAGES/findutils.mo", + "/usr/share/locale/tr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/uk/LC_MESSAGES/findutils.mo", + "/usr/share/locale/vi/LC_MESSAGES/findutils.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/findutils.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/findutils.mo", + "/usr/share/man/man1/find.1.gz", + "/usr/share/man/man1/xargs.1.gz" + ] + }, + { + "ID": "gcc-14-base@14.2.0-19", + "Name": "gcc-14-base", + "Identifier": { + "PURL": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64\u0026distro=debian-13.6", + "UID": "a2d64c6b5f038075" + }, + "Version": "14.2.0", + "Release": "19", + "Arch": "amd64", + "SrcName": "gcc-14", + "SrcVersion": "14.2.0", + "SrcRelease": "19", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-3.0-only", + "GFDL-1.2-only", + "Artistic-2.0", + "LGPL-2.0-or-later" + ], + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/gcc-14-base/README.Debian.amd64.gz", + "/usr/share/doc/gcc-14-base/TODO.Debian", + "/usr/share/doc/gcc-14-base/changelog.Debian.gz", + "/usr/share/doc/gcc-14-base/copyright" + ] + }, + { + "ID": "grep@3.11-4", + "Name": "grep", + "Identifier": { + "PURL": "pkg:deb/debian/grep@3.11-4?arch=amd64\u0026distro=debian-13.6", + "UID": "d450e0ea7fae458f" + }, + "Version": "3.11", + "Release": "4", + "Arch": "amd64", + "SrcName": "grep", + "SrcVersion": "3.11", + "SrcRelease": "4", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/egrep", + "/usr/bin/fgrep", + "/usr/bin/grep", + "/usr/bin/rgrep", + "/usr/share/doc/grep/AUTHORS", + "/usr/share/doc/grep/NEWS.Debian.gz", + "/usr/share/doc/grep/NEWS.gz", + "/usr/share/doc/grep/README", + "/usr/share/doc/grep/THANKS.gz", + "/usr/share/doc/grep/TODO.gz", + "/usr/share/doc/grep/changelog.Debian.gz", + "/usr/share/doc/grep/changelog.gz", + "/usr/share/doc/grep/copyright", + "/usr/share/info/grep.info.gz", + "/usr/share/locale/af/LC_MESSAGES/grep.mo", + "/usr/share/locale/be/LC_MESSAGES/grep.mo", + "/usr/share/locale/bg/LC_MESSAGES/grep.mo", + "/usr/share/locale/ca/LC_MESSAGES/grep.mo", + "/usr/share/locale/cs/LC_MESSAGES/grep.mo", + "/usr/share/locale/da/LC_MESSAGES/grep.mo", + "/usr/share/locale/de/LC_MESSAGES/grep.mo", + "/usr/share/locale/el/LC_MESSAGES/grep.mo", + "/usr/share/locale/eo/LC_MESSAGES/grep.mo", + "/usr/share/locale/es/LC_MESSAGES/grep.mo", + "/usr/share/locale/et/LC_MESSAGES/grep.mo", + "/usr/share/locale/eu/LC_MESSAGES/grep.mo", + "/usr/share/locale/fi/LC_MESSAGES/grep.mo", + "/usr/share/locale/fr/LC_MESSAGES/grep.mo", + "/usr/share/locale/ga/LC_MESSAGES/grep.mo", + "/usr/share/locale/gl/LC_MESSAGES/grep.mo", + "/usr/share/locale/he/LC_MESSAGES/grep.mo", + "/usr/share/locale/hr/LC_MESSAGES/grep.mo", + "/usr/share/locale/hu/LC_MESSAGES/grep.mo", + "/usr/share/locale/id/LC_MESSAGES/grep.mo", + "/usr/share/locale/it/LC_MESSAGES/grep.mo", + "/usr/share/locale/ja/LC_MESSAGES/grep.mo", + "/usr/share/locale/ka/LC_MESSAGES/grep.mo", + "/usr/share/locale/ko/LC_MESSAGES/grep.mo", + "/usr/share/locale/ky/LC_MESSAGES/grep.mo", + "/usr/share/locale/lt/LC_MESSAGES/grep.mo", + "/usr/share/locale/nb/LC_MESSAGES/grep.mo", + "/usr/share/locale/nl/LC_MESSAGES/grep.mo", + "/usr/share/locale/pa/LC_MESSAGES/grep.mo", + "/usr/share/locale/pl/LC_MESSAGES/grep.mo", + "/usr/share/locale/pt/LC_MESSAGES/grep.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/grep.mo", + "/usr/share/locale/ro/LC_MESSAGES/grep.mo", + "/usr/share/locale/ru/LC_MESSAGES/grep.mo", + "/usr/share/locale/sk/LC_MESSAGES/grep.mo", + "/usr/share/locale/sl/LC_MESSAGES/grep.mo", + "/usr/share/locale/sr/LC_MESSAGES/grep.mo", + "/usr/share/locale/sv/LC_MESSAGES/grep.mo", + "/usr/share/locale/ta/LC_MESSAGES/grep.mo", + "/usr/share/locale/th/LC_MESSAGES/grep.mo", + "/usr/share/locale/tr/LC_MESSAGES/grep.mo", + "/usr/share/locale/uk/LC_MESSAGES/grep.mo", + "/usr/share/locale/vi/LC_MESSAGES/grep.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/grep.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/grep.mo", + "/usr/share/man/man1/grep.1.gz" + ] + }, + { + "ID": "gzip@1.13-1", + "Name": "gzip", + "Identifier": { + "PURL": "pkg:deb/debian/gzip@1.13-1?arch=amd64\u0026distro=debian-13.6", + "UID": "60254b2bea6a1f09" + }, + "Version": "1.13", + "Release": "1", + "Arch": "amd64", + "SrcName": "gzip", + "SrcVersion": "1.13", + "SrcRelease": "1", + "Licenses": [ + "GPL-3.0-or-later", + "GFDL-1.3+-no-invariant", + "FSF-manpages", + "GPL-3.0-only", + "GFDL-3" + ], + "Maintainer": "Milan Kupcevic \u003cmilan@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/gunzip", + "/usr/bin/gzexe", + "/usr/bin/gzip", + "/usr/bin/zcat", + "/usr/bin/zcmp", + "/usr/bin/zdiff", + "/usr/bin/zegrep", + "/usr/bin/zfgrep", + "/usr/bin/zforce", + "/usr/bin/zgrep", + "/usr/bin/zless", + "/usr/bin/zmore", + "/usr/bin/znew", + "/usr/share/doc/gzip/NEWS.gz", + "/usr/share/doc/gzip/README.gz", + "/usr/share/doc/gzip/TODO", + "/usr/share/doc/gzip/changelog.Debian.gz", + "/usr/share/doc/gzip/changelog.gz", + "/usr/share/doc/gzip/copyright", + "/usr/share/info/gzip.info.gz", + "/usr/share/man/man1/gzexe.1.gz", + "/usr/share/man/man1/gzip.1.gz", + "/usr/share/man/man1/zdiff.1.gz", + "/usr/share/man/man1/zforce.1.gz", + "/usr/share/man/man1/zgrep.1.gz", + "/usr/share/man/man1/zless.1.gz", + "/usr/share/man/man1/zmore.1.gz", + "/usr/share/man/man1/znew.1.gz" + ] + }, + { + "ID": "hostname@3.25", + "Name": "hostname", + "Identifier": { + "PURL": "pkg:deb/debian/hostname@3.25?arch=amd64\u0026distro=debian-13.6", + "UID": "87ef62957d44b2c" + }, + "Version": "3.25", + "Arch": "amd64", + "SrcName": "hostname", + "SrcVersion": "3.25", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Michael Meskes \u003cmeskes@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/hostname", + "/usr/share/doc/hostname/changelog.gz", + "/usr/share/doc/hostname/copyright", + "/usr/share/man/man1/hostname.1.gz" + ] + }, + { + "ID": "init-system-helpers@1.69~deb13u1", + "Name": "init-system-helpers", + "Identifier": { + "PURL": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all\u0026distro=debian-13.6", + "UID": "317be9d9c6744acd" + }, + "Version": "1.69~deb13u1", + "Arch": "all", + "SrcName": "init-system-helpers", + "SrcVersion": "1.69~deb13u1", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/deb-systemd-helper", + "/usr/bin/deb-systemd-invoke", + "/usr/sbin/invoke-rc.d", + "/usr/sbin/service", + "/usr/sbin/update-rc.d", + "/usr/share/bug/init-system-helpers/control", + "/usr/share/doc/init-system-helpers/README.invoke-rc.d.gz", + "/usr/share/doc/init-system-helpers/README.policy-rc.d.gz", + "/usr/share/doc/init-system-helpers/changelog.gz", + "/usr/share/doc/init-system-helpers/copyright", + "/usr/share/lintian/overrides/init-system-helpers", + "/usr/share/man/man1/deb-systemd-helper.1p.gz", + "/usr/share/man/man1/deb-systemd-invoke.1p.gz", + "/usr/share/man/man8/invoke-rc.d.8.gz", + "/usr/share/man/man8/service.8.gz", + "/usr/share/man/man8/update-rc.d.8.gz" + ] + }, + { + "ID": "keyutils-libs@1.5.8-3.el7", + "Name": "keyutils-libs", + "Identifier": { + "PURL": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7", + "UID": "93c3119deb6a93bf", + "BOMRef": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9" + }, + "Version": "1.5.8", + "Release": "3.el7", + "SrcName": "keyutils-libs", + "SrcVersion": "1.5.8", + "SrcRelease": "3.el7", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "krb5-libs@1.15.1-55.el7_9", + "Name": "krb5-libs", + "Identifier": { + "PURL": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9", + "UID": "b0fe59dc84398982", + "BOMRef": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#5b1da461e2c57feebadb3f96f47736f4b17ad56a83895f95d60a166ced6472b0" + }, + "Version": "1.15.1", + "Release": "55.el7_9", + "SrcName": "krb5-libs", + "SrcVersion": "1.15.1", + "SrcRelease": "55.el7_9", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "libacl1@2.3.2-2+b1", + "Name": "libacl1", + "Identifier": { + "PURL": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64\u0026distro=debian-13.6", + "UID": "82548f6f7baf25f3" + }, + "Version": "2.3.2", + "Release": "2+b1", + "Arch": "amd64", + "SrcName": "acl", + "SrcVersion": "2.3.2", + "SrcRelease": "2", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2302", + "/usr/share/doc/libacl1/changelog.Debian.amd64.gz", + "/usr/share/doc/libacl1/changelog.Debian.gz", + "/usr/share/doc/libacl1/changelog.gz", + "/usr/share/doc/libacl1/copyright", + "/usr/share/lintian/overrides/libacl1" + ] + }, + { + "ID": "libapt-pkg7.0@3.0.3", + "Name": "libapt-pkg7.0", + "Identifier": { + "PURL": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64\u0026distro=debian-13.6", + "UID": "df08f16788e2e8a6" + }, + "Version": "3.0.3", + "Arch": "amd64", + "SrcName": "apt", + "SrcVersion": "3.0.3", + "Licenses": [ + "GPL-2.0-or-later", + "curl", + "BSD-3-Clause", + "MIT", + "GPL-2.0-only" + ], + "Maintainer": "APT Development Team \u003cdeity@lists.debian.org\u003e", + "DependsOn": [ + "libbz2-1.0@1.0.8-6", + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19", + "liblz4-1@1.10.0-4", + "liblzma5@5.8.1-1+deb13u1", + "libssl3t64@3.5.6-1~deb13u2", + "libstdc++6@14.2.0-19", + "libsystemd0@257.13-1~deb13u1", + "libudev1@257.13-1~deb13u1", + "libxxhash0@0.8.3-2", + "libzstd1@1.5.7+dfsg-1", + "zlib1g@1:1.3.dfsg+really1.3.1-1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libapt-pkg.so.7.0.0", + "/usr/share/doc/libapt-pkg7.0/NEWS.Debian.gz", + "/usr/share/doc/libapt-pkg7.0/changelog.gz", + "/usr/share/doc/libapt-pkg7.0/copyright", + "/usr/share/locale/ar/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ast/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/bg/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/bs/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ca/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/cs/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/cy/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/da/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/de/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/dz/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/el/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/es/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/eu/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/fi/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/fr/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/gl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/hu/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/it/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ja/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/km/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ko/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ku/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/lt/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/mr/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/nb/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ne/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/nl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/nn/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/pl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/pt/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ro/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ru/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/sk/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/sl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/sv/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/th/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/tl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/tr/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/uk/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/vi/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/libapt-pkg7.0.mo" + ] + }, + { + "ID": "libattr1@1:2.5.2-3", + "Name": "libattr1", + "Identifier": { + "PURL": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "953f1d1395118b4b" + }, + "Version": "2.5.2", + "Release": "3", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "attr", + "SrcVersion": "2.5.2", + "SrcRelease": "3", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libattr.so.1.1.2502", + "/usr/share/doc/libattr1/changelog.Debian.gz", + "/usr/share/doc/libattr1/changelog.gz", + "/usr/share/doc/libattr1/copyright", + "/usr/share/lintian/overrides/libattr1" + ] + }, + { + "ID": "libaudit-common@1:4.0.2-2", + "Name": "libaudit-common", + "Identifier": { + "PURL": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all\u0026distro=debian-13.6\u0026epoch=1", + "UID": "4845289e49197cbd" + }, + "Version": "4.0.2", + "Release": "2", + "Epoch": 1, + "Arch": "all", + "SrcName": "audit", + "SrcVersion": "4.0.2", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.1-only", + "GPL-1.0-only" + ], + "Maintainer": "Laurent Bigonville \u003cbigon@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/libaudit-common/changelog.Debian.gz", + "/usr/share/doc/libaudit-common/changelog.gz", + "/usr/share/doc/libaudit-common/copyright", + "/usr/share/man/man5/libaudit.conf.5.gz" + ] + }, + { + "ID": "libaudit1@1:4.0.2-2+b2", + "Name": "libaudit1", + "Identifier": { + "PURL": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "c9ebadb6608e2305" + }, + "Version": "4.0.2", + "Release": "2+b2", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "audit", + "SrcVersion": "4.0.2", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.1-only", + "GPL-1.0-only" + ], + "Maintainer": "Laurent Bigonville \u003cbigon@debian.org\u003e", + "DependsOn": [ + "libaudit-common@1:4.0.2-2", + "libc6@2.41-12+deb13u3", + "libcap-ng0@0.8.5-4+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libaudit.so.1.0.0", + "/usr/share/doc/libaudit1/changelog.Debian.amd64.gz", + "/usr/share/doc/libaudit1/changelog.Debian.gz", + "/usr/share/doc/libaudit1/changelog.gz", + "/usr/share/doc/libaudit1/copyright" + ] + }, + { + "ID": "libblkid1@2.41-5", + "Name": "libblkid1", + "Identifier": { + "PURL": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "c427952a98b1e3ee" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libblkid.so.1.1.0", + "/usr/share/doc/libblkid1/NEWS.Debian.gz", + "/usr/share/doc/libblkid1/changelog.Debian.gz", + "/usr/share/doc/libblkid1/changelog.gz", + "/usr/share/doc/libblkid1/copyright", + "/usr/share/lintian/overrides/libblkid1" + ] + }, + { + "ID": "libbsd0@0.12.2-2", + "Name": "libbsd0", + "Identifier": { + "PURL": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64\u0026distro=debian-13.6", + "UID": "6a35c140077b4eb4" + }, + "Version": "0.12.2", + "Release": "2", + "Arch": "amd64", + "SrcName": "libbsd", + "SrcVersion": "0.12.2", + "SrcRelease": "2", + "Licenses": [ + "BSD-3-Clause", + "BSD-3-clause-Regents", + "BSD-2-Clause-NetBSD", + "BSD-3-clause-author", + "BSD-3-clause-John-Birrell", + "BSD-5-clause-Peter-Wemm", + "BSD-2-Clause", + "BSD-2-clause-verbatim", + "BSD-2-clause-author", + "ISC", + "ISC-Original", + "MIT", + "public-domain", + "Beerware" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libmd0@1.1.0-2+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libbsd.so.0.12.2", + "/usr/share/doc/libbsd0/changelog.Debian.gz", + "/usr/share/doc/libbsd0/changelog.gz", + "/usr/share/doc/libbsd0/copyright", + "/usr/share/lintian/overrides/libbsd0" + ] + }, + { + "ID": "libbz2-1.0@1.0.8-6", + "Name": "libbz2-1.0", + "Identifier": { + "PURL": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64\u0026distro=debian-13.6", + "UID": "395e7ab13e254394" + }, + "Version": "1.0.8", + "Release": "6", + "Arch": "amd64", + "SrcName": "bzip2", + "SrcVersion": "1.0.8", + "SrcRelease": "6", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libbz2.so.1.0.4", + "/usr/share/doc/libbz2-1.0/changelog.Debian.gz", + "/usr/share/doc/libbz2-1.0/changelog.gz", + "/usr/share/doc/libbz2-1.0/copyright" + ] + }, + { + "ID": "libc-bin@2.41-12+deb13u3", + "Name": "libc-bin", + "Identifier": { + "PURL": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64\u0026distro=debian-13.6", + "UID": "c17717cca0e61621" + }, + "Version": "2.41", + "Release": "12+deb13u3", + "Arch": "amd64", + "SrcName": "glibc", + "SrcVersion": "2.41", + "SrcRelease": "12+deb13u3", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.0-or-later", + "LGPL-2.1+-with-link-exception", + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "GPL-2+-with-link-exception", + "GPL-2.0-only", + "GPL-3.0-or-later", + "FSFAP", + "Carnegie", + "Inner-Net", + "MIT-like-Lord", + "BSD-like-Spencer", + "PCRE", + "BSD-3-clause-Carnegie", + "Unicode-DFS-2016", + "BSL-1.0", + "SunPro", + "CORE-MATH", + "BSD-3-clause-Berkeley", + "BSD-3-clause-WIDE", + "BSD-2-Clause", + "BSD-3-clause-Oracle", + "DEC", + "IBM", + "ISC", + "Univ-Coimbra", + "public-domain", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/getconf", + "/usr/bin/getent", + "/usr/bin/iconv", + "/usr/bin/ldd", + "/usr/bin/locale", + "/usr/bin/localedef", + "/usr/bin/pldd", + "/usr/bin/tzselect", + "/usr/bin/zdump", + "/usr/lib/locale/C.utf8/LC_ADDRESS", + "/usr/lib/locale/C.utf8/LC_COLLATE", + "/usr/lib/locale/C.utf8/LC_CTYPE", + "/usr/lib/locale/C.utf8/LC_IDENTIFICATION", + "/usr/lib/locale/C.utf8/LC_MEASUREMENT", + "/usr/lib/locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES", + "/usr/lib/locale/C.utf8/LC_MONETARY", + "/usr/lib/locale/C.utf8/LC_NAME", + "/usr/lib/locale/C.utf8/LC_NUMERIC", + "/usr/lib/locale/C.utf8/LC_PAPER", + "/usr/lib/locale/C.utf8/LC_TELEPHONE", + "/usr/lib/locale/C.utf8/LC_TIME", + "/usr/sbin/iconvconfig", + "/usr/sbin/ldconfig", + "/usr/sbin/zic", + "/usr/share/doc/libc-bin/changelog.Debian.gz", + "/usr/share/doc/libc-bin/changelog.gz", + "/usr/share/doc/libc-bin/copyright", + "/usr/share/libc-bin/nsswitch.conf", + "/usr/share/lintian/overrides/libc-bin", + "/usr/share/man/man1/getconf.1.gz", + "/usr/share/man/man1/tzselect.1.gz" + ] + }, + { + "ID": "libc6@2.41-12+deb13u3", + "Name": "libc6", + "Identifier": { + "PURL": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64\u0026distro=debian-13.6", + "UID": "19d151c4d1229080" + }, + "Version": "2.41", + "Release": "12+deb13u3", + "Arch": "amd64", + "SrcName": "glibc", + "SrcVersion": "2.41", + "SrcRelease": "12+deb13u3", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.0-or-later", + "LGPL-2.1+-with-link-exception", + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "GPL-2+-with-link-exception", + "GPL-2.0-only", + "GPL-3.0-or-later", + "FSFAP", + "Carnegie", + "Inner-Net", + "MIT-like-Lord", + "BSD-like-Spencer", + "PCRE", + "BSD-3-clause-Carnegie", + "Unicode-DFS-2016", + "BSL-1.0", + "SunPro", + "CORE-MATH", + "BSD-3-clause-Berkeley", + "BSD-3-clause-WIDE", + "BSD-2-Clause", + "BSD-3-clause-Oracle", + "DEC", + "IBM", + "ISC", + "Univ-Coimbra", + "public-domain", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "DependsOn": [ + "libgcc-s1@14.2.0-19" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/gconv/ANSI_X3.110.so", + "/usr/lib/x86_64-linux-gnu/gconv/ARMSCII-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/ASMO_449.so", + "/usr/lib/x86_64-linux-gnu/gconv/BIG5.so", + "/usr/lib/x86_64-linux-gnu/gconv/BIG5HKSCS.so", + "/usr/lib/x86_64-linux-gnu/gconv/BRF.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP10007.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1125.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1250.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1251.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1252.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1253.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1254.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1255.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1256.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1257.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1258.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP737.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP770.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP771.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP772.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP773.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP774.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP775.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP932.so", + "/usr/lib/x86_64-linux-gnu/gconv/CSN_369103.so", + "/usr/lib/x86_64-linux-gnu/gconv/CWI.so", + "/usr/lib/x86_64-linux-gnu/gconv/DEC-MCS.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-AT-DE-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-AT-DE.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-CA-FR.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-DK-NO-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-DK-NO.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-ES-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-ES-S.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-ES.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-FI-SE-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-FI-SE.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-FR.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-IS-FRISS.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-IT.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-PT.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-UK.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-US.so", + "/usr/lib/x86_64-linux-gnu/gconv/ECMA-CYRILLIC.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-CN.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-JISX0213.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-JP-MS.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-JP.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-KR.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-TW.so", + "/usr/lib/x86_64-linux-gnu/gconv/GB18030.so", + "/usr/lib/x86_64-linux-gnu/gconv/GBBIG5.so", + "/usr/lib/x86_64-linux-gnu/gconv/GBGBK.so", + "/usr/lib/x86_64-linux-gnu/gconv/GBK.so", + "/usr/lib/x86_64-linux-gnu/gconv/GEORGIAN-ACADEMY.so", + "/usr/lib/x86_64-linux-gnu/gconv/GEORGIAN-PS.so", + "/usr/lib/x86_64-linux-gnu/gconv/GOST_19768-74.so", + "/usr/lib/x86_64-linux-gnu/gconv/GREEK-CCITT.so", + "/usr/lib/x86_64-linux-gnu/gconv/GREEK7-OLD.so", + "/usr/lib/x86_64-linux-gnu/gconv/GREEK7.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-GREEK8.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-ROMAN8.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-ROMAN9.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-THAI8.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-TURKISH8.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM037.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM038.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1004.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1008.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1008_420.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1025.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1026.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1046.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1047.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1097.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1112.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1122.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1123.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1124.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1129.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1130.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1132.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1133.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1137.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1140.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1141.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1142.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1143.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1144.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1145.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1146.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1147.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1148.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1149.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1153.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1154.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1155.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1156.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1157.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1158.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1160.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1161.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1162.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1163.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1164.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1166.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1167.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM12712.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1364.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1371.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1388.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1390.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1399.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM16804.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM256.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM273.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM274.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM275.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM277.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM278.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM280.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM281.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM284.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM285.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM290.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM297.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM420.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM423.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM424.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM437.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4517.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4899.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4909.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4971.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM500.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM5347.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM803.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM850.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM851.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM852.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM855.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM856.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM857.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM858.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM860.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM861.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM862.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM863.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM864.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM865.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM866.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM866NAV.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM868.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM869.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM870.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM871.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM874.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM875.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM880.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM891.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM901.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM902.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM903.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM9030.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM904.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM905.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM9066.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM918.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM921.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM922.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM930.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM932.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM933.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM935.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM937.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM939.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM943.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM9448.so", + "/usr/lib/x86_64-linux-gnu/gconv/IEC_P27-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/INIS-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/INIS-CYRILLIC.so", + "/usr/lib/x86_64-linux-gnu/gconv/INIS.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISIRI-3342.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-CN-EXT.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-CN.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-JP-3.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-JP.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-KR.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-IR-197.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-IR-209.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO646.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-10.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-11.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-13.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-14.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-15.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-16.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-2.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-3.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-4.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-5.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-6.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-7.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-9.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-9E.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_10367-BOX.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_11548-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_2033.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_5427-EXT.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_5427.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_5428.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_6937-2.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_6937.so", + "/usr/lib/x86_64-linux-gnu/gconv/JOHAB.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-R.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-RU.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-T.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-U.so", + "/usr/lib/x86_64-linux-gnu/gconv/LATIN-GREEK-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/LATIN-GREEK.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-CENTRALEUROPE.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-IS.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-SAMI.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-UK.so", + "/usr/lib/x86_64-linux-gnu/gconv/MACINTOSH.so", + "/usr/lib/x86_64-linux-gnu/gconv/MIK.so", + "/usr/lib/x86_64-linux-gnu/gconv/NATS-DANO.so", + "/usr/lib/x86_64-linux-gnu/gconv/NATS-SEFI.so", + "/usr/lib/x86_64-linux-gnu/gconv/PT154.so", + "/usr/lib/x86_64-linux-gnu/gconv/RK1048.so", + "/usr/lib/x86_64-linux-gnu/gconv/SAMI-WS2.so", + "/usr/lib/x86_64-linux-gnu/gconv/SHIFT_JISX0213.so", + "/usr/lib/x86_64-linux-gnu/gconv/SJIS.so", + "/usr/lib/x86_64-linux-gnu/gconv/T.61.so", + "/usr/lib/x86_64-linux-gnu/gconv/TCVN5712-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/TIS-620.so", + "/usr/lib/x86_64-linux-gnu/gconv/TSCII.so", + "/usr/lib/x86_64-linux-gnu/gconv/UHC.so", + "/usr/lib/x86_64-linux-gnu/gconv/UNICODE.so", + "/usr/lib/x86_64-linux-gnu/gconv/UTF-16.so", + "/usr/lib/x86_64-linux-gnu/gconv/UTF-32.so", + "/usr/lib/x86_64-linux-gnu/gconv/UTF-7.so", + "/usr/lib/x86_64-linux-gnu/gconv/VISCII.so", + "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules", + "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", + "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.d/gconv-modules-extra.conf", + "/usr/lib/x86_64-linux-gnu/gconv/libCNS.so", + "/usr/lib/x86_64-linux-gnu/gconv/libGB.so", + "/usr/lib/x86_64-linux-gnu/gconv/libISOIR165.so", + "/usr/lib/x86_64-linux-gnu/gconv/libJIS.so", + "/usr/lib/x86_64-linux-gnu/gconv/libJISX0213.so", + "/usr/lib/x86_64-linux-gnu/gconv/libKSC.so", + "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + "/usr/lib/x86_64-linux-gnu/libBrokenLocale.so.1", + "/usr/lib/x86_64-linux-gnu/libanl.so.1", + "/usr/lib/x86_64-linux-gnu/libc.so.6", + "/usr/lib/x86_64-linux-gnu/libc_malloc_debug.so.0", + "/usr/lib/x86_64-linux-gnu/libdl.so.2", + "/usr/lib/x86_64-linux-gnu/libm.so.6", + "/usr/lib/x86_64-linux-gnu/libmemusage.so", + "/usr/lib/x86_64-linux-gnu/libmvec.so.1", + "/usr/lib/x86_64-linux-gnu/libnsl.so.1", + "/usr/lib/x86_64-linux-gnu/libnss_compat.so.2", + "/usr/lib/x86_64-linux-gnu/libnss_dns.so.2", + "/usr/lib/x86_64-linux-gnu/libnss_files.so.2", + "/usr/lib/x86_64-linux-gnu/libnss_hesiod.so.2", + "/usr/lib/x86_64-linux-gnu/libpcprofile.so", + "/usr/lib/x86_64-linux-gnu/libpthread.so.0", + "/usr/lib/x86_64-linux-gnu/libresolv.so.2", + "/usr/lib/x86_64-linux-gnu/librt.so.1", + "/usr/lib/x86_64-linux-gnu/libthread_db.so.1", + "/usr/lib/x86_64-linux-gnu/libutil.so.1", + "/usr/share/doc/libc6/NEWS.Debian.gz", + "/usr/share/doc/libc6/NEWS.gz", + "/usr/share/doc/libc6/README.Debian.gz", + "/usr/share/doc/libc6/README.hesiod.gz", + "/usr/share/doc/libc6/changelog.Debian.gz", + "/usr/share/doc/libc6/changelog.gz", + "/usr/share/doc/libc6/copyright", + "/usr/share/lintian/overrides/libc6" + ] + }, + { + "ID": "libcap-ng0@0.8.5-4+b1", + "Name": "libcap-ng0", + "Identifier": { + "PURL": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64\u0026distro=debian-13.6", + "UID": "853c3a4f587b1e09" + }, + "Version": "0.8.5", + "Release": "4+b1", + "Arch": "amd64", + "SrcName": "libcap-ng", + "SrcVersion": "0.8.5", + "SrcRelease": "4", + "Licenses": [ + "LGPL-2.1-or-later", + "GPL-2.0-or-later", + "GPL-3.0-only", + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Håvard F. Aasen \u003chavard.f.aasen@pfft.no\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libcap-ng.so.0.0.0", + "/usr/lib/x86_64-linux-gnu/libdrop_ambient.so.0.0.0", + "/usr/share/doc/libcap-ng0/changelog.Debian.amd64.gz", + "/usr/share/doc/libcap-ng0/changelog.Debian.gz", + "/usr/share/doc/libcap-ng0/changelog.gz", + "/usr/share/doc/libcap-ng0/copyright" + ] + }, + { + "ID": "libcap2@1:2.75-10+deb13u1+b1", + "Name": "libcap2", + "Identifier": { + "PURL": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "451a510e42b50c7c" + }, + "Version": "2.75", + "Release": "10+deb13u1+b1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "libcap2", + "SrcVersion": "2.75", + "SrcRelease": "10+deb13u1", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Christian Kastner \u003cckk@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libcap.so.2.75", + "/usr/lib/x86_64-linux-gnu/libpsx.so.2.75", + "/usr/share/doc/libcap2/changelog.Debian.amd64.gz", + "/usr/share/doc/libcap2/changelog.Debian.gz", + "/usr/share/doc/libcap2/changelog.gz", + "/usr/share/doc/libcap2/copyright" + ] + }, + { + "ID": "libcom_err@1.42.9-19.el7", + "Name": "libcom_err", + "Identifier": { + "PURL": "pkg:rpm/centos/libcom_err@1.42.9-19.el7", + "UID": "dd13ac5974bf1c3f", + "BOMRef": "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce" + }, + "Version": "1.42.9", + "Release": "19.el7", + "SrcName": "libcom_err", + "SrcVersion": "1.42.9", + "SrcRelease": "19.el7", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "libcrypt1@1:4.4.38-1", + "Name": "libcrypt1", + "Identifier": { + "PURL": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "185d1b457ef399b8" + }, + "Version": "4.4.38", + "Release": "1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "libxcrypt", + "SrcVersion": "4.4.38", + "SrcRelease": "1", + "SrcEpoch": 1, + "Maintainer": "Marco d'Itri \u003cmd@linux.it\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libcrypt.so.1.1.0", + "/usr/share/doc/libcrypt1/changelog.Debian.gz", + "/usr/share/doc/libcrypt1/changelog.gz", + "/usr/share/doc/libcrypt1/copyright" + ] + }, + { + "ID": "libdb5.3t64@5.3.28+dfsg2-9", + "Name": "libdb5.3t64", + "Identifier": { + "PURL": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64\u0026distro=debian-13.6", + "UID": "1d93101a053d025d" + }, + "Version": "5.3.28+dfsg2", + "Release": "9", + "Arch": "amd64", + "SrcName": "db5.3", + "SrcVersion": "5.3.28+dfsg2", + "SrcRelease": "9", + "Licenses": [ + "Sleepycat", + "BSD-3-Clause", + "MS-PL", + "GPL-2.0-or-later", + "Artistic-2.0", + "X11", + "MIT-old", + "TCL-like", + "BSD-3-clause-fjord", + "GPL-3.0-only", + "Zlib" + ], + "Maintainer": "Debian QA Group \u003cpackages@qa.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libdb-5.3.so", + "/usr/share/doc/libdb5.3t64/build_signature_amd64.txt", + "/usr/share/doc/libdb5.3t64/changelog.Debian.gz", + "/usr/share/doc/libdb5.3t64/copyright", + "/usr/share/lintian/overrides/libdb5.3t64" + ] + }, + { + "ID": "libdebconfclient0@0.280", + "Name": "libdebconfclient0", + "Identifier": { + "PURL": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64\u0026distro=debian-13.6", + "UID": "f4d35c54ea8ebcdc" + }, + "Version": "0.280", + "Arch": "amd64", + "SrcName": "cdebconf", + "SrcVersion": "0.280", + "Licenses": [ + "BSD-2-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian Install System Team \u003cdebian-boot@lists.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libdebconfclient.so.0.0.0", + "/usr/share/doc/libdebconfclient0/changelog.gz", + "/usr/share/doc/libdebconfclient0/copyright" + ] + }, + { + "ID": "libffi8@3.4.8-2", + "Name": "libffi8", + "Identifier": { + "PURL": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64\u0026distro=debian-13.6", + "UID": "d78c144a996938aa" + }, + "Version": "3.4.8", + "Release": "2", + "Arch": "amd64", + "SrcName": "libffi", + "SrcVersion": "3.4.8", + "SrcRelease": "2", + "Licenses": [ + "MIT", + "X11", + "GPL-2.0-or-later", + "GPL-3.0-or-later", + "MPL-1.1", + "LGPL-2.1-or-later", + "public-domain" + ], + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libffi.so.8.1.4", + "/usr/share/doc/libffi8/changelog.Debian.gz", + "/usr/share/doc/libffi8/copyright" + ] + }, + { + "ID": "libgcc-s1@14.2.0-19", + "Name": "libgcc-s1", + "Identifier": { + "PURL": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64\u0026distro=debian-13.6", + "UID": "a939ab1a9133b3fa" + }, + "Version": "14.2.0", + "Release": "19", + "Arch": "amd64", + "SrcName": "gcc-14", + "SrcVersion": "14.2.0", + "SrcRelease": "19", + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "DependsOn": [ + "gcc-14-base@14.2.0-19", + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libgcc_s.so.1", + "/usr/share/lintian/overrides/libgcc-s1" + ] + }, + { + "ID": "libgdbm6t64@1.24-2", + "Name": "libgdbm6t64", + "Identifier": { + "PURL": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64\u0026distro=debian-13.6", + "UID": "79fdad8a6bd05b2d" + }, + "Version": "1.24", + "Release": "2", + "Arch": "amd64", + "SrcName": "gdbm", + "SrcVersion": "1.24", + "SrcRelease": "2", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-2.0-or-later", + "GFDL-1.3-no-invariants-or-later", + "GPL-3.0-only", + "GPL-2.0-only" + ], + "Maintainer": "Nicolas Mora \u003cbabelouest@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libgdbm.so.6.0.0", + "/usr/share/doc/libgdbm6t64/changelog.Debian.gz", + "/usr/share/doc/libgdbm6t64/changelog.gz", + "/usr/share/doc/libgdbm6t64/copyright", + "/usr/share/lintian/overrides/libgdbm6t64" + ] + }, + { + "ID": "libgmp10@2:6.3.0+dfsg-3", + "Name": "libgmp10", + "Identifier": { + "PURL": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64\u0026distro=debian-13.6\u0026epoch=2", + "UID": "8d1eefd06321d7f5" + }, + "Version": "6.3.0+dfsg", + "Release": "3", + "Epoch": 2, + "Arch": "amd64", + "SrcName": "gmp", + "SrcVersion": "6.3.0+dfsg", + "SrcRelease": "3", + "SrcEpoch": 2, + "Licenses": [ + "GPL-2.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-or-later", + "GPL-3+ with Bison exception", + "GPL-2.0-only", + "GPL-3.0-only", + "LGPL-3.0-only" + ], + "Maintainer": "Debian Science Maintainers \u003cdebian-science-maintainers@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libgmp.so.10.5.0", + "/usr/share/doc/libgmp10/README.Debian", + "/usr/share/doc/libgmp10/changelog.Debian.gz", + "/usr/share/doc/libgmp10/changelog.gz", + "/usr/share/doc/libgmp10/copyright" + ] + }, + { + "ID": "libhogweed6t64@3.10.1-1", + "Name": "libhogweed6t64", + "Identifier": { + "PURL": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "8c7c2e41ab9a40c8" + }, + "Version": "3.10.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "nettle", + "SrcVersion": "3.10.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "MIT", + "GPL-3.0-with-autoconf-exception+", + "public-domain", + "GPL-2.0-only", + "GAP" + ], + "Maintainer": "Magnus Holmgren \u003cholmgren@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libgmp10@2:6.3.0+dfsg-3", + "libnettle8t64@3.10.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libhogweed.so.6.10", + "/usr/share/doc/libhogweed6t64/changelog.Debian.gz", + "/usr/share/doc/libhogweed6t64/changelog.gz", + "/usr/share/doc/libhogweed6t64/copyright", + "/usr/share/lintian/overrides/libhogweed6t64" + ] + }, + { + "ID": "liblastlog2-2@2.41-5", + "Name": "liblastlog2-2", + "Identifier": { + "PURL": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "d5ec9bb1797e1476" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libsqlite3-0@3.46.1-7+deb13u1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/liblastlog2.so.2.0.0", + "/usr/share/doc/liblastlog2-2/NEWS.Debian.gz", + "/usr/share/doc/liblastlog2-2/changelog.Debian.gz", + "/usr/share/doc/liblastlog2-2/changelog.gz", + "/usr/share/doc/liblastlog2-2/copyright" + ] + }, + { + "ID": "liblz4-1@1.10.0-4", + "Name": "liblz4-1", + "Identifier": { + "PURL": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64\u0026distro=debian-13.6", + "UID": "d06629bc3067545a" + }, + "Version": "1.10.0", + "Release": "4", + "Arch": "amd64", + "SrcName": "lz4", + "SrcVersion": "1.10.0", + "SrcRelease": "4", + "Licenses": [ + "GPL-2.0-or-later", + "BSD-2-Clause", + "GPL-2.0-only" + ], + "Maintainer": "Nobuhiro Iwamatsu \u003ciwamatsu@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libxxhash0@0.8.3-2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/liblz4.so.1.10.0", + "/usr/share/doc/liblz4-1/changelog.Debian.gz", + "/usr/share/doc/liblz4-1/copyright" + ] + }, + { + "ID": "liblzma5@5.8.1-1+deb13u1", + "Name": "liblzma5", + "Identifier": { + "PURL": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "6bb07f060c067c08" + }, + "Version": "5.8.1", + "Release": "1+deb13u1", + "Arch": "amd64", + "SrcName": "xz-utils", + "SrcVersion": "5.8.1", + "SrcRelease": "1+deb13u1", + "Licenses": [ + "0BSD", + "GPL-2.0-or-later", + "LGPL-2.1-or-later", + "FSFULLR", + "GPL-3.0-or-later-WITH-Autoconf-exception-macro", + "none", + "PD", + "permissive-nowarranty", + "FSFUL", + "noderivs", + "PD-debian", + "LGPL-2.1-only", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "Maintainer": "Sebastian Andrzej Siewior \u003csebastian@breakpoint.cc\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/liblzma.so.5.8.1", + "/usr/share/doc/liblzma5/AUTHORS", + "/usr/share/doc/liblzma5/NEWS.gz", + "/usr/share/doc/liblzma5/THANKS.gz", + "/usr/share/doc/liblzma5/changelog.Debian.gz", + "/usr/share/doc/liblzma5/changelog.gz", + "/usr/share/doc/liblzma5/copyright" + ] + }, + { + "ID": "libmd0@1.1.0-2+b1", + "Name": "libmd0", + "Identifier": { + "PURL": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64\u0026distro=debian-13.6", + "UID": "8f7242077c74e850" + }, + "Version": "1.1.0", + "Release": "2+b1", + "Arch": "amd64", + "SrcName": "libmd", + "SrcVersion": "1.1.0", + "SrcRelease": "2", + "Licenses": [ + "BSD-3-Clause", + "BSD-3-clause-Aaron-D-Gifford", + "BSD-2-Clause", + "BSD-2-Clause-NetBSD", + "ISC", + "Beerware", + "public-domain-md4", + "public-domain-md5", + "public-domain-sha1" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libmd.so.0.1.0", + "/usr/share/doc/libmd0/changelog.Debian.amd64.gz", + "/usr/share/doc/libmd0/changelog.Debian.gz", + "/usr/share/doc/libmd0/changelog.gz", + "/usr/share/doc/libmd0/copyright" + ] + }, + { + "ID": "libmount1@2.41-5", + "Name": "libmount1", + "Identifier": { + "PURL": "pkg:deb/debian/libmount1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "e4ddbeb1b284f3ca" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libblkid1@2.41-5", + "libc6@2.41-12+deb13u3", + "libselinux1@3.8.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libmount.so.1.1.0", + "/usr/share/doc/libmount1/NEWS.Debian.gz", + "/usr/share/doc/libmount1/changelog.Debian.gz", + "/usr/share/doc/libmount1/changelog.gz", + "/usr/share/doc/libmount1/copyright", + "/usr/share/lintian/overrides/libmount1" + ] + }, + { + "ID": "libncursesw6@6.5+20250216-2", + "Name": "libncursesw6", + "Identifier": { + "PURL": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64\u0026distro=debian-13.6", + "UID": "fac6fddb91f7c21c" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libtinfo6@6.5+20250216-2" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libformw.so.6.5", + "/usr/lib/x86_64-linux-gnu/libmenuw.so.6.5", + "/usr/lib/x86_64-linux-gnu/libncursesw.so.6.5", + "/usr/lib/x86_64-linux-gnu/libpanelw.so.6.5" + ] + }, + { + "ID": "libnettle8t64@3.10.1-1", + "Name": "libnettle8t64", + "Identifier": { + "PURL": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "3cd10b6383088c9" + }, + "Version": "3.10.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "nettle", + "SrcVersion": "3.10.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "MIT", + "GPL-3.0-with-autoconf-exception+", + "public-domain", + "GPL-2.0-only", + "GAP" + ], + "Maintainer": "Magnus Holmgren \u003cholmgren@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libnettle.so.8.10", + "/usr/share/doc/libnettle8t64/NEWS.gz", + "/usr/share/doc/libnettle8t64/README", + "/usr/share/doc/libnettle8t64/changelog.Debian.gz", + "/usr/share/doc/libnettle8t64/changelog.gz", + "/usr/share/doc/libnettle8t64/copyright", + "/usr/share/lintian/overrides/libnettle8t64" + ] + }, + { + "ID": "libpam-modules@1.7.0-5", + "Name": "libpam-modules", + "Identifier": { + "PURL": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64\u0026distro=debian-13.6", + "UID": "b9d6f9c66558c40d" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/security/pam_access.so", + "/usr/lib/x86_64-linux-gnu/security/pam_canonicalize_user.so", + "/usr/lib/x86_64-linux-gnu/security/pam_debug.so", + "/usr/lib/x86_64-linux-gnu/security/pam_deny.so", + "/usr/lib/x86_64-linux-gnu/security/pam_echo.so", + "/usr/lib/x86_64-linux-gnu/security/pam_env.so", + "/usr/lib/x86_64-linux-gnu/security/pam_exec.so", + "/usr/lib/x86_64-linux-gnu/security/pam_faildelay.so", + "/usr/lib/x86_64-linux-gnu/security/pam_faillock.so", + "/usr/lib/x86_64-linux-gnu/security/pam_filter.so", + "/usr/lib/x86_64-linux-gnu/security/pam_ftp.so", + "/usr/lib/x86_64-linux-gnu/security/pam_group.so", + "/usr/lib/x86_64-linux-gnu/security/pam_issue.so", + "/usr/lib/x86_64-linux-gnu/security/pam_keyinit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_limits.so", + "/usr/lib/x86_64-linux-gnu/security/pam_listfile.so", + "/usr/lib/x86_64-linux-gnu/security/pam_localuser.so", + "/usr/lib/x86_64-linux-gnu/security/pam_loginuid.so", + "/usr/lib/x86_64-linux-gnu/security/pam_mail.so", + "/usr/lib/x86_64-linux-gnu/security/pam_mkhomedir.so", + "/usr/lib/x86_64-linux-gnu/security/pam_motd.so", + "/usr/lib/x86_64-linux-gnu/security/pam_namespace.so", + "/usr/lib/x86_64-linux-gnu/security/pam_nologin.so", + "/usr/lib/x86_64-linux-gnu/security/pam_permit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_pwhistory.so", + "/usr/lib/x86_64-linux-gnu/security/pam_rhosts.so", + "/usr/lib/x86_64-linux-gnu/security/pam_rootok.so", + "/usr/lib/x86_64-linux-gnu/security/pam_securetty.so", + "/usr/lib/x86_64-linux-gnu/security/pam_selinux.so", + "/usr/lib/x86_64-linux-gnu/security/pam_sepermit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_setquota.so", + "/usr/lib/x86_64-linux-gnu/security/pam_shells.so", + "/usr/lib/x86_64-linux-gnu/security/pam_stress.so", + "/usr/lib/x86_64-linux-gnu/security/pam_succeed_if.so", + "/usr/lib/x86_64-linux-gnu/security/pam_time.so", + "/usr/lib/x86_64-linux-gnu/security/pam_timestamp.so", + "/usr/lib/x86_64-linux-gnu/security/pam_tty_audit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_umask.so", + "/usr/lib/x86_64-linux-gnu/security/pam_unix.so", + "/usr/lib/x86_64-linux-gnu/security/pam_userdb.so", + "/usr/lib/x86_64-linux-gnu/security/pam_usertype.so", + "/usr/lib/x86_64-linux-gnu/security/pam_warn.so", + "/usr/lib/x86_64-linux-gnu/security/pam_wheel.so", + "/usr/lib/x86_64-linux-gnu/security/pam_xauth.so", + "/usr/share/doc/libpam-modules/NEWS.Debian.gz", + "/usr/share/doc/libpam-modules/changelog.Debian.gz", + "/usr/share/doc/libpam-modules/changelog.gz", + "/usr/share/doc/libpam-modules/copyright", + "/usr/share/doc/libpam-modules/examples/upperLOWER.c", + "/usr/share/lintian/overrides/libpam-modules", + "/usr/share/pam-configs/mkhomedir" + ] + }, + { + "ID": "libpam-modules-bin@1.7.0-5", + "Name": "libpam-modules-bin", + "Identifier": { + "PURL": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64\u0026distro=debian-13.6", + "UID": "f047dc4396624183" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "DependsOn": [ + "libaudit1@1:4.0.2-2+b2", + "libc6@2.41-12+deb13u3", + "libcrypt1@1:4.4.38-1", + "libpam0g@1.7.0-5", + "libselinux1@3.8.1-1", + "libsystemd0@257.13-1~deb13u1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/systemd/system/pam_namespace.service", + "/usr/sbin/faillock", + "/usr/sbin/mkhomedir_helper", + "/usr/sbin/pam_namespace_helper", + "/usr/sbin/pam_timestamp_check", + "/usr/sbin/pwhistory_helper", + "/usr/sbin/unix_chkpwd", + "/usr/sbin/unix_update", + "/usr/share/doc/libpam-modules-bin/changelog.Debian.gz", + "/usr/share/doc/libpam-modules-bin/changelog.gz", + "/usr/share/doc/libpam-modules-bin/copyright", + "/usr/share/lintian/overrides/libpam-modules-bin" + ] + }, + { + "ID": "libpam-runtime@1.7.0-5", + "Name": "libpam-runtime", + "Identifier": { + "PURL": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all\u0026distro=debian-13.6", + "UID": "2e8bd19930283d52" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "all", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91", + "libpam-modules@1.7.0-5" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/sbin/pam-auth-update", + "/usr/sbin/pam_getenv", + "/usr/share/doc/libpam-runtime/changelog.Debian.gz", + "/usr/share/doc/libpam-runtime/changelog.gz", + "/usr/share/doc/libpam-runtime/copyright", + "/usr/share/lintian/overrides/libpam-runtime", + "/usr/share/locale/af/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/am/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ar/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/as/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/az/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/be/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bg/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bn_IN/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bs/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ca/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/cs/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/cy/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/da/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/de/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/de_CH/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/el/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/eo/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/es/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/et/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/eu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/fa/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/fi/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/fr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ga/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/gl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/gu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/he/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/hi/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/hr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/hu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ia/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/id/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/is/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/it/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ja/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ka/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/kk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/km/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/kn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ko/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/kw_GB/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ky/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/lt/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/lv/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/mk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ml/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/mn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/mr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ms/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/my/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/nb/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ne/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/nl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/nn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/or/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pa/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pt/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ro/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ru/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/si/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sq/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sr@latin/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sv/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ta/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/te/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/tg/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/th/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/tr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/uk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ur/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/vi/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/yo/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zh_HK/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/man/man5/access.conf.5.gz", + "/usr/share/man/man5/faillock.conf.5.gz", + "/usr/share/man/man5/group.conf.5.gz", + "/usr/share/man/man5/limits.conf.5.gz", + "/usr/share/man/man5/namespace.conf.5.gz", + "/usr/share/man/man5/pam.conf.5.gz", + "/usr/share/man/man5/pam_env.conf.5.gz", + "/usr/share/man/man5/pwhistory.conf.5.gz", + "/usr/share/man/man5/sepermit.conf.5.gz", + "/usr/share/man/man5/time.conf.5.gz", + "/usr/share/man/man7/PAM.7.gz", + "/usr/share/man/man8/faillock.8.gz", + "/usr/share/man/man8/mkhomedir_helper.8.gz", + "/usr/share/man/man8/pam-auth-update.8.gz", + "/usr/share/man/man8/pam_access.8.gz", + "/usr/share/man/man8/pam_canonicalize_user.8.gz", + "/usr/share/man/man8/pam_debug.8.gz", + "/usr/share/man/man8/pam_deny.8.gz", + "/usr/share/man/man8/pam_echo.8.gz", + "/usr/share/man/man8/pam_env.8.gz", + "/usr/share/man/man8/pam_exec.8.gz", + "/usr/share/man/man8/pam_faildelay.8.gz", + "/usr/share/man/man8/pam_faillock.8.gz", + "/usr/share/man/man8/pam_filter.8.gz", + "/usr/share/man/man8/pam_ftp.8.gz", + "/usr/share/man/man8/pam_getenv.8.gz", + "/usr/share/man/man8/pam_group.8.gz", + "/usr/share/man/man8/pam_issue.8.gz", + "/usr/share/man/man8/pam_keyinit.8.gz", + "/usr/share/man/man8/pam_limits.8.gz", + "/usr/share/man/man8/pam_listfile.8.gz", + "/usr/share/man/man8/pam_localuser.8.gz", + "/usr/share/man/man8/pam_loginuid.8.gz", + "/usr/share/man/man8/pam_mail.8.gz", + "/usr/share/man/man8/pam_mkhomedir.8.gz", + "/usr/share/man/man8/pam_motd.8.gz", + "/usr/share/man/man8/pam_namespace.8.gz", + "/usr/share/man/man8/pam_namespace_helper.8.gz", + "/usr/share/man/man8/pam_nologin.8.gz", + "/usr/share/man/man8/pam_permit.8.gz", + "/usr/share/man/man8/pam_pwhistory.8.gz", + "/usr/share/man/man8/pam_rhosts.8.gz", + "/usr/share/man/man8/pam_rootok.8.gz", + "/usr/share/man/man8/pam_securetty.8.gz", + "/usr/share/man/man8/pam_selinux.8.gz", + "/usr/share/man/man8/pam_sepermit.8.gz", + "/usr/share/man/man8/pam_setquota.8.gz", + "/usr/share/man/man8/pam_shells.8.gz", + "/usr/share/man/man8/pam_stress.8.gz", + "/usr/share/man/man8/pam_succeed_if.8.gz", + "/usr/share/man/man8/pam_time.8.gz", + "/usr/share/man/man8/pam_timestamp.8.gz", + "/usr/share/man/man8/pam_timestamp_check.8.gz", + "/usr/share/man/man8/pam_tty_audit.8.gz", + "/usr/share/man/man8/pam_umask.8.gz", + "/usr/share/man/man8/pam_unix.8.gz", + "/usr/share/man/man8/pam_userdb.8.gz", + "/usr/share/man/man8/pam_usertype.8.gz", + "/usr/share/man/man8/pam_warn.8.gz", + "/usr/share/man/man8/pam_wheel.8.gz", + "/usr/share/man/man8/pam_xauth.8.gz", + "/usr/share/man/man8/pwhistory_helper.8.gz", + "/usr/share/man/man8/unix_chkpwd.8.gz", + "/usr/share/man/man8/unix_update.8.gz", + "/usr/share/pam-configs/unix", + "/usr/share/pam/common-account", + "/usr/share/pam/common-account.md5sums", + "/usr/share/pam/common-auth", + "/usr/share/pam/common-auth.md5sums", + "/usr/share/pam/common-password", + "/usr/share/pam/common-password.md5sums", + "/usr/share/pam/common-session", + "/usr/share/pam/common-session-noninteractive", + "/usr/share/pam/common-session-noninteractive.md5sums", + "/usr/share/pam/common-session.md5sums" + ] + }, + { + "ID": "libpam0g@1.7.0-5", + "Name": "libpam0g", + "Identifier": { + "PURL": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64\u0026distro=debian-13.6", + "UID": "5dbda12bb939f426" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91", + "libaudit1@1:4.0.2-2+b2", + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libpam.so.0.85.1", + "/usr/lib/x86_64-linux-gnu/libpam_misc.so.0.82.1", + "/usr/lib/x86_64-linux-gnu/libpamc.so.0.82.1", + "/usr/share/doc/libpam0g/Debian-PAM-MiniPolicy.gz", + "/usr/share/doc/libpam0g/README", + "/usr/share/doc/libpam0g/README.Debian", + "/usr/share/doc/libpam0g/TODO.Debian", + "/usr/share/doc/libpam0g/changelog.Debian.gz", + "/usr/share/doc/libpam0g/changelog.gz", + "/usr/share/doc/libpam0g/copyright", + "/usr/share/lintian/overrides/libpam0g" + ] + }, + { + "ID": "libpcre2-8-0@10.46-1~deb13u1", + "Name": "libpcre2-8-0", + "Identifier": { + "PURL": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "cb52bbc65534e04d" + }, + "Version": "10.46", + "Release": "1~deb13u1", + "Arch": "amd64", + "SrcName": "pcre2", + "SrcVersion": "10.46", + "SrcRelease": "1~deb13u1", + "Licenses": [ + "BSD-3-clause-Cambridge with BINARY LIBRARY-LIKE PACKAGES exception", + "BSD-3-Clause", + "X11", + "BSD-2-Clause", + "public-domain" + ], + "Maintainer": "Matthew Vernon \u003cmatthew@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0.14.0", + "/usr/share/doc/libpcre2-8-0/README.Debian", + "/usr/share/doc/libpcre2-8-0/changelog.Debian.gz", + "/usr/share/doc/libpcre2-8-0/changelog.gz", + "/usr/share/doc/libpcre2-8-0/copyright" + ] + }, + { + "ID": "libreadline8t64@8.2-6", + "Name": "libreadline8t64", + "Identifier": { + "PURL": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64\u0026distro=debian-13.6", + "UID": "3182860c74d6d08d" + }, + "Version": "8.2", + "Release": "6", + "Arch": "amd64", + "SrcName": "readline", + "SrcVersion": "8.2", + "SrcRelease": "6", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only", + "GFDL-1.3-no-invariants-or-later", + "GFDL-1.3-or-later", + "ISC-no-attribution" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libtinfo6@6.5+20250216-2", + "readline-common@8.2-6" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libhistory.so.8.2", + "/usr/lib/x86_64-linux-gnu/libreadline.so.8.2", + "/usr/share/doc/libreadline8t64/README.Debian", + "/usr/share/doc/libreadline8t64/USAGE", + "/usr/share/doc/libreadline8t64/changelog.Debian.gz", + "/usr/share/doc/libreadline8t64/changelog.gz", + "/usr/share/doc/libreadline8t64/copyright", + "/usr/share/doc/libreadline8t64/examples/Inputrc", + "/usr/share/doc/libreadline8t64/inputrc.arrows" + ] + }, + { + "ID": "libseccomp2@2.6.0-2", + "Name": "libseccomp2", + "Identifier": { + "PURL": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64\u0026distro=debian-13.6", + "UID": "872fd916e9134574" + }, + "Version": "2.6.0", + "Release": "2", + "Arch": "amd64", + "SrcName": "libseccomp", + "SrcVersion": "2.6.0", + "SrcRelease": "2", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "Kees Cook \u003ckees@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libseccomp.so.2.6.0", + "/usr/share/doc/libseccomp2/changelog.Debian.gz", + "/usr/share/doc/libseccomp2/changelog.gz", + "/usr/share/doc/libseccomp2/copyright" + ] + }, + { + "ID": "libselinux@2.5-15.el7", + "Name": "libselinux", + "Identifier": { + "PURL": "pkg:rpm/centos/libselinux@2.5-15.el7", + "UID": "6e37f06bf3057b37", + "BOMRef": "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed" + }, + "Version": "2.5", + "Release": "15.el7", + "SrcName": "libselinux", + "SrcVersion": "2.5", + "SrcRelease": "15.el7", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "libselinux1@3.8.1-1", + "Name": "libselinux1", + "Identifier": { + "PURL": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "dad36e826c972129" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "libselinux", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "public-domain", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libpcre2-8-0@10.46-1~deb13u1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/tmpfiles.d/libselinux1.conf", + "/usr/lib/x86_64-linux-gnu/libselinux.so.1", + "/usr/share/doc/libselinux1/changelog.Debian.gz", + "/usr/share/doc/libselinux1/copyright" + ] + }, + { + "ID": "libsemanage-common@3.8.1-1", + "Name": "libsemanage-common", + "Identifier": { + "PURL": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all\u0026distro=debian-13.6", + "UID": "82e27fcff653c8e2" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "all", + "SrcName": "libsemanage", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/libsemanage-common/changelog.Debian.gz", + "/usr/share/doc/libsemanage-common/copyright", + "/usr/share/man/man5/semanage.conf.5.gz" + ] + }, + { + "ID": "libsemanage2@3.8.1-1", + "Name": "libsemanage2", + "Identifier": { + "PURL": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "dc362d6ee87a25c7" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "libsemanage", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libaudit1@1:4.0.2-2+b2", + "libbz2-1.0@1.0.8-6", + "libc6@2.41-12+deb13u3", + "libselinux1@3.8.1-1", + "libsemanage-common@3.8.1-1", + "libsepol2@3.8.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsemanage.so.2", + "/usr/share/doc/libsemanage2/changelog.Debian.gz", + "/usr/share/doc/libsemanage2/copyright" + ] + }, + { + "ID": "libsepol2@3.8.1-1", + "Name": "libsepol2", + "Identifier": { + "PURL": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "8a467f0e7ce023a5" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "libsepol", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "Zlib", + "GPL-2.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsepol.so.2", + "/usr/share/doc/libsepol2/changelog.Debian.gz", + "/usr/share/doc/libsepol2/copyright" + ] + }, + { + "ID": "libsmartcols1@2.41-5", + "Name": "libsmartcols1", + "Identifier": { + "PURL": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "4d10e734941c1d1d" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsmartcols.so.1.1.0", + "/usr/share/doc/libsmartcols1/NEWS.Debian.gz", + "/usr/share/doc/libsmartcols1/changelog.Debian.gz", + "/usr/share/doc/libsmartcols1/changelog.gz", + "/usr/share/doc/libsmartcols1/copyright", + "/usr/share/lintian/overrides/libsmartcols1" + ] + }, + { + "ID": "libsqlite3-0@3.46.1-7+deb13u1", + "Name": "libsqlite3-0", + "Identifier": { + "PURL": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "49853bfc5d923fa8" + }, + "Version": "3.46.1", + "Release": "7+deb13u1", + "Arch": "amd64", + "SrcName": "sqlite3", + "SrcVersion": "3.46.1", + "SrcRelease": "7+deb13u1", + "Licenses": [ + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Laszlo Boszormenyi (GCS) \u003cgcs@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6", + "/usr/share/doc/libsqlite3-0/README.Debian", + "/usr/share/doc/libsqlite3-0/changelog.Debian.gz", + "/usr/share/doc/libsqlite3-0/changelog.gz", + "/usr/share/doc/libsqlite3-0/changelog.html.gz", + "/usr/share/doc/libsqlite3-0/copyright" + ] + }, + { + "ID": "libssl3t64@3.5.6-1~deb13u2", + "Name": "libssl3t64", + "Identifier": { + "PURL": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64\u0026distro=debian-13.6", + "UID": "1ca83fc0831fc12a" + }, + "Version": "3.5.6", + "Release": "1~deb13u2", + "Arch": "amd64", + "SrcName": "openssl", + "SrcVersion": "3.5.6", + "SrcRelease": "1~deb13u2", + "Licenses": [ + "Apache-2.0", + "Artistic-2.0", + "GPL-1.0-or-later", + "GPL-1.0-only" + ], + "Maintainer": "Debian OpenSSL Team \u003cpkg-openssl-devel@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libzstd1@1.5.7+dfsg-1", + "openssl-provider-legacy@3.5.6-1~deb13u2", + "zlib1g@1:1.3.dfsg+really1.3.1-1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/engines-3/afalg.so", + "/usr/lib/x86_64-linux-gnu/engines-3/loader_attic.so", + "/usr/lib/x86_64-linux-gnu/engines-3/padlock.so", + "/usr/lib/x86_64-linux-gnu/libcrypto.so.3", + "/usr/lib/x86_64-linux-gnu/libssl.so.3", + "/usr/share/doc/libssl3t64/NEWS.Debian.gz", + "/usr/share/doc/libssl3t64/changelog.Debian.gz", + "/usr/share/doc/libssl3t64/changelog.gz", + "/usr/share/doc/libssl3t64/copyright", + "/usr/share/lintian/overrides/libssl3t64" + ] + }, + { + "ID": "libstdc++6@14.2.0-19", + "Name": "libstdc++6", + "Identifier": { + "PURL": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64\u0026distro=debian-13.6", + "UID": "44612415c1730efa" + }, + "Version": "14.2.0", + "Release": "19", + "Arch": "amd64", + "SrcName": "gcc-14", + "SrcVersion": "14.2.0", + "SrcRelease": "19", + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "DependsOn": [ + "gcc-14-base@14.2.0-19", + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33", + "/usr/share/gcc/python/libstdcxx/__init__.py", + "/usr/share/gcc/python/libstdcxx/v6/__init__.py", + "/usr/share/gcc/python/libstdcxx/v6/printers.py", + "/usr/share/gcc/python/libstdcxx/v6/xmethods.py", + "/usr/share/gdb/auto-load/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33-gdb.py" + ] + }, + { + "ID": "libsystemd0@257.13-1~deb13u1", + "Name": "libsystemd0", + "Identifier": { + "PURL": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "d7f6e0bcc5fd6683" + }, + "Version": "257.13", + "Release": "1~deb13u1", + "Arch": "amd64", + "SrcName": "systemd", + "SrcVersion": "257.13", + "SrcRelease": "1~deb13u1", + "Licenses": [ + "LGPL-2.1-or-later", + "CC0-1.0", + "GPL-2 with Linux-syscall-note exception", + "MIT", + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libcap2@1:2.75-10+deb13u1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsystemd.so.0.40.0", + "/usr/share/doc/libsystemd0/NEWS.Debian.gz", + "/usr/share/doc/libsystemd0/changelog.Debian.gz", + "/usr/share/doc/libsystemd0/copyright" + ] + }, + { + "ID": "libtinfo6@6.5+20250216-2", + "Name": "libtinfo6", + "Identifier": { + "PURL": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64\u0026distro=debian-13.6", + "UID": "12095a35cca3541c" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Licenses": [ + "MIT/X11", + "X11", + "BSD-3-Clause" + ], + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libtic.so.6.5", + "/usr/lib/x86_64-linux-gnu/libtinfo.so.6.5", + "/usr/share/doc/libtinfo6/changelog.Debian.gz", + "/usr/share/doc/libtinfo6/changelog.gz", + "/usr/share/doc/libtinfo6/copyright" + ] + }, + { + "ID": "libudev1@257.13-1~deb13u1", + "Name": "libudev1", + "Identifier": { + "PURL": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "151b8b6a80baf5ce" + }, + "Version": "257.13", + "Release": "1~deb13u1", + "Arch": "amd64", + "SrcName": "systemd", + "SrcVersion": "257.13", + "SrcRelease": "1~deb13u1", + "Licenses": [ + "LGPL-2.1-or-later", + "CC0-1.0", + "GPL-2 with Linux-syscall-note exception", + "MIT", + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libcap2@1:2.75-10+deb13u1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libudev.so.1.7.10", + "/usr/share/doc/libudev1/NEWS.Debian.gz", + "/usr/share/doc/libudev1/changelog.Debian.gz", + "/usr/share/doc/libudev1/copyright" + ] + }, + { + "ID": "libuuid1@2.41-5", + "Name": "libuuid1", + "Identifier": { + "PURL": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "ad45ef419bdcd798" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libuuid.so.1.3.0", + "/usr/share/doc/libuuid1/NEWS.Debian.gz", + "/usr/share/doc/libuuid1/changelog.Debian.gz", + "/usr/share/doc/libuuid1/changelog.gz", + "/usr/share/doc/libuuid1/copyright" + ] + }, + { + "ID": "libxxhash0@0.8.3-2", + "Name": "libxxhash0", + "Identifier": { + "PURL": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64\u0026distro=debian-13.6", + "UID": "413b9b44940ce169" + }, + "Version": "0.8.3", + "Release": "2", + "Arch": "amd64", + "SrcName": "xxhash", + "SrcVersion": "0.8.3", + "SrcRelease": "2", + "Licenses": [ + "BSD-2-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Josue Ortega \u003cjosue@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libxxhash.so.0.8.3", + "/usr/share/doc/libxxhash0/changelog.Debian.gz", + "/usr/share/doc/libxxhash0/changelog.gz", + "/usr/share/doc/libxxhash0/copyright" + ] + }, + { + "ID": "libzstd1@1.5.7+dfsg-1", + "Name": "libzstd1", + "Identifier": { + "PURL": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64\u0026distro=debian-13.6", + "UID": "7262601866572971" + }, + "Version": "1.5.7+dfsg", + "Release": "1", + "Arch": "amd64", + "SrcName": "libzstd", + "SrcVersion": "1.5.7+dfsg", + "SrcRelease": "1", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "Zlib", + "MIT" + ], + "Maintainer": "RPM packaging team \u003cteam+pkg-rpm@tracker.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7", + "/usr/share/doc/libzstd1/changelog.Debian.gz", + "/usr/share/doc/libzstd1/changelog.gz", + "/usr/share/doc/libzstd1/copyright" + ] + }, + { + "ID": "login@1:4.16.0-2+really2.41-5", + "Name": "login", + "Identifier": { + "PURL": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "468f3d4a374ef1" + }, + "Version": "4.16.0-2+really2.41", + "Release": "5", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libaudit1@1:4.0.2-2+b2", + "libc6@2.41-12+deb13u3", + "libcrypt1@1:4.4.38-1", + "libpam-modules@1.7.0-5", + "libpam-runtime@1.7.0-5", + "libpam0g@1.7.0-5" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/login", + "/usr/bin/newgrp", + "/usr/sbin/nologin", + "/usr/share/bash-completion/completions/newgrp", + "/usr/share/doc/login/NEWS.Debian.gz", + "/usr/share/doc/login/changelog.Debian.gz", + "/usr/share/doc/login/changelog.gz", + "/usr/share/doc/login/copyright", + "/usr/share/lintian/overrides/login", + "/usr/share/man/de/man1/login.1.gz", + "/usr/share/man/de/man8/nologin.8.gz", + "/usr/share/man/fr/man1/login.1.gz", + "/usr/share/man/man1/login.1.gz", + "/usr/share/man/man1/newgrp.1.gz", + "/usr/share/man/man8/nologin.8.gz", + "/usr/share/man/pl/man1/login.1.gz", + "/usr/share/man/pl/man1/newgrp.1.gz", + "/usr/share/man/pl/man8/nologin.8.gz", + "/usr/share/man/ro/man1/login.1.gz", + "/usr/share/man/ro/man1/newgrp.1.gz", + "/usr/share/man/ro/man8/nologin.8.gz", + "/usr/share/man/sr/man1/login.1.gz", + "/usr/share/man/sr/man8/nologin.8.gz", + "/usr/share/man/uk/man1/login.1.gz", + "/usr/share/man/uk/man1/newgrp.1.gz", + "/usr/share/man/uk/man8/nologin.8.gz" + ] + }, + { + "ID": "login.defs@1:4.17.4-2", + "Name": "login.defs", + "Identifier": { + "PURL": "pkg:deb/debian/login.defs@4.17.4-2?arch=all\u0026distro=debian-13.6\u0026epoch=1", + "UID": "b2ebc9108569350a" + }, + "Version": "4.17.4", + "Release": "2", + "Epoch": 1, + "Arch": "all", + "SrcName": "shadow", + "SrcVersion": "4.17.4", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-1.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/login.defs/NEWS.Debian.gz", + "/usr/share/doc/login.defs/changelog.Debian.gz", + "/usr/share/doc/login.defs/changelog.gz", + "/usr/share/doc/login.defs/copyright", + "/usr/share/man/de/man5/login.defs.5.gz", + "/usr/share/man/fr/man5/login.defs.5.gz", + "/usr/share/man/it/man5/login.defs.5.gz", + "/usr/share/man/ja/man5/login.defs.5.gz", + "/usr/share/man/man5/login.defs.5.gz", + "/usr/share/man/ru/man5/login.defs.5.gz", + "/usr/share/man/uk/man5/login.defs.5.gz", + "/usr/share/man/zh_CN/man5/login.defs.5.gz" + ] + }, + { + "ID": "mawk@1.3.4.20250131-1", + "Name": "mawk", + "Identifier": { + "PURL": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64\u0026distro=debian-13.6", + "UID": "9048a1b0d5acbb7e" + }, + "Version": "1.3.4.20250131", + "Release": "1", + "Arch": "amd64", + "SrcName": "mawk", + "SrcVersion": "1.3.4.20250131", + "SrcRelease": "1", + "Licenses": [ + "GPL-2.0-only", + "X11", + "CC-BY-3.0" + ], + "Maintainer": "Boyuan Yang \u003cbyang@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/mawk", + "/usr/share/doc/mawk/ACKNOWLEDGMENT", + "/usr/share/doc/mawk/README", + "/usr/share/doc/mawk/changelog.Debian.gz", + "/usr/share/doc/mawk/changelog.gz", + "/usr/share/doc/mawk/copyright", + "/usr/share/doc/mawk/examples/ct_length.awk", + "/usr/share/doc/mawk/examples/decl.awk", + "/usr/share/doc/mawk/examples/deps.awk", + "/usr/share/doc/mawk/examples/eatc.awk", + "/usr/share/doc/mawk/examples/gdecl.awk", + "/usr/share/doc/mawk/examples/hcal", + "/usr/share/doc/mawk/examples/hical", + "/usr/share/doc/mawk/examples/nocomment.awk", + "/usr/share/doc/mawk/examples/primes.awk", + "/usr/share/doc/mawk/examples/qsort.awk", + "/usr/share/man/man1/mawk.1.gz", + "/usr/share/man/man7/mawk-arrays.7.gz", + "/usr/share/man/man7/mawk-code.7.gz" + ] + }, + { + "ID": "mount@2.41-5", + "Name": "mount", + "Identifier": { + "PURL": "pkg:deb/debian/mount@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "c6fdc5cf989db569" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/mount", + "/usr/bin/umount", + "/usr/sbin/losetup", + "/usr/sbin/swapoff", + "/usr/sbin/swapon", + "/usr/share/bash-completion/completions/losetup", + "/usr/share/bash-completion/completions/mount", + "/usr/share/bash-completion/completions/swapoff", + "/usr/share/bash-completion/completions/swapon", + "/usr/share/bash-completion/completions/umount", + "/usr/share/doc/mount/NEWS.Debian.gz", + "/usr/share/doc/mount/changelog.Debian.gz", + "/usr/share/doc/mount/changelog.gz", + "/usr/share/doc/mount/copyright", + "/usr/share/doc/mount/examples/filesystems", + "/usr/share/doc/mount/examples/fstab", + "/usr/share/doc/mount/examples/mount.fstab", + "/usr/share/doc/mount/mount.txt", + "/usr/share/lintian/overrides/mount", + "/usr/share/man/man5/fstab.5.gz", + "/usr/share/man/man8/losetup.8.gz", + "/usr/share/man/man8/mount.8.gz", + "/usr/share/man/man8/swapon.8.gz", + "/usr/share/man/man8/umount.8.gz" + ] + }, + { + "ID": "ncurses-base@6.5+20250216-2", + "Name": "ncurses-base", + "Identifier": { + "PURL": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all\u0026distro=debian-13.6", + "UID": "76a1fb5936f344dc" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "all", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Licenses": [ + "MIT/X11", + "X11", + "BSD-3-Clause" + ], + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/ncurses-base/FAQ", + "/usr/share/doc/ncurses-base/TODO.Debian", + "/usr/share/doc/ncurses-base/changelog.Debian.gz", + "/usr/share/doc/ncurses-base/changelog.gz", + "/usr/share/doc/ncurses-base/copyright", + "/usr/share/lintian/overrides/ncurses-base", + "/usr/share/tabset/std", + "/usr/share/tabset/stdcrt", + "/usr/share/tabset/vt100", + "/usr/share/tabset/vt300", + "/usr/share/terminfo/E/Eterm", + "/usr/share/terminfo/a/ansi", + "/usr/share/terminfo/c/cons25", + "/usr/share/terminfo/c/cygwin", + "/usr/share/terminfo/d/dumb", + "/usr/share/terminfo/h/hurd", + "/usr/share/terminfo/l/linux", + "/usr/share/terminfo/m/mach", + "/usr/share/terminfo/m/mach-bold", + "/usr/share/terminfo/m/mach-color", + "/usr/share/terminfo/m/mach-gnu", + "/usr/share/terminfo/m/mach-gnu-color", + "/usr/share/terminfo/p/pcansi", + "/usr/share/terminfo/r/rxvt", + "/usr/share/terminfo/r/rxvt-basic", + "/usr/share/terminfo/r/rxvt-unicode", + "/usr/share/terminfo/r/rxvt-unicode-256color", + "/usr/share/terminfo/s/screen", + "/usr/share/terminfo/s/screen-256color", + "/usr/share/terminfo/s/screen-256color-bce", + "/usr/share/terminfo/s/screen-bce", + "/usr/share/terminfo/s/screen-s", + "/usr/share/terminfo/s/screen-w", + "/usr/share/terminfo/s/screen.xterm-256color", + "/usr/share/terminfo/s/sun", + "/usr/share/terminfo/t/tmux", + "/usr/share/terminfo/t/tmux-256color", + "/usr/share/terminfo/v/vt100", + "/usr/share/terminfo/v/vt102", + "/usr/share/terminfo/v/vt220", + "/usr/share/terminfo/v/vt52", + "/usr/share/terminfo/w/wsvt25", + "/usr/share/terminfo/w/wsvt25m", + "/usr/share/terminfo/x/xterm", + "/usr/share/terminfo/x/xterm-256color", + "/usr/share/terminfo/x/xterm-color", + "/usr/share/terminfo/x/xterm-mono", + "/usr/share/terminfo/x/xterm-r5", + "/usr/share/terminfo/x/xterm-r6", + "/usr/share/terminfo/x/xterm-vt220", + "/usr/share/terminfo/x/xterm-xfree86" + ] + }, + { + "ID": "ncurses-bin@6.5+20250216-2", + "Name": "ncurses-bin", + "Identifier": { + "PURL": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64\u0026distro=debian-13.6", + "UID": "d03e89ad6a7a5243" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Licenses": [ + "MIT/X11", + "X11", + "BSD-3-Clause" + ], + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/clear", + "/usr/bin/infocmp", + "/usr/bin/tabs", + "/usr/bin/tic", + "/usr/bin/toe", + "/usr/bin/tput", + "/usr/bin/tset", + "/usr/share/doc/ncurses-bin/changelog.Debian.gz", + "/usr/share/doc/ncurses-bin/changelog.gz", + "/usr/share/doc/ncurses-bin/copyright", + "/usr/share/man/man1/captoinfo.1.gz", + "/usr/share/man/man1/clear.1.gz", + "/usr/share/man/man1/infocmp.1.gz", + "/usr/share/man/man1/infotocap.1.gz", + "/usr/share/man/man1/tabs.1.gz", + "/usr/share/man/man1/tic.1.gz", + "/usr/share/man/man1/toe.1.gz", + "/usr/share/man/man1/tput.1.gz", + "/usr/share/man/man1/tset.1.gz", + "/usr/share/man/man5/scr_dump.5.gz", + "/usr/share/man/man5/term.5.gz", + "/usr/share/man/man5/terminfo.5.gz", + "/usr/share/man/man5/user_caps.5.gz", + "/usr/share/man/man7/term.7.gz" + ] + }, + { + "ID": "netbase@6.5", + "Name": "netbase", + "Identifier": { + "PURL": "pkg:deb/debian/netbase@6.5?arch=all\u0026distro=debian-13.6", + "UID": "b9a2c240e75fe15e" + }, + "Version": "6.5", + "Arch": "all", + "SrcName": "netbase", + "SrcVersion": "6.5", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Marco d'Itri \u003cmd@linux.it\u003e", + "Layer": { + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + "InstalledFiles": [ + "/usr/share/doc/netbase/changelog.gz", + "/usr/share/doc/netbase/copyright" + ] + }, + { + "ID": "openssl@3.5.6-1~deb13u2", + "Name": "openssl", + "Identifier": { + "PURL": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64\u0026distro=debian-13.6", + "UID": "8f9e5d7117307079" + }, + "Version": "3.5.6", + "Release": "1~deb13u2", + "Arch": "amd64", + "SrcName": "openssl", + "SrcVersion": "3.5.6", + "SrcRelease": "1~deb13u2", + "Licenses": [ + "Apache-2.0", + "Artistic-2.0", + "GPL-1.0-or-later", + "GPL-1.0-only" + ], + "Maintainer": "Debian OpenSSL Team \u003cpkg-openssl-devel@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libssl3t64@3.5.6-1~deb13u2" + ], + "Layer": { + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + "InstalledFiles": [ + "/usr/bin/c_rehash", + "/usr/bin/openssl", + "/usr/lib/ssl/misc/CA.pl", + "/usr/lib/ssl/misc/tsget.pl", + "/usr/share/doc/openssl/HOWTO/certificates.txt.gz", + "/usr/share/doc/openssl/HOWTO/documenting-functions-and-macros.md.gz", + "/usr/share/doc/openssl/HOWTO/keys.txt.gz", + "/usr/share/doc/openssl/NEWS.md.gz", + "/usr/share/doc/openssl/README-ENGINES.md.gz", + "/usr/share/doc/openssl/README-PROVIDERS.md.gz", + "/usr/share/doc/openssl/README-QUIC.md.gz", + "/usr/share/doc/openssl/README.Debian", + "/usr/share/doc/openssl/README.md.gz", + "/usr/share/doc/openssl/changelog.Debian.gz", + "/usr/share/doc/openssl/changelog.gz", + "/usr/share/doc/openssl/copyright", + "/usr/share/doc/openssl/fingerprints.txt", + "/usr/share/lintian/overrides/openssl", + "/usr/share/man/man1/CA.pl.1ssl.gz", + "/usr/share/man/man1/openssl-asn1parse.1ssl.gz", + "/usr/share/man/man1/openssl-ca.1ssl.gz", + "/usr/share/man/man1/openssl-ciphers.1ssl.gz", + "/usr/share/man/man1/openssl-cmds.1ssl.gz", + "/usr/share/man/man1/openssl-cmp.1ssl.gz", + "/usr/share/man/man1/openssl-cms.1ssl.gz", + "/usr/share/man/man1/openssl-crl.1ssl.gz", + "/usr/share/man/man1/openssl-crl2pkcs7.1ssl.gz", + "/usr/share/man/man1/openssl-dgst.1ssl.gz", + "/usr/share/man/man1/openssl-dhparam.1ssl.gz", + "/usr/share/man/man1/openssl-dsa.1ssl.gz", + "/usr/share/man/man1/openssl-dsaparam.1ssl.gz", + "/usr/share/man/man1/openssl-ec.1ssl.gz", + "/usr/share/man/man1/openssl-ecparam.1ssl.gz", + "/usr/share/man/man1/openssl-enc.1ssl.gz", + "/usr/share/man/man1/openssl-engine.1ssl.gz", + "/usr/share/man/man1/openssl-errstr.1ssl.gz", + "/usr/share/man/man1/openssl-fipsinstall.1ssl.gz", + "/usr/share/man/man1/openssl-format-options.1ssl.gz", + "/usr/share/man/man1/openssl-gendsa.1ssl.gz", + "/usr/share/man/man1/openssl-genpkey.1ssl.gz", + "/usr/share/man/man1/openssl-genrsa.1ssl.gz", + "/usr/share/man/man1/openssl-info.1ssl.gz", + "/usr/share/man/man1/openssl-kdf.1ssl.gz", + "/usr/share/man/man1/openssl-list.1ssl.gz", + "/usr/share/man/man1/openssl-mac.1ssl.gz", + "/usr/share/man/man1/openssl-namedisplay-options.1ssl.gz", + "/usr/share/man/man1/openssl-nseq.1ssl.gz", + "/usr/share/man/man1/openssl-ocsp.1ssl.gz", + "/usr/share/man/man1/openssl-passphrase-options.1ssl.gz", + "/usr/share/man/man1/openssl-passwd.1ssl.gz", + "/usr/share/man/man1/openssl-pkcs12.1ssl.gz", + "/usr/share/man/man1/openssl-pkcs7.1ssl.gz", + "/usr/share/man/man1/openssl-pkcs8.1ssl.gz", + "/usr/share/man/man1/openssl-pkey.1ssl.gz", + "/usr/share/man/man1/openssl-pkeyparam.1ssl.gz", + "/usr/share/man/man1/openssl-pkeyutl.1ssl.gz", + "/usr/share/man/man1/openssl-prime.1ssl.gz", + "/usr/share/man/man1/openssl-rand.1ssl.gz", + "/usr/share/man/man1/openssl-rehash.1ssl.gz", + "/usr/share/man/man1/openssl-req.1ssl.gz", + "/usr/share/man/man1/openssl-rsa.1ssl.gz", + "/usr/share/man/man1/openssl-rsautl.1ssl.gz", + "/usr/share/man/man1/openssl-s_client.1ssl.gz", + "/usr/share/man/man1/openssl-s_server.1ssl.gz", + "/usr/share/man/man1/openssl-s_time.1ssl.gz", + "/usr/share/man/man1/openssl-sess_id.1ssl.gz", + "/usr/share/man/man1/openssl-skeyutl.1ssl.gz", + "/usr/share/man/man1/openssl-smime.1ssl.gz", + "/usr/share/man/man1/openssl-speed.1ssl.gz", + "/usr/share/man/man1/openssl-spkac.1ssl.gz", + "/usr/share/man/man1/openssl-srp.1ssl.gz", + "/usr/share/man/man1/openssl-storeutl.1ssl.gz", + "/usr/share/man/man1/openssl-ts.1ssl.gz", + "/usr/share/man/man1/openssl-verification-options.1ssl.gz", + "/usr/share/man/man1/openssl-verify.1ssl.gz", + "/usr/share/man/man1/openssl-version.1ssl.gz", + "/usr/share/man/man1/openssl-x509.1ssl.gz", + "/usr/share/man/man1/openssl.1ssl.gz", + "/usr/share/man/man1/tsget.1ssl.gz", + "/usr/share/man/man5/config.5ssl.gz", + "/usr/share/man/man5/fips_config.5ssl.gz", + "/usr/share/man/man5/x509v3_config.5ssl.gz", + "/usr/share/man/man7/EVP_ASYM_CIPHER-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_ASYM_CIPHER-SM2.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-AES.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-ARIA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-BLOWFISH.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-CAMELLIA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-CAST.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-CHACHA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-DES.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-IDEA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-NULL.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-RC2.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-RC4.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-RC5.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-SEED.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-SM4.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-ARGON2.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-HKDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-HMAC-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-KB.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-KRB5KDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PBKDF1.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PBKDF2.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PKCS12KDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PVKKDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-SCRYPT.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-SS.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-SSHKDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-TLS13_KDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-TLS1_PRF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-X942-ASN1.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-X942-CONCAT.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-X963.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-EC.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-ML-KEM.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-X25519.7ssl.gz", + "/usr/share/man/man7/EVP_KEYEXCH-DH.7ssl.gz", + "/usr/share/man/man7/EVP_KEYEXCH-ECDH.7ssl.gz", + "/usr/share/man/man7/EVP_KEYEXCH-X25519.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-BLAKE2.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-CMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-GMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-HMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-KMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-Poly1305.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-Siphash.7ssl.gz", + "/usr/share/man/man7/EVP_MD-BLAKE2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-KECCAK.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD4.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD5-SHA1.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD5.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MDC2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-NULL.7ssl.gz", + "/usr/share/man/man7/EVP_MD-RIPEMD160.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHA1.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHA2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHA3.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHAKE.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SM3.7ssl.gz", + "/usr/share/man/man7/EVP_MD-WHIRLPOOL.7ssl.gz", + "/usr/share/man/man7/EVP_MD-common.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-DH.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-EC.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-FFC.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-HMAC.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-ML-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-ML-KEM.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-SLH-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-SM2.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-X25519.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-CRNG-TEST.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-CTR-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-HASH-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-HMAC-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-JITTER.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-SEED-SRC.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-TEST-RAND.7ssl.gz", + "/usr/share/man/man7/EVP_RAND.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-ECDSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-ED25519.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-HMAC.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-ML-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-SLH-DSA.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-FIPS.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-base.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-default.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-legacy.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-null.7ssl.gz", + "/usr/share/man/man7/OSSL_STORE-winstore.7ssl.gz", + "/usr/share/man/man7/RAND.7ssl.gz", + "/usr/share/man/man7/RSA-PSS.7ssl.gz", + "/usr/share/man/man7/X25519.7ssl.gz", + "/usr/share/man/man7/bio.7ssl.gz", + "/usr/share/man/man7/ct.7ssl.gz", + "/usr/share/man/man7/des_modes.7ssl.gz", + "/usr/share/man/man7/evp.7ssl.gz", + "/usr/share/man/man7/fips_module.7ssl.gz", + "/usr/share/man/man7/life_cycle-cipher.7ssl.gz", + "/usr/share/man/man7/life_cycle-digest.7ssl.gz", + "/usr/share/man/man7/life_cycle-kdf.7ssl.gz", + "/usr/share/man/man7/life_cycle-mac.7ssl.gz", + "/usr/share/man/man7/life_cycle-pkey.7ssl.gz", + "/usr/share/man/man7/life_cycle-rand.7ssl.gz", + "/usr/share/man/man7/openssl-core.h.7ssl.gz", + "/usr/share/man/man7/openssl-core_dispatch.h.7ssl.gz", + "/usr/share/man/man7/openssl-core_names.h.7ssl.gz", + "/usr/share/man/man7/openssl-env.7ssl.gz", + "/usr/share/man/man7/openssl-glossary.7ssl.gz", + "/usr/share/man/man7/openssl-qlog.7ssl.gz", + "/usr/share/man/man7/openssl-quic-concurrency.7ssl.gz", + "/usr/share/man/man7/openssl-quic.7ssl.gz", + "/usr/share/man/man7/openssl-threads.7ssl.gz", + "/usr/share/man/man7/openssl_user_macros.7ssl.gz", + "/usr/share/man/man7/ossl-guide-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-libcrypto-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-libraries-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-libssl-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-migration.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-client-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-client-non-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-multi-stream.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-server-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-server-non-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-client-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-client-non-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-server-block.7ssl.gz", + "/usr/share/man/man7/ossl_store-file.7ssl.gz", + "/usr/share/man/man7/ossl_store.7ssl.gz", + "/usr/share/man/man7/passphrase-encoding.7ssl.gz", + "/usr/share/man/man7/property.7ssl.gz", + "/usr/share/man/man7/provider-asym_cipher.7ssl.gz", + "/usr/share/man/man7/provider-base.7ssl.gz", + "/usr/share/man/man7/provider-cipher.7ssl.gz", + "/usr/share/man/man7/provider-decoder.7ssl.gz", + "/usr/share/man/man7/provider-digest.7ssl.gz", + "/usr/share/man/man7/provider-encoder.7ssl.gz", + "/usr/share/man/man7/provider-kdf.7ssl.gz", + "/usr/share/man/man7/provider-kem.7ssl.gz", + "/usr/share/man/man7/provider-keyexch.7ssl.gz", + "/usr/share/man/man7/provider-keymgmt.7ssl.gz", + "/usr/share/man/man7/provider-mac.7ssl.gz", + "/usr/share/man/man7/provider-object.7ssl.gz", + "/usr/share/man/man7/provider-rand.7ssl.gz", + "/usr/share/man/man7/provider-signature.7ssl.gz", + "/usr/share/man/man7/provider-skeymgmt.7ssl.gz", + "/usr/share/man/man7/provider-storemgmt.7ssl.gz", + "/usr/share/man/man7/provider.7ssl.gz", + "/usr/share/man/man7/proxy-certificates.7ssl.gz", + "/usr/share/man/man7/x509.7ssl.gz" + ] + }, + { + "ID": "openssl-provider-legacy@3.5.6-1~deb13u2", + "Name": "openssl-provider-legacy", + "Identifier": { + "PURL": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64\u0026distro=debian-13.6", + "UID": "f40d953a73d33d41" + }, + "Version": "3.5.6", + "Release": "1~deb13u2", + "Arch": "amd64", + "SrcName": "openssl", + "SrcVersion": "3.5.6", + "SrcRelease": "1~deb13u2", + "Licenses": [ + "Apache-2.0", + "Artistic-2.0", + "GPL-1.0-or-later", + "GPL-1.0-only" + ], + "Maintainer": "Debian OpenSSL Team \u003cpkg-openssl-devel@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libssl3t64@3.5.6-1~deb13u2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/ossl-modules/legacy.so", + "/usr/share/doc/openssl-provider-legacy/changelog.Debian.gz", + "/usr/share/doc/openssl-provider-legacy/changelog.gz", + "/usr/share/doc/openssl-provider-legacy/copyright" + ] + }, + { + "ID": "passwd@1:4.17.4-2", + "Name": "passwd", + "Identifier": { + "PURL": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "5c48c1fc5bd92522" + }, + "Version": "4.17.4", + "Release": "2", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "shadow", + "SrcVersion": "4.17.4", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-1.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "base-passwd@3.6.7", + "libacl1@2.3.2-2+b1", + "libattr1@1:2.5.2-3", + "libaudit1@1:4.0.2-2+b2", + "libbsd0@0.12.2-2", + "libc6@2.41-12+deb13u3", + "libcrypt1@1:4.4.38-1", + "libpam-modules@1.7.0-5", + "libpam0g@1.7.0-5", + "libselinux1@3.8.1-1", + "libsemanage2@3.8.1-1", + "login.defs@1:4.17.4-2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/chage", + "/usr/bin/chfn", + "/usr/bin/chsh", + "/usr/bin/expiry", + "/usr/bin/gpasswd", + "/usr/bin/passwd", + "/usr/lib/tmpfiles.d/passwd.conf", + "/usr/sbin/chgpasswd", + "/usr/sbin/chpasswd", + "/usr/sbin/groupadd", + "/usr/sbin/groupdel", + "/usr/sbin/groupmod", + "/usr/sbin/grpck", + "/usr/sbin/grpconv", + "/usr/sbin/grpunconv", + "/usr/sbin/newusers", + "/usr/sbin/pwck", + "/usr/sbin/pwconv", + "/usr/sbin/pwunconv", + "/usr/sbin/shadowconfig", + "/usr/sbin/useradd", + "/usr/sbin/userdel", + "/usr/sbin/usermod", + "/usr/sbin/vipw", + "/usr/share/doc/passwd/NEWS.Debian.gz", + "/usr/share/doc/passwd/README.Debian", + "/usr/share/doc/passwd/TODO.Debian", + "/usr/share/doc/passwd/changelog.Debian.gz", + "/usr/share/doc/passwd/changelog.gz", + "/usr/share/doc/passwd/copyright", + "/usr/share/doc/passwd/examples/passwd.expire.cron", + "/usr/share/lintian/overrides/passwd", + "/usr/share/locale/bs/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ca/LC_MESSAGES/shadow.mo", + "/usr/share/locale/cs/LC_MESSAGES/shadow.mo", + "/usr/share/locale/da/LC_MESSAGES/shadow.mo", + "/usr/share/locale/de/LC_MESSAGES/shadow.mo", + "/usr/share/locale/dz/LC_MESSAGES/shadow.mo", + "/usr/share/locale/el/LC_MESSAGES/shadow.mo", + "/usr/share/locale/es/LC_MESSAGES/shadow.mo", + "/usr/share/locale/eu/LC_MESSAGES/shadow.mo", + "/usr/share/locale/fi/LC_MESSAGES/shadow.mo", + "/usr/share/locale/fr/LC_MESSAGES/shadow.mo", + "/usr/share/locale/gl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/he/LC_MESSAGES/shadow.mo", + "/usr/share/locale/hu/LC_MESSAGES/shadow.mo", + "/usr/share/locale/id/LC_MESSAGES/shadow.mo", + "/usr/share/locale/it/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ja/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ka/LC_MESSAGES/shadow.mo", + "/usr/share/locale/kk/LC_MESSAGES/shadow.mo", + "/usr/share/locale/km/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ko/LC_MESSAGES/shadow.mo", + "/usr/share/locale/nb/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ne/LC_MESSAGES/shadow.mo", + "/usr/share/locale/nl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/nn/LC_MESSAGES/shadow.mo", + "/usr/share/locale/pl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/pt/LC_MESSAGES/shadow.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ro/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ru/LC_MESSAGES/shadow.mo", + "/usr/share/locale/sk/LC_MESSAGES/shadow.mo", + "/usr/share/locale/sq/LC_MESSAGES/shadow.mo", + "/usr/share/locale/sv/LC_MESSAGES/shadow.mo", + "/usr/share/locale/tl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/tr/LC_MESSAGES/shadow.mo", + "/usr/share/locale/uk/LC_MESSAGES/shadow.mo", + "/usr/share/locale/vi/LC_MESSAGES/shadow.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/shadow.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/shadow.mo", + "/usr/share/man/cs/man1/expiry.1.gz", + "/usr/share/man/cs/man1/gpasswd.1.gz", + "/usr/share/man/cs/man5/gshadow.5.gz", + "/usr/share/man/cs/man5/passwd.5.gz", + "/usr/share/man/cs/man5/shadow.5.gz", + "/usr/share/man/cs/man8/groupadd.8.gz", + "/usr/share/man/cs/man8/groupdel.8.gz", + "/usr/share/man/cs/man8/groupmod.8.gz", + "/usr/share/man/cs/man8/grpck.8.gz", + "/usr/share/man/cs/man8/vipw.8.gz", + "/usr/share/man/da/man1/chfn.1.gz", + "/usr/share/man/da/man5/gshadow.5.gz", + "/usr/share/man/da/man8/groupdel.8.gz", + "/usr/share/man/da/man8/vipw.8.gz", + "/usr/share/man/de/man1/chage.1.gz", + "/usr/share/man/de/man1/chfn.1.gz", + "/usr/share/man/de/man1/chsh.1.gz", + "/usr/share/man/de/man1/expiry.1.gz", + "/usr/share/man/de/man1/gpasswd.1.gz", + "/usr/share/man/de/man1/passwd.1.gz", + "/usr/share/man/de/man5/gshadow.5.gz", + "/usr/share/man/de/man5/passwd.5.gz", + "/usr/share/man/de/man5/shadow.5.gz", + "/usr/share/man/de/man8/chgpasswd.8.gz", + "/usr/share/man/de/man8/chpasswd.8.gz", + "/usr/share/man/de/man8/groupadd.8.gz", + "/usr/share/man/de/man8/groupdel.8.gz", + "/usr/share/man/de/man8/groupmod.8.gz", + "/usr/share/man/de/man8/grpck.8.gz", + "/usr/share/man/de/man8/newusers.8.gz", + "/usr/share/man/de/man8/pwck.8.gz", + "/usr/share/man/de/man8/pwconv.8.gz", + "/usr/share/man/de/man8/useradd.8.gz", + "/usr/share/man/de/man8/userdel.8.gz", + "/usr/share/man/de/man8/usermod.8.gz", + "/usr/share/man/de/man8/vipw.8.gz", + "/usr/share/man/fi/man1/chfn.1.gz", + "/usr/share/man/fi/man1/chsh.1.gz", + "/usr/share/man/fr/man1/chage.1.gz", + "/usr/share/man/fr/man1/chfn.1.gz", + "/usr/share/man/fr/man1/chsh.1.gz", + "/usr/share/man/fr/man1/expiry.1.gz", + "/usr/share/man/fr/man1/gpasswd.1.gz", + "/usr/share/man/fr/man1/passwd.1.gz", + "/usr/share/man/fr/man5/gshadow.5.gz", + "/usr/share/man/fr/man5/passwd.5.gz", + "/usr/share/man/fr/man5/shadow.5.gz", + "/usr/share/man/fr/man5/subgid.5.gz", + "/usr/share/man/fr/man5/subuid.5.gz", + "/usr/share/man/fr/man8/chgpasswd.8.gz", + "/usr/share/man/fr/man8/chpasswd.8.gz", + "/usr/share/man/fr/man8/groupadd.8.gz", + "/usr/share/man/fr/man8/groupdel.8.gz", + "/usr/share/man/fr/man8/groupmod.8.gz", + "/usr/share/man/fr/man8/grpck.8.gz", + "/usr/share/man/fr/man8/newusers.8.gz", + "/usr/share/man/fr/man8/pwck.8.gz", + "/usr/share/man/fr/man8/pwconv.8.gz", + "/usr/share/man/fr/man8/useradd.8.gz", + "/usr/share/man/fr/man8/userdel.8.gz", + "/usr/share/man/fr/man8/usermod.8.gz", + "/usr/share/man/fr/man8/vipw.8.gz", + "/usr/share/man/hu/man1/chsh.1.gz", + "/usr/share/man/hu/man1/gpasswd.1.gz", + "/usr/share/man/hu/man1/passwd.1.gz", + "/usr/share/man/hu/man5/passwd.5.gz", + "/usr/share/man/id/man1/chsh.1.gz", + "/usr/share/man/id/man8/useradd.8.gz", + "/usr/share/man/it/man1/chage.1.gz", + "/usr/share/man/it/man1/chfn.1.gz", + "/usr/share/man/it/man1/chsh.1.gz", + "/usr/share/man/it/man1/expiry.1.gz", + "/usr/share/man/it/man1/gpasswd.1.gz", + "/usr/share/man/it/man1/passwd.1.gz", + "/usr/share/man/it/man5/gshadow.5.gz", + "/usr/share/man/it/man5/passwd.5.gz", + "/usr/share/man/it/man5/shadow.5.gz", + "/usr/share/man/it/man8/chgpasswd.8.gz", + "/usr/share/man/it/man8/chpasswd.8.gz", + "/usr/share/man/it/man8/groupadd.8.gz", + "/usr/share/man/it/man8/groupdel.8.gz", + "/usr/share/man/it/man8/groupmod.8.gz", + "/usr/share/man/it/man8/grpck.8.gz", + "/usr/share/man/it/man8/newusers.8.gz", + "/usr/share/man/it/man8/pwck.8.gz", + "/usr/share/man/it/man8/pwconv.8.gz", + "/usr/share/man/it/man8/useradd.8.gz", + "/usr/share/man/it/man8/userdel.8.gz", + "/usr/share/man/it/man8/usermod.8.gz", + "/usr/share/man/it/man8/vipw.8.gz", + "/usr/share/man/ja/man1/chage.1.gz", + "/usr/share/man/ja/man1/chfn.1.gz", + "/usr/share/man/ja/man1/chsh.1.gz", + "/usr/share/man/ja/man1/expiry.1.gz", + "/usr/share/man/ja/man1/gpasswd.1.gz", + "/usr/share/man/ja/man1/passwd.1.gz", + "/usr/share/man/ja/man5/passwd.5.gz", + "/usr/share/man/ja/man5/shadow.5.gz", + "/usr/share/man/ja/man8/chpasswd.8.gz", + "/usr/share/man/ja/man8/groupadd.8.gz", + "/usr/share/man/ja/man8/groupdel.8.gz", + "/usr/share/man/ja/man8/groupmod.8.gz", + "/usr/share/man/ja/man8/grpck.8.gz", + "/usr/share/man/ja/man8/newusers.8.gz", + "/usr/share/man/ja/man8/pwck.8.gz", + "/usr/share/man/ja/man8/pwconv.8.gz", + "/usr/share/man/ja/man8/useradd.8.gz", + "/usr/share/man/ja/man8/userdel.8.gz", + "/usr/share/man/ja/man8/usermod.8.gz", + "/usr/share/man/ja/man8/vipw.8.gz", + "/usr/share/man/ko/man1/chfn.1.gz", + "/usr/share/man/ko/man1/chsh.1.gz", + "/usr/share/man/ko/man5/passwd.5.gz", + "/usr/share/man/ko/man8/vipw.8.gz", + "/usr/share/man/man1/chage.1.gz", + "/usr/share/man/man1/chfn.1.gz", + "/usr/share/man/man1/chsh.1.gz", + "/usr/share/man/man1/expiry.1.gz", + "/usr/share/man/man1/gpasswd.1.gz", + "/usr/share/man/man1/passwd.1.gz", + "/usr/share/man/man5/gshadow.5.gz", + "/usr/share/man/man5/passwd.5.gz", + "/usr/share/man/man5/shadow.5.gz", + "/usr/share/man/man5/subgid.5.gz", + "/usr/share/man/man5/subuid.5.gz", + "/usr/share/man/man8/chgpasswd.8.gz", + "/usr/share/man/man8/chpasswd.8.gz", + "/usr/share/man/man8/groupadd.8.gz", + "/usr/share/man/man8/groupdel.8.gz", + "/usr/share/man/man8/groupmod.8.gz", + "/usr/share/man/man8/grpck.8.gz", + "/usr/share/man/man8/newusers.8.gz", + "/usr/share/man/man8/pwck.8.gz", + "/usr/share/man/man8/pwconv.8.gz", + "/usr/share/man/man8/shadowconfig.8.gz", + "/usr/share/man/man8/useradd.8.gz", + "/usr/share/man/man8/userdel.8.gz", + "/usr/share/man/man8/usermod.8.gz", + "/usr/share/man/man8/vipw.8.gz", + "/usr/share/man/pl/man1/chage.1.gz", + "/usr/share/man/pl/man1/chsh.1.gz", + "/usr/share/man/pl/man1/expiry.1.gz", + "/usr/share/man/pl/man8/groupadd.8.gz", + "/usr/share/man/pl/man8/groupdel.8.gz", + "/usr/share/man/pl/man8/groupmod.8.gz", + "/usr/share/man/pl/man8/grpck.8.gz", + "/usr/share/man/pl/man8/userdel.8.gz", + "/usr/share/man/pl/man8/usermod.8.gz", + "/usr/share/man/pl/man8/vipw.8.gz", + "/usr/share/man/pt_BR/man1/gpasswd.1.gz", + "/usr/share/man/pt_BR/man5/passwd.5.gz", + "/usr/share/man/pt_BR/man5/shadow.5.gz", + "/usr/share/man/pt_BR/man8/groupadd.8.gz", + "/usr/share/man/pt_BR/man8/groupdel.8.gz", + "/usr/share/man/pt_BR/man8/groupmod.8.gz", + "/usr/share/man/ru/man1/chage.1.gz", + "/usr/share/man/ru/man1/chfn.1.gz", + "/usr/share/man/ru/man1/chsh.1.gz", + "/usr/share/man/ru/man1/expiry.1.gz", + "/usr/share/man/ru/man1/gpasswd.1.gz", + "/usr/share/man/ru/man1/passwd.1.gz", + "/usr/share/man/ru/man5/gshadow.5.gz", + "/usr/share/man/ru/man5/passwd.5.gz", + "/usr/share/man/ru/man5/shadow.5.gz", + "/usr/share/man/ru/man8/chgpasswd.8.gz", + "/usr/share/man/ru/man8/chpasswd.8.gz", + "/usr/share/man/ru/man8/groupadd.8.gz", + "/usr/share/man/ru/man8/groupdel.8.gz", + "/usr/share/man/ru/man8/groupmod.8.gz", + "/usr/share/man/ru/man8/grpck.8.gz", + "/usr/share/man/ru/man8/newusers.8.gz", + "/usr/share/man/ru/man8/pwck.8.gz", + "/usr/share/man/ru/man8/pwconv.8.gz", + "/usr/share/man/ru/man8/useradd.8.gz", + "/usr/share/man/ru/man8/userdel.8.gz", + "/usr/share/man/ru/man8/usermod.8.gz", + "/usr/share/man/ru/man8/vipw.8.gz", + "/usr/share/man/sv/man1/chage.1.gz", + "/usr/share/man/sv/man1/chsh.1.gz", + "/usr/share/man/sv/man1/expiry.1.gz", + "/usr/share/man/sv/man1/passwd.1.gz", + "/usr/share/man/sv/man5/gshadow.5.gz", + "/usr/share/man/sv/man5/passwd.5.gz", + "/usr/share/man/sv/man8/groupadd.8.gz", + "/usr/share/man/sv/man8/groupdel.8.gz", + "/usr/share/man/sv/man8/groupmod.8.gz", + "/usr/share/man/sv/man8/grpck.8.gz", + "/usr/share/man/sv/man8/pwck.8.gz", + "/usr/share/man/sv/man8/userdel.8.gz", + "/usr/share/man/sv/man8/vipw.8.gz", + "/usr/share/man/tr/man1/chage.1.gz", + "/usr/share/man/tr/man1/chfn.1.gz", + "/usr/share/man/tr/man1/passwd.1.gz", + "/usr/share/man/tr/man5/passwd.5.gz", + "/usr/share/man/tr/man5/shadow.5.gz", + "/usr/share/man/tr/man8/groupadd.8.gz", + "/usr/share/man/tr/man8/groupdel.8.gz", + "/usr/share/man/tr/man8/groupmod.8.gz", + "/usr/share/man/tr/man8/useradd.8.gz", + "/usr/share/man/tr/man8/userdel.8.gz", + "/usr/share/man/tr/man8/usermod.8.gz", + "/usr/share/man/uk/man1/chage.1.gz", + "/usr/share/man/uk/man1/chfn.1.gz", + "/usr/share/man/uk/man1/chsh.1.gz", + "/usr/share/man/uk/man1/expiry.1.gz", + "/usr/share/man/uk/man1/gpasswd.1.gz", + "/usr/share/man/uk/man1/passwd.1.gz", + "/usr/share/man/uk/man5/gshadow.5.gz", + "/usr/share/man/uk/man5/passwd.5.gz", + "/usr/share/man/uk/man5/shadow.5.gz", + "/usr/share/man/uk/man8/chgpasswd.8.gz", + "/usr/share/man/uk/man8/chpasswd.8.gz", + "/usr/share/man/uk/man8/groupadd.8.gz", + "/usr/share/man/uk/man8/groupdel.8.gz", + "/usr/share/man/uk/man8/groupmod.8.gz", + "/usr/share/man/uk/man8/grpck.8.gz", + "/usr/share/man/uk/man8/newusers.8.gz", + "/usr/share/man/uk/man8/pwck.8.gz", + "/usr/share/man/uk/man8/pwconv.8.gz", + "/usr/share/man/uk/man8/useradd.8.gz", + "/usr/share/man/uk/man8/userdel.8.gz", + "/usr/share/man/uk/man8/usermod.8.gz", + "/usr/share/man/uk/man8/vipw.8.gz", + "/usr/share/man/zh_CN/man1/chage.1.gz", + "/usr/share/man/zh_CN/man1/chfn.1.gz", + "/usr/share/man/zh_CN/man1/chsh.1.gz", + "/usr/share/man/zh_CN/man1/expiry.1.gz", + "/usr/share/man/zh_CN/man1/gpasswd.1.gz", + "/usr/share/man/zh_CN/man1/passwd.1.gz", + "/usr/share/man/zh_CN/man5/gshadow.5.gz", + "/usr/share/man/zh_CN/man5/passwd.5.gz", + "/usr/share/man/zh_CN/man5/shadow.5.gz", + "/usr/share/man/zh_CN/man8/chgpasswd.8.gz", + "/usr/share/man/zh_CN/man8/chpasswd.8.gz", + "/usr/share/man/zh_CN/man8/groupadd.8.gz", + "/usr/share/man/zh_CN/man8/groupdel.8.gz", + "/usr/share/man/zh_CN/man8/groupmod.8.gz", + "/usr/share/man/zh_CN/man8/grpck.8.gz", + "/usr/share/man/zh_CN/man8/newusers.8.gz", + "/usr/share/man/zh_CN/man8/pwck.8.gz", + "/usr/share/man/zh_CN/man8/pwconv.8.gz", + "/usr/share/man/zh_CN/man8/useradd.8.gz", + "/usr/share/man/zh_CN/man8/userdel.8.gz", + "/usr/share/man/zh_CN/man8/usermod.8.gz", + "/usr/share/man/zh_CN/man8/vipw.8.gz", + "/usr/share/man/zh_TW/man1/chfn.1.gz", + "/usr/share/man/zh_TW/man1/chsh.1.gz", + "/usr/share/man/zh_TW/man5/passwd.5.gz", + "/usr/share/man/zh_TW/man8/chpasswd.8.gz", + "/usr/share/man/zh_TW/man8/groupadd.8.gz", + "/usr/share/man/zh_TW/man8/groupdel.8.gz", + "/usr/share/man/zh_TW/man8/groupmod.8.gz", + "/usr/share/man/zh_TW/man8/useradd.8.gz", + "/usr/share/man/zh_TW/man8/userdel.8.gz", + "/usr/share/man/zh_TW/man8/usermod.8.gz" + ] + }, + { + "ID": "pcre@8.32-17.el7", + "Name": "pcre", + "Identifier": { + "PURL": "pkg:rpm/centos/pcre@8.32-17.el7", + "UID": "bb3e738eb75d1a13", + "BOMRef": "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72" + }, + "Version": "8.32", + "Release": "17.el7", + "SrcName": "pcre", + "SrcVersion": "8.32", + "SrcRelease": "17.el7", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "perl-base@5.40.1-6", + "Name": "perl-base", + "Identifier": { + "PURL": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64\u0026distro=debian-13.6", + "UID": "17f06da2c02a11c6" + }, + "Version": "5.40.1", + "Release": "6", + "Arch": "amd64", + "SrcName": "perl", + "SrcVersion": "5.40.1", + "SrcRelease": "6", + "Licenses": [ + "GPL-1.0-or-later", + "Artistic-2.0", + "MIT", + "REGCOMP", + "GPL-2.0-with-bison-exception+", + "Unicode", + "BZIP", + "Zlib", + "GPL-2.0-or-later", + "FSFAP", + "BSD-3-clause-with-weird-numbering", + "CC0-1.0", + "TEXT-TABS", + "BSD-4-clause-POWERDOG", + "BSD-3-clause-GENERIC", + "BSD-3-Clause", + "SDBM-PUBLIC-DOMAIN", + "DONT-CHANGE-THE-GPL", + "Artistic-dist", + "LGPL-2.1-only", + "GPL-1.0-only", + "GPL-2.0-only", + "Artistic-2" + ], + "Maintainer": "Niko Tyni \u003cntyni@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/perl", + "/usr/bin/perl5.40.1", + "/usr/lib/x86_64-linux-gnu/perl-base/AutoLoader.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Carp.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Carp/Heavy.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Config.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Config_git.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/Config_heavy.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/Cwd.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/DynaLoader.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Errno.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Exporter.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Exporter/Heavy.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Fcntl.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Basename.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Glob.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Path.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Spec.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Spec/Unix.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Temp.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/FileHandle.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Getopt/Long.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Getopt/Long/Parser.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Hash/Util.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/File.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Handle.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Pipe.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Seekable.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Select.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket/INET.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket/IP.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket/UNIX.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IPC/Open2.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IPC/Open3.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/List/Util.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/POSIX.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Scalar/Util.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/SelectSaver.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Socket.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Symbol.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Text/ParseWords.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Text/Tabs.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Text/Wrap.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Tie/Hash.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/XSLoader.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/attributes.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Cwd/Cwd.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Fcntl/Fcntl.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/File/Glob/Glob.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Hash/Util/Util.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/IO/IO.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/List/Util/Util.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/POSIX/POSIX.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Socket/Socket.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/attributes/attributes.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/re/re.so", + "/usr/lib/x86_64-linux-gnu/perl-base/base.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/builtin.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/bytes.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/constant.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/feature.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/fields.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/integer.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/lib.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/locale.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/overload.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/overloading.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/parent.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/re.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/strict.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Age.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bmg.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bpb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bpt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Cf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Ea.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/EqUIdeo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/GCB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Gc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Hst.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Identif2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Identifi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/InPC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/InSC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Isc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Jg.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Jt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Lb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Lc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFCQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFDQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFKCCF.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFKCQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFKDQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Na1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NameAlia.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Nt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Nv.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/PerlDeci.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/SB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Sc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Scx.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Tc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Uc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Vo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/WB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/_PerlLB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/_PerlSCX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/NA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V100.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V11.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V110.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V120.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V130.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V140.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V150.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V20.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V30.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V31.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V32.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V40.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V41.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V50.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V51.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V52.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V60.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V61.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V70.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V80.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V90.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Alpha/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/AL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/AN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/B.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/BN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/CS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/EN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/ES.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/ET.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/L.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/NSM.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/ON.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/R.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/WS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/BidiC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/BidiM/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Blk/NB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bpt/C.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bpt/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bpt/O.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CE/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CI/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWCF/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWCM/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWKCF/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWL/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWT/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWU/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Cased/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/A.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/AL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/AR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/ATAR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/B.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/BR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/DB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/NK.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/NR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/OV.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/VR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CompEx/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/DI/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dash/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dep/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dia/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Com.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Enc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Fin.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Font.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Init.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Iso.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Med.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Nar.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Nb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/NonCanon.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Sqr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Sub.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Sup.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Vert.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/EBase/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/EComp/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/EPres/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/A.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/H.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/Na.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/W.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Emoji/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ext/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/ExtPict/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/CN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/LV.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/LVT.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/PP.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/SM.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/C.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Cf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Cn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/L.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/LC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Ll.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Lm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Lo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Lu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/M.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Mc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Me.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Mn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Nd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Nl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/No.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/P.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pe.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Po.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Ps.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/S.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Sc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Sk.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Sm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/So.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Z.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Zs.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GrBase/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GrExt/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Hex/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Hst/NA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Hyphen/T.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IDC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IDS/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdStatus/Allowed.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdStatus/Restrict.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/DefaultI.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Exclusio.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Inclusio.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/LimitedU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/NotChara.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/NotNFKC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/NotXID.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Obsolete.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Recommen.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Technica.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Uncommon.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ideo/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/10_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/11_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/12_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/12_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/13_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/14_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/15_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/2_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/2_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/3_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/3_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/3_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/4_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/4_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/5_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/5_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/5_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/7_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/8_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/9_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Bottom.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/BottomAn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Left.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/LeftAndR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/NA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Overstru.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Right.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Top.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndBo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndL2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndLe.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndRi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/VisualOr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Avagraha.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Bindu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Cantilla.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona5.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona6.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona7.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona8.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona9.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consonan.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Geminati.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Invisibl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Nukta.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Number.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Other.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/PureKill.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Syllable.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/ToneMark.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Virama.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Visarga.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Vowel.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/VowelDep.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/VowelInd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Ain.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Alef.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Beh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Dal.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/FarsiYeh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Feh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Gaf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Hah.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/HanifiRo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Kaf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Lam.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/NoJoinin.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Noon.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Qaf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Reh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Sad.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Seen.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Tah.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Waw.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Yeh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/C.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/D.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/L.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/R.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/T.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/U.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/AI.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/AL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/BA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/BB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/CJ.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/CL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/CM.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/GL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/ID.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/IN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/IS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/NS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/NU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/OP.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/PO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/PR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/QU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/SA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lower/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Math/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFCQC/M.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFCQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFDQC/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFDQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKCQC/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKCQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKDQC/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKDQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nt/Di.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nt/None.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nt/Nu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/10.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/100.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/10000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/100000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/11.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/12.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/13.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/14.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/15.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/16.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/17.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/18.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/19.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_16.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_6.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_8.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/20.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/200.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/2000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/20000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/2_3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/30.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/300.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/30000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3_16.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3_4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/40.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/400.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/4000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/40000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/5.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/50.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/500.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/5000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/50000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/6.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/60.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/600.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/6000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/60000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/7.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/70.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/700.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/7000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/70000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/8.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/80.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/800.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/8000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/80000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/9.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/90.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/900.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/9000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/90000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/PCM/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/PatSyn/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Alnum.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Assigned.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Blank.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Graph.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/PerlWord.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/PosixPun.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Print.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/SpacePer.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Title.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Word.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/XPosixPu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlAny.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlCh2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlCha.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlFol.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlIDC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlIDS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlIsI.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlNch.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlPat.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlPr2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlPro.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlQuo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/QMark/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/AT.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/CL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/FO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/LE.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/LO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/NU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/SC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/ST.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/Sp.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/UP.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SD/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/STerm/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Arab.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Beng.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Cprt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Cyrl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Deva.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Dupl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Geor.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Glag.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gonm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gran.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Grek.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gujr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Guru.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Han.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Hang.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Hira.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Kana.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Knda.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Latn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Limb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Linb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Mlym.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Mong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Mult.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Orya.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Sinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Syrc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Taml.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Telu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Zinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Zyyy.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Adlm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Arab.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Armn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Beng.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Bhks.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Bopo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cakm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cham.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Copt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cprt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cyrl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Deva.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Diak.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Dupl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Ethi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Geor.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Glag.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gonm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gran.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Grek.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gujr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Guru.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Han.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hang.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hebr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hira.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hmng.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hmnp.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Kana.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Khar.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Khmr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Khoj.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Knda.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Kthi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Lana.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Lao.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Latn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Limb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Lina.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Linb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mlym.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mult.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mymr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Nand.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Nko.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Orya.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Phlp.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Rohg.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Shrd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Sind.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Sinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Syrc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tagb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Takr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Talu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Taml.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tang.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Telu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Thaa.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tibt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tirh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Vith.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Xsux.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Yezi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Yi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Zinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Zyyy.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Zzzz.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Term/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/UIdeo/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Upper/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/VS/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/R.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/Tr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/Tu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/U.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/Extend.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/FO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/HL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/KA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/LE.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/MB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/ML.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/MN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/NU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/WSegSpac.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/XIDC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/XIDS/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/utf8.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/vars.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/warnings.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/warnings/register.pm", + "/usr/share/doc/perl-base/changelog.Debian.gz", + "/usr/share/doc/perl-base/changelog.gz", + "/usr/share/doc/perl-base/copyright", + "/usr/share/doc/perl/AUTHORS.gz", + "/usr/share/doc/perl/Documentation", + "/usr/share/lintian/overrides/perl-base", + "/usr/share/man/man1/perl.1.gz" + ] + }, + { + "ID": "readline-common@8.2-6", + "Name": "readline-common", + "Identifier": { + "PURL": "pkg:deb/debian/readline-common@8.2-6?arch=all\u0026distro=debian-13.6", + "UID": "7f5f4bcfd1669a46" + }, + "Version": "8.2", + "Release": "6", + "Arch": "all", + "SrcName": "readline", + "SrcVersion": "8.2", + "SrcRelease": "6", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only", + "GFDL-1.3-no-invariants-or-later", + "GFDL-1.3-or-later", + "ISC-no-attribution" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/share/doc/readline-common/changelog.Debian.gz", + "/usr/share/doc/readline-common/changelog.gz", + "/usr/share/doc/readline-common/copyright", + "/usr/share/doc/readline-common/inputrc.arrows", + "/usr/share/info/rluserman.info.gz", + "/usr/share/lintian/overrides/readline-common", + "/usr/share/man/man3/history.3readline.gz", + "/usr/share/man/man3/readline.3readline.gz", + "/usr/share/readline/inputrc" + ] + }, + { + "ID": "sed@4.9-2+deb13u1", + "Name": "sed", + "Identifier": { + "PURL": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "d9e2231b96ca2bda" + }, + "Version": "4.9", + "Release": "2+deb13u1", + "Arch": "amd64", + "SrcName": "sed", + "SrcVersion": "4.9", + "SrcRelease": "2+deb13u1", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "X11", + "GFDL-1.3-no-invariants-or-later", + "GFDL-1.3-only", + "ISC", + "BSD-4-Clause-UC", + "BSL-1", + "pcre" + ], + "Maintainer": "Clint Adams \u003cclint@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/sed", + "/usr/share/doc/sed/AUTHORS", + "/usr/share/doc/sed/BUGS.gz", + "/usr/share/doc/sed/NEWS.gz", + "/usr/share/doc/sed/README", + "/usr/share/doc/sed/THANKS.gz", + "/usr/share/doc/sed/changelog.Debian.gz", + "/usr/share/doc/sed/changelog.gz", + "/usr/share/doc/sed/copyright", + "/usr/share/doc/sed/examples/dc.sed", + "/usr/share/doc/sed/sedfaq.txt.gz", + "/usr/share/info/sed.info.gz", + "/usr/share/locale/af/LC_MESSAGES/sed.mo", + "/usr/share/locale/ast/LC_MESSAGES/sed.mo", + "/usr/share/locale/bg/LC_MESSAGES/sed.mo", + "/usr/share/locale/ca/LC_MESSAGES/sed.mo", + "/usr/share/locale/cs/LC_MESSAGES/sed.mo", + "/usr/share/locale/da/LC_MESSAGES/sed.mo", + "/usr/share/locale/de/LC_MESSAGES/sed.mo", + "/usr/share/locale/el/LC_MESSAGES/sed.mo", + "/usr/share/locale/eo/LC_MESSAGES/sed.mo", + "/usr/share/locale/es/LC_MESSAGES/sed.mo", + "/usr/share/locale/et/LC_MESSAGES/sed.mo", + "/usr/share/locale/eu/LC_MESSAGES/sed.mo", + "/usr/share/locale/fi/LC_MESSAGES/sed.mo", + "/usr/share/locale/fr/LC_MESSAGES/sed.mo", + "/usr/share/locale/ga/LC_MESSAGES/sed.mo", + "/usr/share/locale/gl/LC_MESSAGES/sed.mo", + "/usr/share/locale/he/LC_MESSAGES/sed.mo", + "/usr/share/locale/hr/LC_MESSAGES/sed.mo", + "/usr/share/locale/hu/LC_MESSAGES/sed.mo", + "/usr/share/locale/id/LC_MESSAGES/sed.mo", + "/usr/share/locale/it/LC_MESSAGES/sed.mo", + "/usr/share/locale/ja/LC_MESSAGES/sed.mo", + "/usr/share/locale/ka/LC_MESSAGES/sed.mo", + "/usr/share/locale/ko/LC_MESSAGES/sed.mo", + "/usr/share/locale/nb/LC_MESSAGES/sed.mo", + "/usr/share/locale/nl/LC_MESSAGES/sed.mo", + "/usr/share/locale/pl/LC_MESSAGES/sed.mo", + "/usr/share/locale/pt/LC_MESSAGES/sed.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/sed.mo", + "/usr/share/locale/ro/LC_MESSAGES/sed.mo", + "/usr/share/locale/ru/LC_MESSAGES/sed.mo", + "/usr/share/locale/sk/LC_MESSAGES/sed.mo", + "/usr/share/locale/sl/LC_MESSAGES/sed.mo", + "/usr/share/locale/sr/LC_MESSAGES/sed.mo", + "/usr/share/locale/sv/LC_MESSAGES/sed.mo", + "/usr/share/locale/tr/LC_MESSAGES/sed.mo", + "/usr/share/locale/uk/LC_MESSAGES/sed.mo", + "/usr/share/locale/vi/LC_MESSAGES/sed.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/sed.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/sed.mo", + "/usr/share/man/man1/sed.1.gz" + ] + }, + { + "ID": "sqv@1.3.0-3+b2", + "Name": "sqv", + "Identifier": { + "PURL": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64\u0026distro=debian-13.6", + "UID": "82c65dd56fbcfd0e" + }, + "Version": "1.3.0", + "Release": "3+b2", + "Arch": "amd64", + "SrcName": "rust-sequoia-sqv", + "SrcVersion": "1.3.0", + "SrcRelease": "3", + "Licenses": [ + "LGPL-2.0-or-later", + "LGPL-2.0-only" + ], + "Maintainer": "Debian Rust Maintainers \u003cpkg-rust-maintainers@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19", + "libgmp10@2:6.3.0+dfsg-3", + "libhogweed6t64@3.10.1-1", + "libnettle8t64@3.10.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/sqv", + "/usr/share/bash-completion/completions/sqv.bash", + "/usr/share/doc/sqv/NEWS.gz", + "/usr/share/doc/sqv/changelog.Debian.amd64.gz", + "/usr/share/doc/sqv/changelog.Debian.gz", + "/usr/share/doc/sqv/copyright", + "/usr/share/fish/completions/sqv.fish", + "/usr/share/man/man1/sqv.1.gz", + "/usr/share/zsh/vendor-completions/_sqv" + ] + }, + { + "ID": "sysvinit-utils@3.14-4", + "Name": "sysvinit-utils", + "Identifier": { + "PURL": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64\u0026distro=debian-13.6", + "UID": "8699daa05d734d69" + }, + "Version": "3.14", + "Release": "4", + "Arch": "amd64", + "SrcName": "sysvinit", + "SrcVersion": "3.14", + "SrcRelease": "4", + "Licenses": [ + "GPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-only", + "GPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian sysvinit maintainers \u003cdebian-init-diversity@chiark.greenend.org.uk\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/init/init-d-script", + "/usr/lib/init/vars.sh", + "/usr/lib/lsb/init-functions", + "/usr/lib/lsb/init-functions.d/00-verbose", + "/usr/sbin/fstab-decode", + "/usr/sbin/killall5", + "/usr/share/doc/sysvinit-utils/changelog.Debian.gz", + "/usr/share/doc/sysvinit-utils/copyright", + "/usr/share/man/man5/init-d-script.5.gz", + "/usr/share/man/man8/fstab-decode.8.gz", + "/usr/share/man/man8/killall5.8.gz", + "/usr/share/man/man8/pidof.8.gz" + ] + }, + { + "ID": "tar@1.35+dfsg-3.1", + "Name": "tar", + "Identifier": { + "PURL": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64\u0026distro=debian-13.6", + "UID": "50aee76d081ea925" + }, + "Version": "1.35+dfsg", + "Release": "3.1", + "Arch": "amd64", + "SrcName": "tar", + "SrcVersion": "1.35+dfsg", + "SrcRelease": "3.1", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "LGPL-3.0-or-later", + "LGPL-3.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Janos Lenart \u003cocsi@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/tar", + "/usr/lib/mime/packages/tar", + "/usr/sbin/rmt-tar", + "/usr/sbin/tarcat", + "/usr/share/doc/tar/AUTHORS", + "/usr/share/doc/tar/NEWS.gz", + "/usr/share/doc/tar/README.Debian", + "/usr/share/doc/tar/THANKS.gz", + "/usr/share/doc/tar/changelog.1.gz", + "/usr/share/doc/tar/changelog.Debian.gz", + "/usr/share/doc/tar/changelog.gz", + "/usr/share/doc/tar/copyright", + "/usr/share/locale/bg/LC_MESSAGES/tar.mo", + "/usr/share/locale/ca/LC_MESSAGES/tar.mo", + "/usr/share/locale/cs/LC_MESSAGES/tar.mo", + "/usr/share/locale/da/LC_MESSAGES/tar.mo", + "/usr/share/locale/de/LC_MESSAGES/tar.mo", + "/usr/share/locale/el/LC_MESSAGES/tar.mo", + "/usr/share/locale/eo/LC_MESSAGES/tar.mo", + "/usr/share/locale/es/LC_MESSAGES/tar.mo", + "/usr/share/locale/et/LC_MESSAGES/tar.mo", + "/usr/share/locale/eu/LC_MESSAGES/tar.mo", + "/usr/share/locale/fi/LC_MESSAGES/tar.mo", + "/usr/share/locale/fr/LC_MESSAGES/tar.mo", + "/usr/share/locale/ga/LC_MESSAGES/tar.mo", + "/usr/share/locale/gl/LC_MESSAGES/tar.mo", + "/usr/share/locale/hr/LC_MESSAGES/tar.mo", + "/usr/share/locale/hu/LC_MESSAGES/tar.mo", + "/usr/share/locale/id/LC_MESSAGES/tar.mo", + "/usr/share/locale/it/LC_MESSAGES/tar.mo", + "/usr/share/locale/ja/LC_MESSAGES/tar.mo", + "/usr/share/locale/ka/LC_MESSAGES/tar.mo", + "/usr/share/locale/ko/LC_MESSAGES/tar.mo", + "/usr/share/locale/ky/LC_MESSAGES/tar.mo", + "/usr/share/locale/ms/LC_MESSAGES/tar.mo", + "/usr/share/locale/nb/LC_MESSAGES/tar.mo", + "/usr/share/locale/nl/LC_MESSAGES/tar.mo", + "/usr/share/locale/pl/LC_MESSAGES/tar.mo", + "/usr/share/locale/pt/LC_MESSAGES/tar.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/tar.mo", + "/usr/share/locale/ro/LC_MESSAGES/tar.mo", + "/usr/share/locale/ru/LC_MESSAGES/tar.mo", + "/usr/share/locale/sk/LC_MESSAGES/tar.mo", + "/usr/share/locale/sl/LC_MESSAGES/tar.mo", + "/usr/share/locale/sr/LC_MESSAGES/tar.mo", + "/usr/share/locale/sv/LC_MESSAGES/tar.mo", + "/usr/share/locale/tr/LC_MESSAGES/tar.mo", + "/usr/share/locale/uk/LC_MESSAGES/tar.mo", + "/usr/share/locale/vi/LC_MESSAGES/tar.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/tar.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/tar.mo", + "/usr/share/man/man1/tar.1.gz", + "/usr/share/man/man1/tarcat.1.gz", + "/usr/share/man/man8/rmt-tar.8.gz" + ] + }, + { + "ID": "tzdata@2026b-0+deb13u1", + "Name": "tzdata", + "Identifier": { + "PURL": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all\u0026distro=debian-13.6", + "UID": "6698e2883de2f799" + }, + "Version": "2026b", + "Release": "0+deb13u1", + "Arch": "all", + "SrcName": "tzdata", + "SrcVersion": "2026b", + "SrcRelease": "0+deb13u1", + "Licenses": [ + "public-domain" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/tzdata/NEWS.Debian.gz", + "/usr/share/doc/tzdata/README.Debian", + "/usr/share/doc/tzdata/changelog.Debian.gz", + "/usr/share/doc/tzdata/changelog.gz", + "/usr/share/doc/tzdata/copyright", + "/usr/share/lintian/overrides/tzdata", + "/usr/share/zoneinfo/Africa/Abidjan", + "/usr/share/zoneinfo/Africa/Accra", + "/usr/share/zoneinfo/Africa/Addis_Ababa", + "/usr/share/zoneinfo/Africa/Algiers", + "/usr/share/zoneinfo/Africa/Asmara", + "/usr/share/zoneinfo/Africa/Bamako", + "/usr/share/zoneinfo/Africa/Bangui", + "/usr/share/zoneinfo/Africa/Banjul", + "/usr/share/zoneinfo/Africa/Bissau", + "/usr/share/zoneinfo/Africa/Blantyre", + "/usr/share/zoneinfo/Africa/Brazzaville", + "/usr/share/zoneinfo/Africa/Bujumbura", + "/usr/share/zoneinfo/Africa/Cairo", + "/usr/share/zoneinfo/Africa/Casablanca", + "/usr/share/zoneinfo/Africa/Ceuta", + "/usr/share/zoneinfo/Africa/Conakry", + "/usr/share/zoneinfo/Africa/Dakar", + "/usr/share/zoneinfo/Africa/Dar_es_Salaam", + "/usr/share/zoneinfo/Africa/Djibouti", + "/usr/share/zoneinfo/Africa/Douala", + "/usr/share/zoneinfo/Africa/El_Aaiun", + "/usr/share/zoneinfo/Africa/Freetown", + "/usr/share/zoneinfo/Africa/Gaborone", + "/usr/share/zoneinfo/Africa/Harare", + "/usr/share/zoneinfo/Africa/Johannesburg", + "/usr/share/zoneinfo/Africa/Juba", + "/usr/share/zoneinfo/Africa/Kampala", + "/usr/share/zoneinfo/Africa/Khartoum", + "/usr/share/zoneinfo/Africa/Kigali", + "/usr/share/zoneinfo/Africa/Kinshasa", + "/usr/share/zoneinfo/Africa/Lagos", + "/usr/share/zoneinfo/Africa/Libreville", + "/usr/share/zoneinfo/Africa/Lome", + "/usr/share/zoneinfo/Africa/Luanda", + "/usr/share/zoneinfo/Africa/Lubumbashi", + "/usr/share/zoneinfo/Africa/Lusaka", + "/usr/share/zoneinfo/Africa/Malabo", + "/usr/share/zoneinfo/Africa/Maputo", + "/usr/share/zoneinfo/Africa/Maseru", + "/usr/share/zoneinfo/Africa/Mbabane", + "/usr/share/zoneinfo/Africa/Mogadishu", + "/usr/share/zoneinfo/Africa/Monrovia", + "/usr/share/zoneinfo/Africa/Nairobi", + "/usr/share/zoneinfo/Africa/Ndjamena", + "/usr/share/zoneinfo/Africa/Niamey", + "/usr/share/zoneinfo/Africa/Nouakchott", + "/usr/share/zoneinfo/Africa/Ouagadougou", + "/usr/share/zoneinfo/Africa/Porto-Novo", + "/usr/share/zoneinfo/Africa/Sao_Tome", + "/usr/share/zoneinfo/Africa/Tripoli", + "/usr/share/zoneinfo/Africa/Tunis", + "/usr/share/zoneinfo/Africa/Windhoek", + "/usr/share/zoneinfo/America/Adak", + "/usr/share/zoneinfo/America/Anchorage", + "/usr/share/zoneinfo/America/Anguilla", + "/usr/share/zoneinfo/America/Antigua", + "/usr/share/zoneinfo/America/Araguaina", + "/usr/share/zoneinfo/America/Argentina/Buenos_Aires", + "/usr/share/zoneinfo/America/Argentina/Catamarca", + "/usr/share/zoneinfo/America/Argentina/Cordoba", + "/usr/share/zoneinfo/America/Argentina/Jujuy", + "/usr/share/zoneinfo/America/Argentina/La_Rioja", + "/usr/share/zoneinfo/America/Argentina/Mendoza", + "/usr/share/zoneinfo/America/Argentina/Rio_Gallegos", + "/usr/share/zoneinfo/America/Argentina/Salta", + "/usr/share/zoneinfo/America/Argentina/San_Juan", + "/usr/share/zoneinfo/America/Argentina/San_Luis", + "/usr/share/zoneinfo/America/Argentina/Tucuman", + "/usr/share/zoneinfo/America/Argentina/Ushuaia", + "/usr/share/zoneinfo/America/Aruba", + "/usr/share/zoneinfo/America/Asuncion", + "/usr/share/zoneinfo/America/Atikokan", + "/usr/share/zoneinfo/America/Bahia", + "/usr/share/zoneinfo/America/Bahia_Banderas", + "/usr/share/zoneinfo/America/Barbados", + "/usr/share/zoneinfo/America/Belem", + "/usr/share/zoneinfo/America/Belize", + "/usr/share/zoneinfo/America/Blanc-Sablon", + "/usr/share/zoneinfo/America/Boa_Vista", + "/usr/share/zoneinfo/America/Bogota", + "/usr/share/zoneinfo/America/Boise", + "/usr/share/zoneinfo/America/Cambridge_Bay", + "/usr/share/zoneinfo/America/Campo_Grande", + "/usr/share/zoneinfo/America/Cancun", + "/usr/share/zoneinfo/America/Caracas", + "/usr/share/zoneinfo/America/Cayenne", + "/usr/share/zoneinfo/America/Cayman", + "/usr/share/zoneinfo/America/Chicago", + "/usr/share/zoneinfo/America/Chihuahua", + "/usr/share/zoneinfo/America/Ciudad_Juarez", + "/usr/share/zoneinfo/America/Costa_Rica", + "/usr/share/zoneinfo/America/Coyhaique", + "/usr/share/zoneinfo/America/Creston", + "/usr/share/zoneinfo/America/Cuiaba", + "/usr/share/zoneinfo/America/Curacao", + "/usr/share/zoneinfo/America/Danmarkshavn", + "/usr/share/zoneinfo/America/Dawson", + "/usr/share/zoneinfo/America/Dawson_Creek", + "/usr/share/zoneinfo/America/Denver", + "/usr/share/zoneinfo/America/Detroit", + "/usr/share/zoneinfo/America/Dominica", + "/usr/share/zoneinfo/America/Edmonton", + "/usr/share/zoneinfo/America/Eirunepe", + "/usr/share/zoneinfo/America/El_Salvador", + "/usr/share/zoneinfo/America/Fort_Nelson", + "/usr/share/zoneinfo/America/Fortaleza", + "/usr/share/zoneinfo/America/Glace_Bay", + "/usr/share/zoneinfo/America/Goose_Bay", + "/usr/share/zoneinfo/America/Grand_Turk", + "/usr/share/zoneinfo/America/Grenada", + "/usr/share/zoneinfo/America/Guadeloupe", + "/usr/share/zoneinfo/America/Guatemala", + "/usr/share/zoneinfo/America/Guayaquil", + "/usr/share/zoneinfo/America/Guyana", + "/usr/share/zoneinfo/America/Halifax", + "/usr/share/zoneinfo/America/Havana", + "/usr/share/zoneinfo/America/Hermosillo", + "/usr/share/zoneinfo/America/Indiana/Indianapolis", + "/usr/share/zoneinfo/America/Indiana/Knox", + "/usr/share/zoneinfo/America/Indiana/Marengo", + "/usr/share/zoneinfo/America/Indiana/Petersburg", + "/usr/share/zoneinfo/America/Indiana/Tell_City", + "/usr/share/zoneinfo/America/Indiana/Vevay", + "/usr/share/zoneinfo/America/Indiana/Vincennes", + "/usr/share/zoneinfo/America/Indiana/Winamac", + "/usr/share/zoneinfo/America/Inuvik", + "/usr/share/zoneinfo/America/Iqaluit", + "/usr/share/zoneinfo/America/Jamaica", + "/usr/share/zoneinfo/America/Juneau", + "/usr/share/zoneinfo/America/Kentucky/Louisville", + "/usr/share/zoneinfo/America/Kentucky/Monticello", + "/usr/share/zoneinfo/America/La_Paz", + "/usr/share/zoneinfo/America/Lima", + "/usr/share/zoneinfo/America/Los_Angeles", + "/usr/share/zoneinfo/America/Maceio", + "/usr/share/zoneinfo/America/Managua", + "/usr/share/zoneinfo/America/Manaus", + "/usr/share/zoneinfo/America/Martinique", + "/usr/share/zoneinfo/America/Matamoros", + "/usr/share/zoneinfo/America/Mazatlan", + "/usr/share/zoneinfo/America/Menominee", + "/usr/share/zoneinfo/America/Merida", + "/usr/share/zoneinfo/America/Metlakatla", + "/usr/share/zoneinfo/America/Mexico_City", + "/usr/share/zoneinfo/America/Miquelon", + "/usr/share/zoneinfo/America/Moncton", + "/usr/share/zoneinfo/America/Monterrey", + "/usr/share/zoneinfo/America/Montevideo", + "/usr/share/zoneinfo/America/Montserrat", + "/usr/share/zoneinfo/America/Nassau", + "/usr/share/zoneinfo/America/New_York", + "/usr/share/zoneinfo/America/Nome", + "/usr/share/zoneinfo/America/Noronha", + "/usr/share/zoneinfo/America/North_Dakota/Beulah", + "/usr/share/zoneinfo/America/North_Dakota/Center", + "/usr/share/zoneinfo/America/North_Dakota/New_Salem", + "/usr/share/zoneinfo/America/Nuuk", + "/usr/share/zoneinfo/America/Ojinaga", + "/usr/share/zoneinfo/America/Panama", + "/usr/share/zoneinfo/America/Paramaribo", + "/usr/share/zoneinfo/America/Phoenix", + "/usr/share/zoneinfo/America/Port-au-Prince", + "/usr/share/zoneinfo/America/Port_of_Spain", + "/usr/share/zoneinfo/America/Porto_Velho", + "/usr/share/zoneinfo/America/Puerto_Rico", + "/usr/share/zoneinfo/America/Punta_Arenas", + "/usr/share/zoneinfo/America/Rankin_Inlet", + "/usr/share/zoneinfo/America/Recife", + "/usr/share/zoneinfo/America/Regina", + "/usr/share/zoneinfo/America/Resolute", + "/usr/share/zoneinfo/America/Rio_Branco", + "/usr/share/zoneinfo/America/Santarem", + "/usr/share/zoneinfo/America/Santiago", + "/usr/share/zoneinfo/America/Santo_Domingo", + "/usr/share/zoneinfo/America/Sao_Paulo", + "/usr/share/zoneinfo/America/Scoresbysund", + "/usr/share/zoneinfo/America/Sitka", + "/usr/share/zoneinfo/America/St_Johns", + "/usr/share/zoneinfo/America/St_Kitts", + "/usr/share/zoneinfo/America/St_Lucia", + "/usr/share/zoneinfo/America/St_Thomas", + "/usr/share/zoneinfo/America/St_Vincent", + "/usr/share/zoneinfo/America/Swift_Current", + "/usr/share/zoneinfo/America/Tegucigalpa", + "/usr/share/zoneinfo/America/Thule", + "/usr/share/zoneinfo/America/Tijuana", + "/usr/share/zoneinfo/America/Toronto", + "/usr/share/zoneinfo/America/Tortola", + "/usr/share/zoneinfo/America/Vancouver", + "/usr/share/zoneinfo/America/Whitehorse", + "/usr/share/zoneinfo/America/Winnipeg", + "/usr/share/zoneinfo/America/Yakutat", + "/usr/share/zoneinfo/Antarctica/Casey", + "/usr/share/zoneinfo/Antarctica/Davis", + "/usr/share/zoneinfo/Antarctica/DumontDUrville", + "/usr/share/zoneinfo/Antarctica/Macquarie", + "/usr/share/zoneinfo/Antarctica/Mawson", + "/usr/share/zoneinfo/Antarctica/McMurdo", + "/usr/share/zoneinfo/Antarctica/Palmer", + "/usr/share/zoneinfo/Antarctica/Rothera", + "/usr/share/zoneinfo/Antarctica/Syowa", + "/usr/share/zoneinfo/Antarctica/Troll", + "/usr/share/zoneinfo/Antarctica/Vostok", + "/usr/share/zoneinfo/Asia/Aden", + "/usr/share/zoneinfo/Asia/Almaty", + "/usr/share/zoneinfo/Asia/Amman", + "/usr/share/zoneinfo/Asia/Anadyr", + "/usr/share/zoneinfo/Asia/Aqtau", + "/usr/share/zoneinfo/Asia/Aqtobe", + "/usr/share/zoneinfo/Asia/Ashgabat", + "/usr/share/zoneinfo/Asia/Atyrau", + "/usr/share/zoneinfo/Asia/Baghdad", + "/usr/share/zoneinfo/Asia/Bahrain", + "/usr/share/zoneinfo/Asia/Baku", + "/usr/share/zoneinfo/Asia/Bangkok", + "/usr/share/zoneinfo/Asia/Barnaul", + "/usr/share/zoneinfo/Asia/Beirut", + "/usr/share/zoneinfo/Asia/Bishkek", + "/usr/share/zoneinfo/Asia/Brunei", + "/usr/share/zoneinfo/Asia/Chita", + "/usr/share/zoneinfo/Asia/Colombo", + "/usr/share/zoneinfo/Asia/Damascus", + "/usr/share/zoneinfo/Asia/Dhaka", + "/usr/share/zoneinfo/Asia/Dili", + "/usr/share/zoneinfo/Asia/Dubai", + "/usr/share/zoneinfo/Asia/Dushanbe", + "/usr/share/zoneinfo/Asia/Famagusta", + "/usr/share/zoneinfo/Asia/Gaza", + "/usr/share/zoneinfo/Asia/Hebron", + "/usr/share/zoneinfo/Asia/Ho_Chi_Minh", + "/usr/share/zoneinfo/Asia/Hong_Kong", + "/usr/share/zoneinfo/Asia/Hovd", + "/usr/share/zoneinfo/Asia/Irkutsk", + "/usr/share/zoneinfo/Asia/Jakarta", + "/usr/share/zoneinfo/Asia/Jayapura", + "/usr/share/zoneinfo/Asia/Jerusalem", + "/usr/share/zoneinfo/Asia/Kabul", + "/usr/share/zoneinfo/Asia/Kamchatka", + "/usr/share/zoneinfo/Asia/Karachi", + "/usr/share/zoneinfo/Asia/Kathmandu", + "/usr/share/zoneinfo/Asia/Khandyga", + "/usr/share/zoneinfo/Asia/Kolkata", + "/usr/share/zoneinfo/Asia/Krasnoyarsk", + "/usr/share/zoneinfo/Asia/Kuala_Lumpur", + "/usr/share/zoneinfo/Asia/Kuching", + "/usr/share/zoneinfo/Asia/Kuwait", + "/usr/share/zoneinfo/Asia/Macau", + "/usr/share/zoneinfo/Asia/Magadan", + "/usr/share/zoneinfo/Asia/Makassar", + "/usr/share/zoneinfo/Asia/Manila", + "/usr/share/zoneinfo/Asia/Muscat", + "/usr/share/zoneinfo/Asia/Nicosia", + "/usr/share/zoneinfo/Asia/Novokuznetsk", + "/usr/share/zoneinfo/Asia/Novosibirsk", + "/usr/share/zoneinfo/Asia/Omsk", + "/usr/share/zoneinfo/Asia/Oral", + "/usr/share/zoneinfo/Asia/Phnom_Penh", + "/usr/share/zoneinfo/Asia/Pontianak", + "/usr/share/zoneinfo/Asia/Pyongyang", + "/usr/share/zoneinfo/Asia/Qatar", + "/usr/share/zoneinfo/Asia/Qostanay", + "/usr/share/zoneinfo/Asia/Qyzylorda", + "/usr/share/zoneinfo/Asia/Riyadh", + "/usr/share/zoneinfo/Asia/Sakhalin", + "/usr/share/zoneinfo/Asia/Samarkand", + "/usr/share/zoneinfo/Asia/Seoul", + "/usr/share/zoneinfo/Asia/Shanghai", + "/usr/share/zoneinfo/Asia/Singapore", + "/usr/share/zoneinfo/Asia/Srednekolymsk", + "/usr/share/zoneinfo/Asia/Taipei", + "/usr/share/zoneinfo/Asia/Tashkent", + "/usr/share/zoneinfo/Asia/Tbilisi", + "/usr/share/zoneinfo/Asia/Tehran", + "/usr/share/zoneinfo/Asia/Thimphu", + "/usr/share/zoneinfo/Asia/Tokyo", + "/usr/share/zoneinfo/Asia/Tomsk", + "/usr/share/zoneinfo/Asia/Ulaanbaatar", + "/usr/share/zoneinfo/Asia/Urumqi", + "/usr/share/zoneinfo/Asia/Ust-Nera", + "/usr/share/zoneinfo/Asia/Vientiane", + "/usr/share/zoneinfo/Asia/Vladivostok", + "/usr/share/zoneinfo/Asia/Yakutsk", + "/usr/share/zoneinfo/Asia/Yangon", + "/usr/share/zoneinfo/Asia/Yekaterinburg", + "/usr/share/zoneinfo/Asia/Yerevan", + "/usr/share/zoneinfo/Atlantic/Azores", + "/usr/share/zoneinfo/Atlantic/Bermuda", + "/usr/share/zoneinfo/Atlantic/Canary", + "/usr/share/zoneinfo/Atlantic/Cape_Verde", + "/usr/share/zoneinfo/Atlantic/Faroe", + "/usr/share/zoneinfo/Atlantic/Madeira", + "/usr/share/zoneinfo/Atlantic/Reykjavik", + "/usr/share/zoneinfo/Atlantic/South_Georgia", + "/usr/share/zoneinfo/Atlantic/St_Helena", + "/usr/share/zoneinfo/Atlantic/Stanley", + "/usr/share/zoneinfo/Australia/Adelaide", + "/usr/share/zoneinfo/Australia/Brisbane", + "/usr/share/zoneinfo/Australia/Broken_Hill", + "/usr/share/zoneinfo/Australia/Darwin", + "/usr/share/zoneinfo/Australia/Eucla", + "/usr/share/zoneinfo/Australia/Hobart", + "/usr/share/zoneinfo/Australia/Lindeman", + "/usr/share/zoneinfo/Australia/Lord_Howe", + "/usr/share/zoneinfo/Australia/Melbourne", + "/usr/share/zoneinfo/Australia/Perth", + "/usr/share/zoneinfo/Australia/Sydney", + "/usr/share/zoneinfo/Etc/GMT", + "/usr/share/zoneinfo/Etc/GMT+1", + "/usr/share/zoneinfo/Etc/GMT+10", + "/usr/share/zoneinfo/Etc/GMT+11", + "/usr/share/zoneinfo/Etc/GMT+12", + "/usr/share/zoneinfo/Etc/GMT+2", + "/usr/share/zoneinfo/Etc/GMT+3", + "/usr/share/zoneinfo/Etc/GMT+4", + "/usr/share/zoneinfo/Etc/GMT+5", + "/usr/share/zoneinfo/Etc/GMT+6", + "/usr/share/zoneinfo/Etc/GMT+7", + "/usr/share/zoneinfo/Etc/GMT+8", + "/usr/share/zoneinfo/Etc/GMT+9", + "/usr/share/zoneinfo/Etc/GMT-1", + "/usr/share/zoneinfo/Etc/GMT-10", + "/usr/share/zoneinfo/Etc/GMT-11", + "/usr/share/zoneinfo/Etc/GMT-12", + "/usr/share/zoneinfo/Etc/GMT-13", + "/usr/share/zoneinfo/Etc/GMT-14", + "/usr/share/zoneinfo/Etc/GMT-2", + "/usr/share/zoneinfo/Etc/GMT-3", + "/usr/share/zoneinfo/Etc/GMT-4", + "/usr/share/zoneinfo/Etc/GMT-5", + "/usr/share/zoneinfo/Etc/GMT-6", + "/usr/share/zoneinfo/Etc/GMT-7", + "/usr/share/zoneinfo/Etc/GMT-8", + "/usr/share/zoneinfo/Etc/GMT-9", + "/usr/share/zoneinfo/Etc/UTC", + "/usr/share/zoneinfo/Europe/Amsterdam", + "/usr/share/zoneinfo/Europe/Andorra", + "/usr/share/zoneinfo/Europe/Astrakhan", + "/usr/share/zoneinfo/Europe/Athens", + "/usr/share/zoneinfo/Europe/Belgrade", + "/usr/share/zoneinfo/Europe/Berlin", + "/usr/share/zoneinfo/Europe/Brussels", + "/usr/share/zoneinfo/Europe/Bucharest", + "/usr/share/zoneinfo/Europe/Budapest", + "/usr/share/zoneinfo/Europe/Chisinau", + "/usr/share/zoneinfo/Europe/Copenhagen", + "/usr/share/zoneinfo/Europe/Dublin", + "/usr/share/zoneinfo/Europe/Gibraltar", + "/usr/share/zoneinfo/Europe/Guernsey", + "/usr/share/zoneinfo/Europe/Helsinki", + "/usr/share/zoneinfo/Europe/Isle_of_Man", + "/usr/share/zoneinfo/Europe/Istanbul", + "/usr/share/zoneinfo/Europe/Jersey", + "/usr/share/zoneinfo/Europe/Kaliningrad", + "/usr/share/zoneinfo/Europe/Kirov", + "/usr/share/zoneinfo/Europe/Kyiv", + "/usr/share/zoneinfo/Europe/Lisbon", + "/usr/share/zoneinfo/Europe/Ljubljana", + "/usr/share/zoneinfo/Europe/London", + "/usr/share/zoneinfo/Europe/Luxembourg", + "/usr/share/zoneinfo/Europe/Madrid", + "/usr/share/zoneinfo/Europe/Malta", + "/usr/share/zoneinfo/Europe/Minsk", + "/usr/share/zoneinfo/Europe/Monaco", + "/usr/share/zoneinfo/Europe/Moscow", + "/usr/share/zoneinfo/Europe/Oslo", + "/usr/share/zoneinfo/Europe/Paris", + "/usr/share/zoneinfo/Europe/Prague", + "/usr/share/zoneinfo/Europe/Riga", + "/usr/share/zoneinfo/Europe/Rome", + "/usr/share/zoneinfo/Europe/Samara", + "/usr/share/zoneinfo/Europe/Sarajevo", + "/usr/share/zoneinfo/Europe/Saratov", + "/usr/share/zoneinfo/Europe/Simferopol", + "/usr/share/zoneinfo/Europe/Skopje", + "/usr/share/zoneinfo/Europe/Sofia", + "/usr/share/zoneinfo/Europe/Stockholm", + "/usr/share/zoneinfo/Europe/Tallinn", + "/usr/share/zoneinfo/Europe/Tirane", + "/usr/share/zoneinfo/Europe/Ulyanovsk", + "/usr/share/zoneinfo/Europe/Vaduz", + "/usr/share/zoneinfo/Europe/Vienna", + "/usr/share/zoneinfo/Europe/Vilnius", + "/usr/share/zoneinfo/Europe/Volgograd", + "/usr/share/zoneinfo/Europe/Warsaw", + "/usr/share/zoneinfo/Europe/Zagreb", + "/usr/share/zoneinfo/Europe/Zurich", + "/usr/share/zoneinfo/Factory", + "/usr/share/zoneinfo/Indian/Antananarivo", + "/usr/share/zoneinfo/Indian/Chagos", + "/usr/share/zoneinfo/Indian/Christmas", + "/usr/share/zoneinfo/Indian/Cocos", + "/usr/share/zoneinfo/Indian/Comoro", + "/usr/share/zoneinfo/Indian/Kerguelen", + "/usr/share/zoneinfo/Indian/Mahe", + "/usr/share/zoneinfo/Indian/Maldives", + "/usr/share/zoneinfo/Indian/Mauritius", + "/usr/share/zoneinfo/Indian/Mayotte", + "/usr/share/zoneinfo/Indian/Reunion", + "/usr/share/zoneinfo/Pacific/Apia", + "/usr/share/zoneinfo/Pacific/Auckland", + "/usr/share/zoneinfo/Pacific/Bougainville", + "/usr/share/zoneinfo/Pacific/Chatham", + "/usr/share/zoneinfo/Pacific/Chuuk", + "/usr/share/zoneinfo/Pacific/Easter", + "/usr/share/zoneinfo/Pacific/Efate", + "/usr/share/zoneinfo/Pacific/Fakaofo", + "/usr/share/zoneinfo/Pacific/Fiji", + "/usr/share/zoneinfo/Pacific/Funafuti", + "/usr/share/zoneinfo/Pacific/Galapagos", + "/usr/share/zoneinfo/Pacific/Gambier", + "/usr/share/zoneinfo/Pacific/Guadalcanal", + "/usr/share/zoneinfo/Pacific/Guam", + "/usr/share/zoneinfo/Pacific/Honolulu", + "/usr/share/zoneinfo/Pacific/Kanton", + "/usr/share/zoneinfo/Pacific/Kiritimati", + "/usr/share/zoneinfo/Pacific/Kosrae", + "/usr/share/zoneinfo/Pacific/Kwajalein", + "/usr/share/zoneinfo/Pacific/Majuro", + "/usr/share/zoneinfo/Pacific/Marquesas", + "/usr/share/zoneinfo/Pacific/Midway", + "/usr/share/zoneinfo/Pacific/Nauru", + "/usr/share/zoneinfo/Pacific/Niue", + "/usr/share/zoneinfo/Pacific/Norfolk", + "/usr/share/zoneinfo/Pacific/Noumea", + "/usr/share/zoneinfo/Pacific/Pago_Pago", + "/usr/share/zoneinfo/Pacific/Palau", + "/usr/share/zoneinfo/Pacific/Pitcairn", + "/usr/share/zoneinfo/Pacific/Pohnpei", + "/usr/share/zoneinfo/Pacific/Port_Moresby", + "/usr/share/zoneinfo/Pacific/Rarotonga", + "/usr/share/zoneinfo/Pacific/Saipan", + "/usr/share/zoneinfo/Pacific/Tahiti", + "/usr/share/zoneinfo/Pacific/Tarawa", + "/usr/share/zoneinfo/Pacific/Tongatapu", + "/usr/share/zoneinfo/Pacific/Wake", + "/usr/share/zoneinfo/Pacific/Wallis", + "/usr/share/zoneinfo/iso3166.tab", + "/usr/share/zoneinfo/leap-seconds.list", + "/usr/share/zoneinfo/leapseconds", + "/usr/share/zoneinfo/tzdata.zi", + "/usr/share/zoneinfo/zone.tab", + "/usr/share/zoneinfo/zone1970.tab", + "/usr/share/zoneinfo/zonenow.tab" + ] + }, + { + "ID": "util-linux@2.41-5", + "Name": "util-linux", + "Identifier": { + "PURL": "pkg:deb/debian/util-linux@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "38be4846f19b7fa" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/choom", + "/usr/bin/chrt", + "/usr/bin/dmesg", + "/usr/bin/fallocate", + "/usr/bin/findmnt", + "/usr/bin/flock", + "/usr/bin/getopt", + "/usr/bin/hardlink", + "/usr/bin/ionice", + "/usr/bin/ipcmk", + "/usr/bin/ipcrm", + "/usr/bin/ipcs", + "/usr/bin/lsblk", + "/usr/bin/lscpu", + "/usr/bin/lsipc", + "/usr/bin/lslocks", + "/usr/bin/lslogins", + "/usr/bin/lsmem", + "/usr/bin/lsns", + "/usr/bin/mcookie", + "/usr/bin/more", + "/usr/bin/mountpoint", + "/usr/bin/namei", + "/usr/bin/nsenter", + "/usr/bin/partx", + "/usr/bin/prlimit", + "/usr/bin/rename.ul", + "/usr/bin/rev", + "/usr/bin/setarch", + "/usr/bin/setpriv", + "/usr/bin/setsid", + "/usr/bin/setterm", + "/usr/bin/su", + "/usr/bin/taskset", + "/usr/bin/uclampset", + "/usr/bin/unshare", + "/usr/bin/wdctl", + "/usr/bin/whereis", + "/usr/lib/mime/packages/util-linux", + "/usr/lib/systemd/system/fstrim.service", + "/usr/lib/systemd/system/fstrim.timer", + "/usr/sbin/agetty", + "/usr/sbin/blkdiscard", + "/usr/sbin/blkid", + "/usr/sbin/blkzone", + "/usr/sbin/blockdev", + "/usr/sbin/chcpu", + "/usr/sbin/chmem", + "/usr/sbin/findfs", + "/usr/sbin/fsck", + "/usr/sbin/fsfreeze", + "/usr/sbin/fstrim", + "/usr/sbin/isosize", + "/usr/sbin/ldattach", + "/usr/sbin/mkfs", + "/usr/sbin/mkswap", + "/usr/sbin/pivot_root", + "/usr/sbin/readprofile", + "/usr/sbin/rtcwake", + "/usr/sbin/runuser", + "/usr/sbin/sulogin", + "/usr/sbin/swaplabel", + "/usr/sbin/switch_root", + "/usr/sbin/wipefs", + "/usr/sbin/zramctl", + "/usr/share/bash-completion/completions/blkdiscard", + "/usr/share/bash-completion/completions/blkid", + "/usr/share/bash-completion/completions/blkzone", + "/usr/share/bash-completion/completions/blockdev", + "/usr/share/bash-completion/completions/chcpu", + "/usr/share/bash-completion/completions/chmem", + "/usr/share/bash-completion/completions/chrt", + "/usr/share/bash-completion/completions/dmesg", + "/usr/share/bash-completion/completions/fallocate", + "/usr/share/bash-completion/completions/findfs", + "/usr/share/bash-completion/completions/findmnt", + "/usr/share/bash-completion/completions/flock", + "/usr/share/bash-completion/completions/fsck", + "/usr/share/bash-completion/completions/fsfreeze", + "/usr/share/bash-completion/completions/fstrim", + "/usr/share/bash-completion/completions/getopt", + "/usr/share/bash-completion/completions/hardlink", + "/usr/share/bash-completion/completions/ionice", + "/usr/share/bash-completion/completions/ipcmk", + "/usr/share/bash-completion/completions/ipcrm", + "/usr/share/bash-completion/completions/ipcs", + "/usr/share/bash-completion/completions/isosize", + "/usr/share/bash-completion/completions/ldattach", + "/usr/share/bash-completion/completions/lsblk", + "/usr/share/bash-completion/completions/lscpu", + "/usr/share/bash-completion/completions/lsipc", + "/usr/share/bash-completion/completions/lslocks", + "/usr/share/bash-completion/completions/lslogins", + "/usr/share/bash-completion/completions/lsmem", + "/usr/share/bash-completion/completions/lsns", + "/usr/share/bash-completion/completions/mcookie", + "/usr/share/bash-completion/completions/mkfs", + "/usr/share/bash-completion/completions/mkswap", + "/usr/share/bash-completion/completions/more", + "/usr/share/bash-completion/completions/mountpoint", + "/usr/share/bash-completion/completions/namei", + "/usr/share/bash-completion/completions/nsenter", + "/usr/share/bash-completion/completions/partx", + "/usr/share/bash-completion/completions/pivot_root", + "/usr/share/bash-completion/completions/prlimit", + "/usr/share/bash-completion/completions/readprofile", + "/usr/share/bash-completion/completions/rename.ul", + "/usr/share/bash-completion/completions/rev", + "/usr/share/bash-completion/completions/rtcwake", + "/usr/share/bash-completion/completions/setarch", + "/usr/share/bash-completion/completions/setpriv", + "/usr/share/bash-completion/completions/setsid", + "/usr/share/bash-completion/completions/setterm", + "/usr/share/bash-completion/completions/su", + "/usr/share/bash-completion/completions/swaplabel", + "/usr/share/bash-completion/completions/taskset", + "/usr/share/bash-completion/completions/uclampset", + "/usr/share/bash-completion/completions/unshare", + "/usr/share/bash-completion/completions/wdctl", + "/usr/share/bash-completion/completions/whereis", + "/usr/share/bash-completion/completions/wipefs", + "/usr/share/bash-completion/completions/zramctl", + "/usr/share/doc/util-linux/00-about-docs.txt", + "/usr/share/doc/util-linux/AUTHORS.gz", + "/usr/share/doc/util-linux/NEWS.Debian.gz", + "/usr/share/doc/util-linux/PAM-configuration.txt", + "/usr/share/doc/util-linux/README.Debian", + "/usr/share/doc/util-linux/blkid.txt", + "/usr/share/doc/util-linux/cal.txt", + "/usr/share/doc/util-linux/changelog.Debian.gz", + "/usr/share/doc/util-linux/changelog.gz", + "/usr/share/doc/util-linux/col.txt", + "/usr/share/doc/util-linux/copyright", + "/usr/share/doc/util-linux/deprecated.txt", + "/usr/share/doc/util-linux/examples/getopt-example.bash", + "/usr/share/doc/util-linux/getopt.txt", + "/usr/share/doc/util-linux/getopt_changelog.txt", + "/usr/share/doc/util-linux/howto-build-sys.txt", + "/usr/share/doc/util-linux/howto-compilation.txt", + "/usr/share/doc/util-linux/howto-contribute.txt.gz", + "/usr/share/doc/util-linux/howto-debug.txt", + "/usr/share/doc/util-linux/howto-man-page.txt", + "/usr/share/doc/util-linux/howto-pull-request.txt.gz", + "/usr/share/doc/util-linux/howto-tests.txt", + "/usr/share/doc/util-linux/howto-usage-function.txt.gz", + "/usr/share/doc/util-linux/hwclock.txt", + "/usr/share/doc/util-linux/modems-with-agetty.txt", + "/usr/share/doc/util-linux/mount.txt", + "/usr/share/doc/util-linux/parse-date.txt.gz", + "/usr/share/doc/util-linux/pg.txt", + "/usr/share/doc/util-linux/poeigl.txt.gz", + "/usr/share/doc/util-linux/release-schedule.txt", + "/usr/share/doc/util-linux/releases/v2.13-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.14-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.15-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.16-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.17-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.18-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.19-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.20-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.21-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.22-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.23-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.24-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.25-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.26-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.27-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.28-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.29-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.30-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.31-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.32-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.33-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.34-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.35-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.36-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.37-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.38-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.39-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.40-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.41-ReleaseNotes.gz", + "/usr/share/lintian/overrides/util-linux", + "/usr/share/man/man1/choom.1.gz", + "/usr/share/man/man1/chrt.1.gz", + "/usr/share/man/man1/dmesg.1.gz", + "/usr/share/man/man1/fallocate.1.gz", + "/usr/share/man/man1/flock.1.gz", + "/usr/share/man/man1/getopt.1.gz", + "/usr/share/man/man1/hardlink.1.gz", + "/usr/share/man/man1/ionice.1.gz", + "/usr/share/man/man1/ipcmk.1.gz", + "/usr/share/man/man1/ipcrm.1.gz", + "/usr/share/man/man1/ipcs.1.gz", + "/usr/share/man/man1/lscpu.1.gz", + "/usr/share/man/man1/lsipc.1.gz", + "/usr/share/man/man1/lslogins.1.gz", + "/usr/share/man/man1/lsmem.1.gz", + "/usr/share/man/man1/mcookie.1.gz", + "/usr/share/man/man1/more.1.gz", + "/usr/share/man/man1/mountpoint.1.gz", + "/usr/share/man/man1/namei.1.gz", + "/usr/share/man/man1/nsenter.1.gz", + "/usr/share/man/man1/prlimit.1.gz", + "/usr/share/man/man1/rename.ul.1.gz", + "/usr/share/man/man1/rev.1.gz", + "/usr/share/man/man1/runuser.1.gz", + "/usr/share/man/man1/setpriv.1.gz", + "/usr/share/man/man1/setsid.1.gz", + "/usr/share/man/man1/setterm.1.gz", + "/usr/share/man/man1/su.1.gz", + "/usr/share/man/man1/taskset.1.gz", + "/usr/share/man/man1/uclampset.1.gz", + "/usr/share/man/man1/unshare.1.gz", + "/usr/share/man/man1/whereis.1.gz", + "/usr/share/man/man5/adjtime_config.5.gz", + "/usr/share/man/man5/scols-filter.5.gz", + "/usr/share/man/man5/terminal-colors.d.5.gz", + "/usr/share/man/man8/agetty.8.gz", + "/usr/share/man/man8/blkdiscard.8.gz", + "/usr/share/man/man8/blkid.8.gz", + "/usr/share/man/man8/blkzone.8.gz", + "/usr/share/man/man8/blockdev.8.gz", + "/usr/share/man/man8/chcpu.8.gz", + "/usr/share/man/man8/chmem.8.gz", + "/usr/share/man/man8/findfs.8.gz", + "/usr/share/man/man8/findmnt.8.gz", + "/usr/share/man/man8/fsck.8.gz", + "/usr/share/man/man8/fsfreeze.8.gz", + "/usr/share/man/man8/fstrim.8.gz", + "/usr/share/man/man8/isosize.8.gz", + "/usr/share/man/man8/ldattach.8.gz", + "/usr/share/man/man8/lsblk.8.gz", + "/usr/share/man/man8/lslocks.8.gz", + "/usr/share/man/man8/lsns.8.gz", + "/usr/share/man/man8/mkfs.8.gz", + "/usr/share/man/man8/mkswap.8.gz", + "/usr/share/man/man8/partx.8.gz", + "/usr/share/man/man8/pivot_root.8.gz", + "/usr/share/man/man8/readprofile.8.gz", + "/usr/share/man/man8/rtcwake.8.gz", + "/usr/share/man/man8/setarch.8.gz", + "/usr/share/man/man8/sulogin.8.gz", + "/usr/share/man/man8/swaplabel.8.gz", + "/usr/share/man/man8/switch_root.8.gz", + "/usr/share/man/man8/wdctl.8.gz", + "/usr/share/man/man8/wipefs.8.gz", + "/usr/share/man/man8/zramctl.8.gz", + "/usr/share/util-linux/logcheck/ignore.d.server/util-linux" + ] + }, + { + "ID": "zlib1g@1:1.3.dfsg+really1.3.1-1+b1", + "Name": "zlib1g", + "Identifier": { + "PURL": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "202a4bb3bdd0a341" + }, + "Version": "1.3.dfsg+really1.3.1", + "Release": "1+b1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "zlib", + "SrcVersion": "1.3.dfsg+really1.3.1", + "SrcRelease": "1", + "SrcEpoch": 1, + "Licenses": [ + "Zlib" + ], + "Maintainer": "Mark Brown \u003cbroonie@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libz.so.1.3.1", + "/usr/share/doc/zlib1g/changelog.Debian.amd64.gz", + "/usr/share/doc/zlib1g/changelog.Debian.gz", + "/usr/share/doc/zlib1g/changelog.gz", + "/usr/share/doc/zlib1g/copyright" + ] + } + ] + }, + { + "Target": "Python", + "Class": "lang-pkgs", + "Type": "python-pkg", + "Packages": [ + { + "Name": "PyJWT", + "Identifier": { + "PURL": "pkg:pypi/pyjwt@2.13.0", + "UID": "5fc2d525c3bd7c1f" + }, + "Version": "2.13.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/METADATA", + "Digest": "sha1:78125d2bb60e70bc168fd8787075ceb6a69d4f65" + }, + { + "Name": "annotated-types", + "Identifier": { + "PURL": "pkg:pypi/annotated-types@0.7.0", + "UID": "c71dc606d215f927" + }, + "Version": "0.7.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA", + "Digest": "sha1:b11011181822ac765c9f66c8aa42c26952de6a96" + }, + { + "Name": "anthropic", + "Identifier": { + "PURL": "pkg:pypi/anthropic@0.104.1", + "UID": "1518a81a53566060" + }, + "Version": "0.104.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/anthropic-0.104.1.dist-info/METADATA", + "Digest": "sha1:6a0fce5932599b482bf25ed3db3c06e9211f1358" + }, + { + "Name": "anyio", + "Identifier": { + "PURL": "pkg:pypi/anyio@4.13.0", + "UID": "aeb9e8b83c448893" + }, + "Version": "4.13.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/anyio-4.13.0.dist-info/METADATA", + "Digest": "sha1:5f30168435645daddf756ecef34992631c6e778b" + }, + { + "Name": "attrs", + "Identifier": { + "PURL": "pkg:pypi/attrs@26.1.0", + "UID": "b54e7e42438eca08" + }, + "Version": "26.1.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/attrs-26.1.0.dist-info/METADATA", + "Digest": "sha1:89068272cc1dc340d8fd910a62be241c42414339" + }, + { + "Name": "boto3", + "Identifier": { + "PURL": "pkg:pypi/boto3@1.43.56", + "UID": "3423e7541c1fbba1" + }, + "Version": "1.43.56", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/boto3-1.43.56.dist-info/METADATA", + "Digest": "sha1:f5c7842f2414d0cd2cefdb918d07c52018450ff7" + }, + { + "Name": "botocore", + "Identifier": { + "PURL": "pkg:pypi/botocore@1.43.56", + "UID": "359b1819e48a292f" + }, + "Version": "1.43.56", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/botocore-1.43.56.dist-info/METADATA", + "Digest": "sha1:b8760d1db82eba72c06ec96252b62d541a21b09e" + }, + { + "Name": "certifi", + "Identifier": { + "PURL": "pkg:pypi/certifi@2026.5.20", + "UID": "fe6f01e6c112235d" + }, + "Version": "2026.5.20", + "Licenses": [ + "MPL-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/certifi-2026.5.20.dist-info/METADATA", + "Digest": "sha1:cb42a7b0ba6491d51e71ff594a39bfaf5b8f9d22" + }, + { + "Name": "cffi", + "Identifier": { + "PURL": "pkg:pypi/cffi@2.0.0", + "UID": "e00042ff47d67589" + }, + "Version": "2.0.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/METADATA", + "Digest": "sha1:87e9c9d276c4f4c31f5a314d6a5472f45655674c" + }, + { + "Name": "click", + "Identifier": { + "PURL": "pkg:pypi/click@8.4.1", + "UID": "fe61547eb7f8f82b" + }, + "Version": "8.4.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/click-8.4.1.dist-info/METADATA", + "Digest": "sha1:c486957c59cc28072021bdcfe6d681e9ddafc860" + }, + { + "Name": "cryptography", + "Identifier": { + "PURL": "pkg:pypi/cryptography@49.0.0", + "UID": "c2dd24349681a2e" + }, + "Version": "49.0.0", + "Licenses": [ + "Apache-2.0 OR BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/cryptography-49.0.0.dist-info/METADATA", + "Digest": "sha1:739372eb2cc71602103a206e7a42a926930cecb6" + }, + { + "Name": "distro", + "Identifier": { + "PURL": "pkg:pypi/distro@1.9.0", + "UID": "c89f2e1e7521f2a8" + }, + "Version": "1.9.0", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/distro-1.9.0.dist-info/METADATA", + "Digest": "sha1:ce14620cf14e15a64d2ff574796543f99619e7f3" + }, + { + "Name": "docstring_parser", + "Identifier": { + "PURL": "pkg:pypi/docstring-parser@0.18.0", + "UID": "e4f3b6e5180465d3" + }, + "Version": "0.18.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/docstring_parser-0.18.0.dist-info/METADATA", + "Digest": "sha1:cd475f73b404c399cf87b9fb990fc14669479d24" + }, + { + "Name": "h11", + "Identifier": { + "PURL": "pkg:pypi/h11@0.16.0", + "UID": "bd2ef166e50e47c9" + }, + "Version": "0.16.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA", + "Digest": "sha1:5d41eddffefef5f6e8ff383a2537e81a38a37807" + }, + { + "Name": "httpcore", + "Identifier": { + "PURL": "pkg:pypi/httpcore@1.0.9", + "UID": "7af11667b3914c00" + }, + "Version": "1.0.9", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA", + "Digest": "sha1:2981d359ae33f31d339189a9680db85785339a56" + }, + { + "Name": "httpx", + "Identifier": { + "PURL": "pkg:pypi/httpx@0.28.1", + "UID": "84c8e4f1cadb34f8" + }, + "Version": "0.28.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA", + "Digest": "sha1:537da7e4f29438278e124e10e02d1a500fe33bcc" + }, + { + "Name": "httpx-sse", + "Identifier": { + "PURL": "pkg:pypi/httpx-sse@0.4.3", + "UID": "5aa2ff814a843fb8" + }, + "Version": "0.4.3", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/httpx_sse-0.4.3.dist-info/METADATA", + "Digest": "sha1:df05446be58f0a3a306e50c7ceebef24bb383a6d" + }, + { + "Name": "idna", + "Identifier": { + "PURL": "pkg:pypi/idna@3.16", + "UID": "56f67b383049e92f" + }, + "Version": "3.16", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/idna-3.16.dist-info/METADATA", + "Digest": "sha1:191a7bf1dac83b0cc997024ebb4b8da5c962ae5b" + }, + { + "Name": "jiter", + "Identifier": { + "PURL": "pkg:pypi/jiter@0.15.0", + "UID": "e16e05a7b79c5a19" + }, + "Version": "0.15.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jiter-0.15.0.dist-info/METADATA", + "Digest": "sha1:9de68ecc913d85eafa70a1252b2f5b1b2d2829f5" + }, + { + "Name": "jmespath", + "Identifier": { + "PURL": "pkg:pypi/jmespath@1.1.0", + "UID": "2b038fddcf91443b" + }, + "Version": "1.1.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jmespath-1.1.0.dist-info/METADATA", + "Digest": "sha1:d3f297922cf04b0cc127a18994ad6e6b947f0cc7" + }, + { + "Name": "jsonschema", + "Identifier": { + "PURL": "pkg:pypi/jsonschema@4.26.0", + "UID": "86f464e67bdc7252" + }, + "Version": "4.26.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jsonschema-4.26.0.dist-info/METADATA", + "Digest": "sha1:94b3d1a46cf55d74e42c401eaf4a4b71c76cee31" + }, + { + "Name": "jsonschema-specifications", + "Identifier": { + "PURL": "pkg:pypi/jsonschema-specifications@2025.9.1", + "UID": "4ff310017540d529" + }, + "Version": "2025.9.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA", + "Digest": "sha1:ac33f477be9d3336ae67bc454f68a9ff39c91cf3" + }, + { + "Name": "mcp", + "Identifier": { + "PURL": "pkg:pypi/mcp@1.28.1", + "UID": "20a25e72d0904008" + }, + "Version": "1.28.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/mcp-1.28.1.dist-info/METADATA", + "Digest": "sha1:22fedbbf2f1d94917eba0c0c156781325bd3d786" + }, + { + "Name": "openai", + "Identifier": { + "PURL": "pkg:pypi/openai@2.38.0", + "UID": "e80e37eec1285c9a" + }, + "Version": "2.38.0", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/openai-2.38.0.dist-info/METADATA", + "Digest": "sha1:ea17f685d13b1a896fb055b3bedbfcb5ed8da63a" + }, + { + "Name": "openrath", + "Identifier": { + "PURL": "pkg:pypi/openrath@1.3.0", + "UID": "545d02ff18290308" + }, + "Version": "1.3.0", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/openrath-1.3.0.dist-info/METADATA", + "Digest": "sha1:d04d62d29a60270300a6eefa18f542d44f8d14fc" + }, + { + "Name": "opentelemetry-api", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-api@1.42.1", + "UID": "8bdf3d15c03389e4" + }, + "Version": "1.42.1", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/opentelemetry_api-1.42.1.dist-info/METADATA", + "Digest": "sha1:a54ebbf560cfb13acfd3d7f7a5c385f03e36be08" + }, + { + "Name": "opentelemetry-sdk", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-sdk@1.42.1", + "UID": "ad8ea46e0b53c209" + }, + "Version": "1.42.1", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/opentelemetry_sdk-1.42.1.dist-info/METADATA", + "Digest": "sha1:cdfb9c90d3e4765c62ffce81a835445a8efef7c4" + }, + { + "Name": "opentelemetry-semantic-conventions", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "UID": "94dfcc83b7ff01c7" + }, + "Version": "0.63b1", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/opentelemetry_semantic_conventions-0.63b1.dist-info/METADATA", + "Digest": "sha1:5c7aaa298ae1ba489d7214ebec1427bd3f602556" + }, + { + "Name": "pip", + "Identifier": { + "PURL": "pkg:pypi/pip@25.0.1", + "UID": "6c029e6e913de377" + }, + "Version": "25.0.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "FilePath": "usr/local/lib/python3.12/site-packages/pip-25.0.1.dist-info/METADATA" + }, + { + "Name": "psycopg", + "Identifier": { + "PURL": "pkg:pypi/psycopg@3.3.4", + "UID": "6b4486e675bd7a02" + }, + "Version": "3.3.4", + "Licenses": [ + "LGPL-3.0-only" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/psycopg-3.3.4.dist-info/METADATA", + "Digest": "sha1:dc38178dd59b090117f63d60bc627e6a168205c8" + }, + { + "Name": "psycopg-binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4", + "UID": "e20890c2b22e7f46" + }, + "Version": "3.3.4", + "Licenses": [ + "LGPL-3.0-only" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/psycopg_binary-3.3.4.dist-info/METADATA", + "Digest": "sha1:e8c69d729c969a8a81834cce24f8947ad560813d" + }, + { + "Name": "psycopg-pool", + "Identifier": { + "PURL": "pkg:pypi/psycopg-pool@3.3.1", + "UID": "81277f710880020c" + }, + "Version": "3.3.1", + "Licenses": [ + "LGPL-3.0-only" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/psycopg_pool-3.3.1.dist-info/METADATA", + "Digest": "sha1:60d07aa411067306f5244d0481227a78a5b11135" + }, + { + "ID": "psycopg_binary@3.3.4", + "Name": "psycopg_binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "UID": "56fa9b57c57faa13", + "BOMRef": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + "Version": "3.3.4", + "DependsOn": [ + "krb5-libs@1.15.1-55.el7_9", + "krb5-libs@1.15.1-55.el7_9", + "krb5-libs@1.15.1-55.el7_9", + "krb5-libs@1.15.1-55.el7_9", + "cyrus-sasl-lib@2.1.26-24.el7_9", + "pcre@8.32-17.el7", + "libselinux@2.5-15.el7", + "libcom_err@1.42.9-19.el7", + "keyutils-libs@1.5.8-3.el7" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "ID": "psycopg_binary@3.3.4", + "Name": "psycopg_binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "UID": "76f4c37cac088f9b", + "BOMRef": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + "Version": "3.3.4", + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + } + }, + { + "Name": "pycparser", + "Identifier": { + "PURL": "pkg:pypi/pycparser@3.0", + "UID": "7fa26205633e9bc7" + }, + "Version": "3.0", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/METADATA", + "Digest": "sha1:ec46323dcd4dd2f7742b74b09cc0b030e330e46f" + }, + { + "Name": "pydantic", + "Identifier": { + "PURL": "pkg:pypi/pydantic@2.13.4", + "UID": "309395c26168aba1" + }, + "Version": "2.13.4", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pydantic-2.13.4.dist-info/METADATA", + "Digest": "sha1:291e482df82749c7e52c21e8275551f04de3034b" + }, + { + "Name": "pydantic-settings", + "Identifier": { + "PURL": "pkg:pypi/pydantic-settings@2.14.1", + "UID": "b01af17e9800dc25" + }, + "Version": "2.14.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pydantic_settings-2.14.1.dist-info/METADATA", + "Digest": "sha1:b9246a1f2d8974cb71bb7ec65fb8a9f45710d89a" + }, + { + "Name": "pydantic_core", + "Identifier": { + "PURL": "pkg:pypi/pydantic-core@2.46.4", + "UID": "11710354afa9f5bd" + }, + "Version": "2.46.4", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pydantic_core-2.46.4.dist-info/METADATA", + "Digest": "sha1:f44318e9ae79f745f1f5a7a59b43044ab6fff485" + }, + { + "Name": "python-dateutil", + "Identifier": { + "PURL": "pkg:pypi/python-dateutil@2.9.0.post0", + "UID": "fd4ae8a677536c26" + }, + "Version": "2.9.0.post0", + "Licenses": [ + "BSD-3-Clause", + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA", + "Digest": "sha1:7a3c35abd86cd96034d5afb0d4b241dc9e13e6f8" + }, + { + "Name": "python-dotenv", + "Identifier": { + "PURL": "pkg:pypi/python-dotenv@1.2.2", + "UID": "460b06f33eb8af4b" + }, + "Version": "1.2.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/METADATA", + "Digest": "sha1:a70b92340410dfaf8ce628e658f176278fa2e557" + }, + { + "Name": "python-multipart", + "Identifier": { + "PURL": "pkg:pypi/python-multipart@0.0.32", + "UID": "d69de3c1197a3dc4" + }, + "Version": "0.0.32", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/METADATA", + "Digest": "sha1:9f79572b3702bdac7487183b498470341276b17e" + }, + { + "Name": "redis", + "Identifier": { + "PURL": "pkg:pypi/redis@6.4.0", + "UID": "75c6670b0ecc89e1" + }, + "Version": "6.4.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/redis-6.4.0.dist-info/METADATA", + "Digest": "sha1:9a3de9ffc83addb0d845a4f16c6a415db91a3eda" + }, + { + "Name": "referencing", + "Identifier": { + "PURL": "pkg:pypi/referencing@0.37.0", + "UID": "9388367362afb6d3" + }, + "Version": "0.37.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/referencing-0.37.0.dist-info/METADATA", + "Digest": "sha1:f6fa004340bef5d23995b09dab75ce12e3f367a7" + }, + { + "Name": "rpds-py", + "Identifier": { + "PURL": "pkg:pypi/rpds-py@0.30.0", + "UID": "d63cf34ee7ba499b" + }, + "Version": "0.30.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/rpds_py-0.30.0.dist-info/METADATA", + "Digest": "sha1:9eef46c842a0ca6229680d7bfc1272958efdcc5a" + }, + { + "Name": "s3transfer", + "Identifier": { + "PURL": "pkg:pypi/s3transfer@0.19.2", + "UID": "6d71d4e81b9ba7fc" + }, + "Version": "0.19.2", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/s3transfer-0.19.2.dist-info/METADATA", + "Digest": "sha1:453d1af3240f56bb5d0bd093eb3d21a250cc5777" + }, + { + "Name": "six", + "Identifier": { + "PURL": "pkg:pypi/six@1.17.0", + "UID": "a8157445993a695a" + }, + "Version": "1.17.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/six-1.17.0.dist-info/METADATA", + "Digest": "sha1:483a26554261f6c839703c0e1183f3ef33ff97f1" + }, + { + "Name": "sniffio", + "Identifier": { + "PURL": "pkg:pypi/sniffio@1.3.1", + "UID": "1b1f0a25ec7db717" + }, + "Version": "1.3.1", + "Licenses": [ + "MIT", + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/sniffio-1.3.1.dist-info/METADATA", + "Digest": "sha1:bc1d7aead770fe23c8d22666b84558edb3686da3" + }, + { + "Name": "sse-starlette", + "Identifier": { + "PURL": "pkg:pypi/sse-starlette@3.4.4", + "UID": "f7d0373d022644e5" + }, + "Version": "3.4.4", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/sse_starlette-3.4.4.dist-info/METADATA", + "Digest": "sha1:58e4ad3946eafb572e0219f89bb599bee2b7cca2" + }, + { + "Name": "starlette", + "Identifier": { + "PURL": "pkg:pypi/starlette@1.3.1", + "UID": "5e46860cf2a27721" + }, + "Version": "1.3.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/starlette-1.3.1.dist-info/METADATA", + "Digest": "sha1:9e43f99dc64bcf4498d65999f9cb36a40fa5e94c" + }, + { + "Name": "tqdm", + "Identifier": { + "PURL": "pkg:pypi/tqdm@4.67.3", + "UID": "b7def43f9f2d825e" + }, + "Version": "4.67.3", + "Licenses": [ + "MPL-2.0 AND MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/tqdm-4.67.3.dist-info/METADATA", + "Digest": "sha1:0135af1981d2b0f1326020f991192d869693b873" + }, + { + "Name": "typing-inspection", + "Identifier": { + "PURL": "pkg:pypi/typing-inspection@0.4.2", + "UID": "a9781e801ec5ae38" + }, + "Version": "0.4.2", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/METADATA", + "Digest": "sha1:455fdb9c8e246ba02c2a28655287401b62028b60" + }, + { + "Name": "typing_extensions", + "Identifier": { + "PURL": "pkg:pypi/typing-extensions@4.15.0", + "UID": "1d9a13c2bfbf6a28" + }, + "Version": "4.15.0", + "Licenses": [ + "PSF-2.0" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/METADATA", + "Digest": "sha1:c5c2ce18351f8f2ae0f4a6f7c84c523f342010ee" + }, + { + "Name": "urllib3", + "Identifier": { + "PURL": "pkg:pypi/urllib3@2.7.0", + "UID": "f3bb6bdd35ff9f59" + }, + "Version": "2.7.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/urllib3-2.7.0.dist-info/METADATA", + "Digest": "sha1:d20520d0598c114ced8d55ed14209a2a3bbee22c" + }, + { + "Name": "uvicorn", + "Identifier": { + "PURL": "pkg:pypi/uvicorn@0.47.0", + "UID": "7ddf3857a5dc77ba" + }, + "Version": "0.47.0", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:05d3c6ff8330670d0a9254e9754ef67a2b041da440d4d392ce11f31b80cf9fb5" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/uvicorn-0.47.0.dist-info/METADATA", + "Digest": "sha1:f55652a6d9d6137b9be4cfc40b4c3d30b63717f8" + } + ] + } + ] +} From ec0ac921fe95de4d925113f63b4bda0934880f42 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Mon, 27 Jul 2026 20:56:27 +0800 Subject: [PATCH 12/22] chore(deps): upgrade opensandbox and openviking --- .github/workflows/ci-lint.yml | 2 +- .github/workflows/ci-test-openviking.yml | 9 +- pyproject.toml | 7 +- src/rath/artifacts/store.py | 10 +- src/rath/backend/opensandbox.py | 6 + src/rath/memory/adapters/openviking.py | 31 +- .../backends/test_opensandbox_ci_stability.py | 38 +- .../unit/test_openviking_result_compat.py | 67 +++ uv.lock | 448 +++++++++++++++--- 9 files changed, 523 insertions(+), 95 deletions(-) create mode 100644 tests/memory/unit/test_openviking_result_compat.py diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index da47c98..14de1e9 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -46,5 +46,5 @@ jobs: - uses: astral-sh/setup-uv@v5 with: python-version: '3.12' - - run: uv sync --dev --frozen + - run: uv sync --dev --extra postgres --extra redis --extra otel --frozen - run: uv run mypy --no-incremental diff --git a/.github/workflows/ci-test-openviking.yml b/.github/workflows/ci-test-openviking.yml index a26fd86..d8458a8 100644 --- a/.github/workflows/ci-test-openviking.yml +++ b/.github/workflows/ci-test-openviking.yml @@ -45,8 +45,8 @@ jobs: # When repository secrets are absent we skip uv sync; without this # the post-job cache prune fails because no cache dir was created. prune-cache: false - - name: Install dev dependencies - run: uv sync --dev --frozen + - name: Install dev and OpenViking dependencies + run: uv sync --dev --extra openviking --frozen - name: Check OpenViking credentials id: creds env: @@ -60,9 +60,8 @@ jobs: else echo "available=false" >> "$GITHUB_OUTPUT" fi - - name: Install OpenViking SDK - if: steps.creds.outputs.available == 'true' - run: uv sync --dev --extra openviking --frozen + - name: Run OpenViking SDK and offline contracts + run: uv run pytest -q tests/memory/unit - name: Start OpenViking server if: steps.creds.outputs.available == 'true' env: diff --git a/pyproject.toml b/pyproject.toml index 7290c28..a19c37f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,12 +39,12 @@ litellm = [ "litellm>=1.80,<1.88", ] opensandbox = [ - "opensandbox>=0.1.13", + "opensandbox>=0.1.15", "opensandbox-code-interpreter>=0.1.2", - "opensandbox-server>=0.2.1", + "opensandbox-server>=0.2.2", ] openviking = [ - "openviking>=0.4.7", + "openviking>=0.4.11", ] server = [ "starlette>=1.3.1,<2", @@ -94,6 +94,7 @@ include = [ [dependency-groups] dev = [ "mypy>=1.20.2", + "opentelemetry-sdk>=1.36,<2", "pytest>=9.0.3", "pytest-benchmark>=4.0.0", "pytest-rerunfailures>=15.0", diff --git a/src/rath/artifacts/store.py b/src/rath/artifacts/store.py index 65da189..7596945 100644 --- a/src/rath/artifacts/store.py +++ b/src/rath/artifacts/store.py @@ -254,7 +254,7 @@ def __init__( raise ValueError("max_bytes must be positive") if client is None: try: - import boto3 # type: ignore[import-not-found] + import boto3 # type: ignore except ImportError as exc: raise RuntimeError( "S3 support requires `pip install openrath[s3]`" @@ -309,9 +309,7 @@ def put( def get(self, tenant_id: str, digest: str) -> bytes: payload_key, _ = self._keys(tenant_id, digest) try: - response = self.client.get_object( - Bucket=self.bucket, Key=payload_key - ) + response = self.client.get_object(Bucket=self.bucket, Key=payload_key) except Exception as exc: if _not_found(exc): raise ArtifactNotFound(digest) from exc @@ -324,9 +322,7 @@ def get(self, tenant_id: str, digest: str) -> bytes: def stat(self, tenant_id: str, digest: str) -> Artifact: _, manifest_key = self._keys(tenant_id, digest) try: - response = self.client.get_object( - Bucket=self.bucket, Key=manifest_key - ) + response = self.client.get_object(Bucket=self.bucket, Key=manifest_key) except Exception as exc: if _not_found(exc): raise ArtifactNotFound(digest) from exc diff --git a/src/rath/backend/opensandbox.py b/src/rath/backend/opensandbox.py index 3b347fb..9140da3 100644 --- a/src/rath/backend/opensandbox.py +++ b/src/rath/backend/opensandbox.py @@ -301,6 +301,7 @@ async def _run_code_with_retry( effective = call_timeout if call_timeout is not None else _DEFAULT_TOOL_TIMEOUT_S last_execution: Any = None + saw_client_timeout = False for attempt in range(_CODE_RUN_ATTEMPTS): ci = await CodeInterpreter.create(native) try: @@ -309,6 +310,7 @@ async def _run_code_with_retry( effective, ) except TimeoutError: + saw_client_timeout = True if attempt + 1 >= _CODE_RUN_ATTEMPTS: raise logger.debug("OpenSandbox code.run timed out; retrying once") @@ -318,6 +320,10 @@ async def _run_code_with_retry( last_execution = execution if _is_transient_code_run_result(execution): if attempt + 1 >= _CODE_RUN_ATTEMPTS: + if saw_client_timeout: + raise TimeoutError( + "OpenSandbox code execution remained busy after timeout" + ) break logger.debug( "OpenSandbox code.run returned transient busy state; " diff --git a/src/rath/memory/adapters/openviking.py b/src/rath/memory/adapters/openviking.py index b824ad5..7a9473c 100644 --- a/src/rath/memory/adapters/openviking.py +++ b/src/rath/memory/adapters/openviking.py @@ -94,7 +94,7 @@ class _OpenVikingHandle: @register("openviking") class OpenVikingBackend(MemoryBackend): - """:class:`~rath.memory.MemoryBackend` backed by OpenViking 0.3.x.""" + """:class:`~rath.memory.MemoryBackend` backed by OpenViking 0.4.11+.""" def __init__(self) -> None: self._handles: dict[str, _OpenVikingHandle] = {} @@ -357,20 +357,25 @@ def _dispatch_tree(client: Any, op: "MemoryOpTree") -> MemoryListResult: def _hits_from_findresult(raw: Any) -> tuple[MemoryHit, ...]: - """Walk an ``openviking.FindResult`` into typed :class:`MemoryHit` tuples. + """Normalize OpenViking retrieval results into typed :class:`MemoryHit` tuples. - The SDK exposes two parallel lists — ``raw.memories`` and ``raw.resources`` - — each carrying ``MatchedContext`` objects with ``uri``, ``score``, - ``abstract``, ``overview``, ``level`` (int 0/1/2). + Supports 0.4.11 HTTP mappings and embedded ``FindResult`` objects. Both + shapes group matched contexts under ``memories``, ``resources``, and + ``skills`` with ``uri``, ``score``, summary, and level fields. """ + + def value(item: Any, key: str, default: Any = None) -> Any: + if isinstance(item, Mapping): + return item.get(key, default) + return getattr(item, key, default) + matches: list[Any] = [] for attr in ("memories", "resources", "skills"): - ms = getattr(raw, attr, None) or [] - matches.extend(ms) - matches.sort(key=lambda m: getattr(m, "score", 0.0), reverse=True) + matches.extend(value(raw, attr, []) or []) + matches.sort(key=lambda item: float(value(item, "score", 0.0)), reverse=True) hits: list[MemoryHit] = [] - for m in matches: - level_raw = getattr(m, "level", None) + for match in matches: + level_raw = value(match, "level") level: Any if isinstance(level_raw, int): level = _LEVEL_INT_TO_NAME.get(level_raw) @@ -378,11 +383,11 @@ def _hits_from_findresult(raw: Any) -> tuple[MemoryHit, ...]: level = level_raw if level_raw in _LEVEL_INT_TO_NAME.values() else None else: level = None - snippet = getattr(m, "abstract", None) or getattr(m, "overview", None) + snippet = value(match, "abstract") or value(match, "overview") hits.append( MemoryHit( - uri=to_public_uri(str(getattr(m, "uri", ""))), - score=float(getattr(m, "score", 0.0)), + uri=to_public_uri(str(value(match, "uri", ""))), + score=float(value(match, "score", 0.0)), snippet=snippet, level=level, ) diff --git a/tests/backends/test_opensandbox_ci_stability.py b/tests/backends/test_opensandbox_ci_stability.py index 3b82b7f..9cc42ca 100644 --- a/tests/backends/test_opensandbox_ci_stability.py +++ b/tests/backends/test_opensandbox_ci_stability.py @@ -2,18 +2,23 @@ from __future__ import annotations +import asyncio from pathlib import Path +from types import SimpleNamespace import pytest +import rath.backend.opensandbox as opensandbox_adapter from rath.backend.opensandbox import ( _command_stdout_rerun_allowed, _is_transient_code_run_result, _is_transient_sandbox_create_error, + _run_code_with_retry, _should_retry_command_for_empty_stdout, ) pytest.importorskip("opensandbox") +from code_interpreter import CodeInterpreter # noqa: E402 from opensandbox.exceptions import SandboxInternalException # noqa: E402 from opensandbox.models.execd import ( # noqa: E402 Execution, @@ -73,8 +78,6 @@ def test_command_stdout_rerun_limited_to_print_probes() -> None: def test_transient_code_run_detects_busy_session() -> None: - from types import SimpleNamespace - execution = SimpleNamespace( error=SimpleNamespace(value="error running codes session is busy") ) @@ -82,14 +85,41 @@ def test_transient_code_run_detects_busy_session() -> None: def test_transient_code_run_ignores_real_failures() -> None: - from types import SimpleNamespace - execution = SimpleNamespace( error=SimpleNamespace(value="SyntaxError: invalid syntax"), ) assert not _is_transient_code_run_result(execution) +def test_busy_result_after_client_timeout_stays_a_timeout(monkeypatch) -> None: + calls = 0 + busy = SimpleNamespace( + error=SimpleNamespace(value="error running codes session is busy") + ) + + class FakeCodes: + async def run(self, source, *, language): # type: ignore[no-untyped-def] + return None + + async def fake_create(native): # type: ignore[no-untyped-def] + return SimpleNamespace(codes=FakeCodes()) + + async def fake_await(awaitable, timeout): # type: ignore[no-untyped-def] + nonlocal calls + await awaitable + calls += 1 + if calls == 1: + raise TimeoutError("client deadline") + return busy + + monkeypatch.setattr(CodeInterpreter, "create", staticmethod(fake_create)) + monkeypatch.setattr(opensandbox_adapter, "_await_maybe_timeout", fake_await) + monkeypatch.setattr(opensandbox_adapter, "_CODE_RUN_BACKOFF_S", (0.0,)) + + with pytest.raises(TimeoutError, match="remained busy after timeout"): + asyncio.run(_run_code_with_retry(object(), "pass", "python", 0.5)) + + def test_transient_create_error_rejects_bind_rejection() -> None: exc = ValueError("host path not under any allowed prefix") assert not _is_transient_sandbox_create_error(exc) diff --git a/tests/memory/unit/test_openviking_result_compat.py b/tests/memory/unit/test_openviking_result_compat.py new file mode 100644 index 0000000..4abadb6 --- /dev/null +++ b/tests/memory/unit/test_openviking_result_compat.py @@ -0,0 +1,67 @@ +"""OpenViking SDK result-shape compatibility without a live server.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +pytest.importorskip("openviking") + +from rath.memory.adapters.openviking import _hits_from_findresult # noqa: E402 + + +def test_http_mapping_result_is_normalized() -> None: + hits = _hits_from_findresult( + { + "memories": [ + { + "uri": "viking://user/memories/preferences/style.md", + "score": 0.8, + "abstract": "Prefers concise answers.", + "level": 0, + } + ], + "resources": [ + { + "uri": "viking://resources/runbook.md", + "score": 0.95, + "overview": "Production runbook.", + "level": 1, + } + ], + "skills": [], + } + ) + + assert [hit.uri for hit in hits] == [ + "memory://resources/runbook.md", + "memory://user/memories/preferences/style.md", + ] + assert [hit.level for hit in hits] == ["overview", "abstract"] + assert [hit.snippet for hit in hits] == [ + "Production runbook.", + "Prefers concise answers.", + ] + + +def test_embedded_object_result_remains_supported() -> None: + hits = _hits_from_findresult( + SimpleNamespace( + memories=[], + resources=[], + skills=[ + SimpleNamespace( + uri="viking://agent/skills/release", + score=0.7, + abstract="Release safely.", + overview=None, + level=2, + ) + ], + ) + ) + + assert len(hits) == 1 + assert hits[0].uri == "memory://agent/skills/release" + assert hits[0].level == "detail" diff --git a/uv.lock b/uv.lock index a6393ba..c3ff440 100644 --- a/uv.lock +++ b/uv.lock @@ -280,6 +280,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -493,6 +502,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, +] + +[[package]] +name = "courlan" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "tld" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/16/2a771612ee0b3acaa95ac21cc7e8a3319e815d6360f8ffc5987d1ce28499/courlan-1.4.0.tar.gz", hash = "sha256:fbbac7b7fcde2195ea08e707609503c81cf39c891e8d26cdb1fed4585782d63d", size = 208997, upload-time = "2026-06-01T17:30:17.306Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/38/ce65091ff20a16e06d17418c4353af5f56d3190821b1a06983c79ae79274/courlan-1.4.0-py3-none-any.whl", hash = "sha256:ad1dbdefd912ca7238d4607dc855df5df097f56bac175dd662c84eed3802f49e", size = 34193, upload-time = "2026-06-01T17:30:14.984Z" }, +] + [[package]] name = "cryptography" version = "49.0.0" @@ -537,6 +569,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] +[[package]] +name = "cssselect" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5a/6d6fcf922709391fac986f0a03ad4546f4f45b94d10aeb6c1ee041599993/cssselect-1.5.0.tar.gz", hash = "sha256:3cbe82dd7acbee9ba9e5723b5f9e4749826912f1fb31cd7f92aabed5fde15b15", size = 47598, upload-time = "2026-07-27T09:17:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e9/6734502f67533a752ea8b1c8f7f227c94eecf300252ba8bf23e3e59d8a36/cssselect-1.5.0-py3-none-any.whl", hash = "sha256:1d1aded98e82bdde447ded990a191fd6916177c4f0c914fb62eccd58e2ffcdcc", size = 20797, upload-time = "2026-07-27T09:17:33.04Z" }, +] + +[[package]] +name = "dateparser" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" }, +] + [[package]] name = "decorator" version = "5.3.1" @@ -961,16 +1017,19 @@ wheels = [ ] [[package]] -name = "html5lib" -version = "1.1" +name = "htmldate" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, - { name = "webencodings" }, + { name = "charset-normalizer" }, + { name = "dateparser" }, + { name = "lxml" }, + { name = "python-dateutil" }, + { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/e7cf83e23d7b68105de8b874a8b36ba23b450d6f71388583e4ca3ce475ca/htmldate-1.10.0.tar.gz", hash = "sha256:a38df10772ab5d7dbb11896e3f6a852a8491fb1b0965465bc174e23fc2baae58", size = 44455, upload-time = "2026-06-01T17:43:53.437Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/f7/17/d3356233c826c641f940983d9479eab27faec59d49f4070bc58e80fcc021/htmldate-1.10.0-py3-none-any.whl", hash = "sha256:9211dae35ab94147c8ed9e5fc2c9287a5cf31d2394cb7857e7f5dd814eb2aad6", size = 31561, upload-time = "2026-06-01T17:43:51.797Z" }, ] [[package]] @@ -1071,6 +1130,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" }, ] +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + [[package]] name = "idna" version = "3.16" @@ -1101,6 +1172,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, ] +[[package]] +name = "incremental" +version = "24.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1110,6 +1194,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "itemadapter" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/47/4c75c5396941e653d5f864389964da6951e8f338c6739602dd778f62333e/itemadapter-0.13.1.tar.gz", hash = "sha256:fa139c7be2aa80f8874b2f23d165d5d4aa47c4b85c54ab530b567fd5f684f1b4", size = 32343, upload-time = "2026-01-08T17:56:38.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/a6/48805cef65b13644f1c23545dc525a7051581c84f5227efb1cd9a8ac9b02/itemadapter-0.13.1-py3-none-any.whl", hash = "sha256:f3c6b1babb4fb6cca4aa9061ef0b0c25c783c24a571c30e3667e7bcfea41815b", size = 18540, upload-time = "2026-01-08T17:56:37.29Z" }, +] + +[[package]] +name = "itemloaders" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "itemadapter" }, + { name = "jmespath" }, + { name = "parsel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/bd/916f4fd26e14e6ad292b69693ccca4f192bcaf9f817ba7d6f7162dbbd835/itemloaders-1.4.0.tar.gz", hash = "sha256:b5338308a819098f43525b7afc5f7d46ba338ba4710f5ebe7a21b3b47bb29929", size = 29740, upload-time = "2026-01-29T12:50:38.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/71/d9cd0e4c6a4aace991009fc47362ce9251be0fbcf2b6c533f918b31854d5/itemloaders-1.4.0-py3-none-any.whl", hash = "sha256:202b6f855299b4cadfdf78bb93a6cf977899e3c40c4c54524e120a444e65b5ac", size = 12188, upload-time = "2026-01-29T12:50:36.148Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1243,6 +1350,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "justext" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml", extra = ["html-clean"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" }, +] + [[package]] name = "kubernetes" version = "36.0.0" @@ -1456,6 +1575,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, ] +[package.optional-dependencies] +html-clean = [ + { name = "lxml-html-clean" }, +] + +[[package]] +name = "lxml-html-clean" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/63/195dfdde380a84df309e3bccf4384b034b745dba43426886f7ae623b4fba/lxml_html_clean-0.4.5.tar.gz", hash = "sha256:e2a4c7d5beedd17cd7b484d848a0571e54baa239a4f9df5546e3acba7f990560", size = 24142, upload-time = "2026-05-20T12:17:53.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/bd/6e2b76a6c5dee10397db9c929f0c5066766ec1036046f0335b7ca7ca08b8/lxml_html_clean-0.4.5-py3-none-any.whl", hash = "sha256:c76fcadd1e5bfb9b8bafc2200d51e4e78eb0dad67f56881c21dfb6484c7e7746", size = 14573, upload-time = "2026-05-20T12:17:52.215Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1488,19 +1624,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] -[[package]] -name = "markdownify" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, -] - [[package]] name = "markupsafe" version = "3.0.3" @@ -1904,6 +2027,7 @@ server = [ [package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "opentelemetry-sdk" }, { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-rerunfailures" }, @@ -1928,12 +2052,12 @@ requires-dist = [ { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.80,<1.88" }, { name = "mcp", specifier = ">=1.28.1,<2" }, { name = "openai", specifier = ">=1.0.0" }, - { name = "opensandbox", marker = "extra == 'opensandbox'", specifier = ">=0.1.13" }, + { name = "opensandbox", marker = "extra == 'opensandbox'", specifier = ">=0.1.15" }, { name = "opensandbox-code-interpreter", marker = "extra == 'opensandbox'", specifier = ">=0.1.2" }, - { name = "opensandbox-server", marker = "extra == 'opensandbox'", specifier = ">=0.2.1" }, + { name = "opensandbox-server", marker = "extra == 'opensandbox'", specifier = ">=0.2.2" }, { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.36,<2" }, { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.36,<2" }, - { name = "openviking", marker = "extra == 'openviking'", specifier = ">=0.4.7" }, + { name = "openviking", marker = "extra == 'openviking'", specifier = ">=0.4.11" }, { name = "psycopg", extras = ["binary", "pool"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=6,<7" }, @@ -1945,6 +2069,7 @@ provides-extras = ["litellm", "opensandbox", "openviking", "server", "postgres", [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.20.2" }, + { name = "opentelemetry-sdk", specifier = ">=1.36,<2" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-benchmark", specifier = ">=4.0.0" }, { name = "pytest-rerunfailures", specifier = ">=15.0" }, @@ -1960,7 +2085,7 @@ docs = [ [[package]] name = "opensandbox" -version = "0.1.13" +version = "0.1.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1968,9 +2093,9 @@ dependencies = [ { name = "pydantic" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/e2/d2716adfca0cce6af3913035372d2f8cbe6a836e595d4747e19481113439/opensandbox-0.1.13.tar.gz", hash = "sha256:eab4b6597b2941f0418fa186a377c15b01acb8549f24f84963e7c15434232977", size = 201287, upload-time = "2026-06-25T06:52:57.384Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/21/654a3d69815b09690e926d553f3f4a178640d1206000a1b49f5e22c8eb68/opensandbox-0.1.15.tar.gz", hash = "sha256:017abc9b399b88da51bf077d6fb94ee89b1783e601495e85ba85fed478fed1b0", size = 228729, upload-time = "2026-07-24T09:36:29.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/57/74459959e69e4271929ab3658c5abcbbf977977d126ddd7d861bfde4d0e5/opensandbox-0.1.13-py3-none-any.whl", hash = "sha256:8decd1eb952a3b539fecaf443b025c24d6d4a535f62242abd11644bc20505570", size = 491625, upload-time = "2026-06-25T06:52:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5c/ab87ea696531210790feb8f575471036fccba29dedaff201736f15bbb3a7/opensandbox-0.1.15-py3-none-any.whl", hash = "sha256:992b01490551f4d8e3f99caa25e34cb9d1690f0c5027eeebab912738291957d1", size = 538522, upload-time = "2026-07-24T09:36:28.277Z" }, ] [[package]] @@ -1988,13 +2113,16 @@ wheels = [ [[package]] name = "opensandbox-server" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docker" }, { name = "fastapi" }, { name = "httpx", extra = ["socks"] }, { name = "kubernetes" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, @@ -2005,9 +2133,9 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/0e/3f8cfff895fb77ef0db0622f81c6e0c27fb645d5e73a10beb088dc99294b/opensandbox_server-0.2.1.tar.gz", hash = "sha256:7cea6dcb816f28b4e2686c70ec68d3ee477ae2c4b6a7ad15a2ffc2485249ad99", size = 166552, upload-time = "2026-06-29T11:28:46.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/c8/ef8d1a831ac441864c1ace299733c5431394a35010f98e21e0e83a5860a4/opensandbox_server-0.2.2.tar.gz", hash = "sha256:fa82dde68dd407c67d153d31a84ed89be0d8ef6aba52e48b93877c0a215e18d9", size = 180835, upload-time = "2026-07-20T10:30:47.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/21/c8df684d2e3485692cb19fe987c8a9ca0d62bf26c1c142f5844657879d8d/opensandbox_server-0.2.1-py3-none-any.whl", hash = "sha256:884a630ebd32f9e9dec8c0195fafd34dafa2b7b96539554efbe98639c95bb892", size = 246161, upload-time = "2026-06-29T11:28:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1a/c44162539fa348f90ea4a28725d25cae073a8ab10a1c6b126a37475f56c1/opensandbox_server-0.2.2-py3-none-any.whl", hash = "sha256:08de41d214b90860d4921b04cd6dcca101e8130f3ff8dd5c52e58f81c0fc3ef3", size = 267550, upload-time = "2026-07-20T10:30:45.957Z" }, ] [[package]] @@ -2141,7 +2269,7 @@ wheels = [ [[package]] name = "openviking" -version = "0.4.7" +version = "0.4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apscheduler" }, @@ -2158,7 +2286,6 @@ dependencies = [ { name = "lark-oapi" }, { name = "litellm" }, { name = "loguru" }, - { name = "markdownify" }, { name = "mcp" }, { name = "olefile" }, { name = "openai" }, @@ -2178,9 +2305,10 @@ dependencies = [ { name = "python-multipart" }, { name = "python-pptx" }, { name = "pyyaml" }, - { name = "readabilipy" }, { name = "requests" }, + { name = "scrapy" }, { name = "tabulate" }, + { name = "trafilatura" }, { name = "tree-sitter" }, { name = "tree-sitter-c-sharp" }, { name = "tree-sitter-cpp" }, @@ -2201,25 +2329,25 @@ dependencies = [ { name = "xlrd" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/ad/a9c44e4c7af316a95dc4c808f2b481df02f661227b0d7eb4c30a944c4e1e/openviking-0.4.7.tar.gz", hash = "sha256:5ff7b5a72988ca24b8c497578c4b238100622e6c351b01ebb6d2a0b6467626fd", size = 55450411, upload-time = "2026-07-02T13:46:19.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/07/f2a02e74784810f56a76f15115a7a5d1ac6ca4a982970540de4734e5cae3/openviking-0.4.11.tar.gz", hash = "sha256:4c18cb5c01b737b8ec9d0e9212c0ad54ef29f7f244aa7da79ffc21259d886317", size = 57716155, upload-time = "2026-07-23T07:36:43.763Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/77/9d47f6c326085b3eaac69caf219611afdb74cad56d404b0a741ab26afb54/openviking-0.4.7-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:51c8d3a8eb68772fb1d1b1bb57aaaf26ed654d2ff0213d8994a270d9c4ab544e", size = 18680099, upload-time = "2026-07-02T13:46:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/0a/18/b61d8d669319d6395d86d51eaf10b321e437dd0a5dc32367c4d3e2610aaa/openviking-0.4.7-cp310-abi3-macosx_15_0_x86_64.whl", hash = "sha256:9134f2921218740be697d68cc3c9db5f7e38bfcb53464e55800a74e8bbf33ed8", size = 21055402, upload-time = "2026-07-02T13:46:04.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ba/e98c4e0fbd2cc2a3c94cbe9816df1b94de582d19800087030d9029a10b0f/openviking-0.4.7-cp310-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:0cfdb0308a9cd37f23cec5424425962bf784a0f89f816f7d39f1abc2ebeb03da", size = 19729687, upload-time = "2026-07-02T13:46:07.77Z" }, - { url = "https://files.pythonhosted.org/packages/21/5a/6c0e6e3bc732c4e1335597102478608f9e6cdb582a64f68e5b91a34b8970/openviking-0.4.7-cp310-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:e98c13e1f8cf9c9c805323e86516ccf4ace94053e0b309e212e8b79b242350e3", size = 22227659, upload-time = "2026-07-02T13:46:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9f/7dd3cbd41d3a8fe23a504f474a5cef7e67a6b13084049a32536bf7191ee8/openviking-0.4.7-cp310-abi3-win_amd64.whl", hash = "sha256:da2a032dcae0f025ad2fe1df0d3d4e6d5d37f9263deef78b973e9d125ea0d08b", size = 24679040, upload-time = "2026-07-02T13:46:14.999Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/ea2dbb9fb03218785c81f0b1b380024e22d6af75d06bd42c91e43aa180e7/openviking-0.4.11-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:4a0389f2c3de0eb41cc2f26ccedb89566d8719393c3ab37d2ee2895cf9a8ebee", size = 18980965, upload-time = "2026-07-23T07:36:24.082Z" }, + { url = "https://files.pythonhosted.org/packages/9c/db/5bce6b007e0645c6c541974eb14b35329a5a0db6b96561df675c37d94d25/openviking-0.4.11-cp310-abi3-macosx_15_0_x86_64.whl", hash = "sha256:f3ed8d86917fe3cd94421019fb997bb874ef1f95fd97bada614a10b1516aba77", size = 21346489, upload-time = "2026-07-23T07:36:28.034Z" }, + { url = "https://files.pythonhosted.org/packages/19/9e/648b9528f157691f2df0c3673298af55390e85640bbb9405b28cbb21d5fd/openviking-0.4.11-cp310-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:ae67550704e2d30d1bc1b0f38c35101dc55b0492834021dcd1aee9792e78de2c", size = 20072135, upload-time = "2026-07-23T07:36:31.972Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9a/a0ab4a57ea6363b4849169d1ec282012ed60ea65e984007f50ce5a64deaa/openviking-0.4.11-cp310-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:db7d58a9689651cabe3978caca089eb3fee62cf8ea6521492309f25c56dc0d05", size = 22577257, upload-time = "2026-07-23T07:36:35.429Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/e2c41af0a5672ec79afe0e7e858a03d1c7edb513a81f0a69d1a0f32a096f/openviking-0.4.11-cp310-abi3-win_amd64.whl", hash = "sha256:858f3d7bf2ecb102744d6de704551f9df9b4fa4ab9adc2bdcdd500f8881bf6c8", size = 25023612, upload-time = "2026-07-23T07:36:38.955Z" }, ] [[package]] name = "openviking-sdk" -version = "0.1.3" +version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/46/8fe7438310208e1aaa99bad4db2393d56320c3a718db017d8b8ba4920515/openviking_sdk-0.1.3.tar.gz", hash = "sha256:032632f3d5b93a3781f070757295ba4891be11e1799d3cfd3ad5c84a908ff965", size = 30505, upload-time = "2026-07-03T02:49:05.798Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/9c/b75da8d956bbfaf39288dd3cf7c734ba9225fba34e44362d02fbdb9a871d/openviking_sdk-0.1.5.tar.gz", hash = "sha256:f017dec938267aaea659122165f2872e189933a5604ba6d9ec0ae867b558ecaf", size = 33652, upload-time = "2026-07-23T06:18:56.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/98/1f60859fc6a54b32ebbd4221ed5a777fe1c81828476959fe99a07ea2e1ca/openviking_sdk-0.1.3-py3-none-any.whl", hash = "sha256:20e94db83b7db58018d75a552b32a1bb5705a7b4ec48e2ea34829896921c562e", size = 20468, upload-time = "2026-07-03T02:49:04.625Z" }, + { url = "https://files.pythonhosted.org/packages/53/c0/481ec44e2f98cf1afd47d4c115756dd4b1fa674caa4b99b0f72723d6fc33/openviking_sdk-0.1.5-py3-none-any.whl", hash = "sha256:f29af1c56e1a4477c881d584fa5178b5a34600847926cfe21fddd333ccb576ec", size = 21740, upload-time = "2026-07-23T06:18:54.864Z" }, ] [[package]] @@ -2231,6 +2359,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "parsel" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cssselect" }, + { name = "jmespath" }, + { name = "lxml" }, + { name = "packaging" }, + { name = "w3lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/c8/4ace3a5c61e39ca21734a5715d0e076eea6200dd8daea2a5b99452f5a0d6/parsel-1.11.0.tar.gz", hash = "sha256:5925fe087eb16fc404a7ed91e31e2c1e2a9b230da4b64f34d81358c0d0e27e88", size = 106849, upload-time = "2026-01-29T07:19:23.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/23/4e0dae5e5bee14aea26dba003a682e621563451a20f751ed985810f818b6/parsel-1.11.0-py3-none-any.whl", hash = "sha256:bda82575df1774dd64e1c1396163f3cadb3e383e0f8080d43d45fa6705355daa", size = 14176, upload-time = "2026-01-29T07:19:22.255Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -2443,6 +2587,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "protego" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/1b/6b0ee60bb1561843bfc62f5c7c3cb1ef6147b47e829a5fd6b7fcd2752471/protego-0.6.2.tar.gz", hash = "sha256:88ff004544ce44e61269cc6f8735f7837d12e09bb77619fc94fbb26b72e5d137", size = 3137854, upload-time = "2026-06-25T09:33:16.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/10/cbd3c06603bb8b3e0e871df24ee793a6e3b53d8c4cc8763f1b8b936649aa/protego-0.6.2-py3-none-any.whl", hash = "sha256:714de21d82527c9be900066c3211b266985dd6a19b6e70c57e033fc1a589f3ff", size = 10296, upload-time = "2026-06-25T09:33:14.771Z" }, +] + [[package]] name = "protobuf" version = "6.33.6" @@ -2707,16 +2860,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] @@ -2740,6 +2893,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/cd/e0eda602060f9dc99068f8e54490812d9d34ebb134043ff0ae594cf721a4/pydata_sphinx_theme-0.18.0-py3-none-any.whl", hash = "sha256:fbe5401f26642d487e3c5b6dfcbf69b3b1d579e80dcc479a429632abe0a13929", size = 6200747, upload-time = "2026-05-20T08:32:26.646Z" }, ] +[[package]] +name = "pydispatcher" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/db/030d0700ae90d2f9d52c2f3c1f864881e19cef8cba3b0a08759c8494c19c/PyDispatcher-2.0.7.tar.gz", hash = "sha256:b777c6ad080dc1bad74a4c29d6a46914fa6701ac70f94b0d66fbcfde62f5be31", size = 38891, upload-time = "2023-02-17T20:11:13.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/0e/9ee7bc0b48ec45d93b302fa2d787830dca4dc454d31a237faa5815995988/PyDispatcher-2.0.7-py3-none-any.whl", hash = "sha256:96543bea04115ffde08f851e1d45cacbfd1ee866ac42127d9b476dc5aefa7de0", size = 12040, upload-time = "2023-02-17T20:11:11.991Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -2766,6 +2928,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyopenssl" +version = "26.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024, upload-time = "2026-06-12T20:28:07.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" }, +] + [[package]] name = "pypdfium2" version = "5.8.0" @@ -2795,6 +2970,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/7a/f3dcefe6ee7389aad3ca1488c177e8fbf978206de21c7a99ccf487ea38ab/pypdfium2-5.8.0-py3-none-win_arm64.whl", hash = "sha256:3f17ed97ae8a5a1705301ca93af256a5b02f9009dee4e99c5e175831d46ebd7c", size = 3548362, upload-time = "2026-05-04T17:39:42.304Z" }, ] +[[package]] +name = "pypydispatcher" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7b/65f55513d3c769fd677f90032d8d8703e3dc17e88a41b6074d2177548bca/PyPyDispatcher-2.1.2.tar.gz", hash = "sha256:b6bec5dfcff9d2535bca2b23c80eae367b1ac250a645106948d315fcfa9130f2", size = 23224, upload-time = "2017-07-03T14:20:51.806Z" } + [[package]] name = "pytest" version = "9.0.3" @@ -2997,18 +3178,12 @@ wheels = [ ] [[package]] -name = "readabilipy" -version = "0.3.0" +name = "queuelib" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "html5lib" }, - { name = "lxml" }, - { name = "regex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/e4/260a202516886c2e0cc6e6ae96d1f491792d829098886d9529a2439fbe8e/readabilipy-0.3.0.tar.gz", hash = "sha256:e13313771216953935ac031db4234bdb9725413534bfb3c19dbd6caab0887ae0", size = 35491, upload-time = "2024-12-02T23:03:02.311Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f3/d80ab8c7c91b8c42d9a2aa4dd97a8be1321e7b26000c2675b75e641d958c/queuelib-1.9.0.tar.gz", hash = "sha256:b12fea79fd8c1dd23e212b1f3db58003b773949801d4f4e6f34d882467d4a192", size = 11729, upload-time = "2026-01-29T11:19:37.065Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/46/8a640c6de1a6c6af971f858b2fb178ca5e1db91f223d8ba5f40efe1491e5/readabilipy-0.3.0-py3-none-any.whl", hash = "sha256:d106da0fad11d5fdfcde21f5c5385556bfa8ff0258483037d39ea6b1d6db3943", size = 22158, upload-time = "2024-12-02T23:03:00.438Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1c/8df7b461497b42fcc1e7c44529201975ec77b0e1ebecd00df4b1f096c1d4/queuelib-1.9.0-py3-none-any.whl", hash = "sha256:c5fd3bebf2c924446fa94fca6b72e81168f79cf4c2a9143b8b26f266a423fcf3", size = 13585, upload-time = "2026-01-29T11:19:35.616Z" }, ] [[package]] @@ -3141,6 +3316,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + [[package]] name = "requests-oauthlib" version = "2.0.0" @@ -3332,6 +3519,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, ] +[[package]] +name = "scrapy" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "cssselect" }, + { name = "defusedxml" }, + { name = "itemadapter" }, + { name = "itemloaders" }, + { name = "lxml" }, + { name = "packaging" }, + { name = "parsel" }, + { name = "protego" }, + { name = "pydispatcher", marker = "platform_python_implementation == 'CPython'" }, + { name = "pyopenssl" }, + { name = "pypydispatcher", marker = "platform_python_implementation == 'PyPy'" }, + { name = "queuelib" }, + { name = "service-identity" }, + { name = "tldextract" }, + { name = "twisted" }, + { name = "w3lib" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/9f/9800513193aacb44825f0b9a75e4e6d4a0c416e9ed23ca3aac7374d03d17/scrapy-2.17.0.tar.gz", hash = "sha256:b536d14166b05e2183b19ae105a7af849cd14d4c1f70d3009d9b1828c0ab3d92", size = 1312077, upload-time = "2026-07-07T10:27:43.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e8/30fbfcb45b35da9e24c6f1ae193688dc834cef3fd576a41d3a08ef771125/scrapy-2.17.0-py3-none-any.whl", hash = "sha256:a650a59e9425e7e4b4d59083f858df34ec9a5a8292b3e35671d444fb56fe5085", size = 350445, upload-time = "2026-07-07T10:27:41.59Z" }, +] + +[[package]] +name = "service-identity" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/87/ad52e2c582c0f0e7f0a1b86950494c38d67422dc0f5ed9044a5fb9569a49/service_identity-26.1.0.tar.gz", hash = "sha256:6358c52882c96e66ac4a55eb3a72c7dd4a70763f8cc6fa4e70abde2656f4bf3b", size = 42898, upload-time = "2026-05-30T12:04:55.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/eb/2433e1af4ff903499144de4846569fb3300b816179ae99a03c2f011b666a/service_identity-26.1.0-py3-none-any.whl", hash = "sha256:68c32dadbb69135fb951077677e07cd7f6031020f3a8c8f47a28cda8a0742118", size = 11370, upload-time = "2026-05-30T12:04:53.911Z" }, +] + [[package]] name = "sgmllib3k" version = "1.0.0" @@ -3622,6 +3851,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, ] +[[package]] +name = "tld" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5d/76b4383ac4e5b5e254e50c09807b3e13820bed6d6c11cd540264988d6802/tld-0.13.2.tar.gz", hash = "sha256:d983fa92b9d717400742fca844e29d5e18271079c7bcfabf66d01b39b4a14345", size = 467175, upload-time = "2026-03-06T23:50:34.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/90/39a85a4b63c84213e78b3c17d22e1bf45328acf8ebb33ef93be30d0a3911/tld-0.13.2-py2.py3-none-any.whl", hash = "sha256:9b8fdbdb880e7ba65b216a4937f2c94c49a7226723783d5838fc958ac76f4e0c", size = 296743, upload-time = "2026-03-06T23:50:32.465Z" }, +] + +[[package]] +name = "tldextract" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/7b/644fbbb49564a6cb124a8582013315a41148dba2f72209bba14a84242bf0/tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb", size = 126105, upload-time = "2025-12-28T23:58:05.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/42/0e49d6d0aac449ca71952ec5bae764af009754fcb2e76a5cc097543747b3/tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9", size = 105886, upload-time = "2025-12-28T23:58:04.071Z" }, +] + [[package]] name = "tokenizers" version = "0.23.1" @@ -3697,6 +3950,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "trafilatura" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "courlan" }, + { name = "htmldate" }, + { name = "justext" }, + { name = "lxml" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/19/24833e905df2d80e3bb67424f95febcc17709a1f61a522120bc438afca70/trafilatura-2.1.0.tar.gz", hash = "sha256:f689e2116fc89c7bc0b9a296d01dcfe2eb0b5455f8c371a77dc0db1f06a05643", size = 263876, upload-time = "2026-06-07T17:43:31.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/78/4ad99d79aee2784f49f20fd0a29058ce4c032fe4439047924c43521cd211/trafilatura-2.1.0-py3-none-any.whl", hash = "sha256:0eded5207a806445ddebbe36eae30b9035fe6a2f233c36f6fe82663fca8b9d30", size = 134600, upload-time = "2026-06-07T17:43:28.404Z" }, +] + [[package]] name = "tree-sitter" version = "0.25.2" @@ -3888,6 +4159,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, ] +[[package]] +name = "twisted" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/97/6e9beb1e78247ae6dc34114f27d538cf2cb183c4afcd3609dfdf2b0439c8/twisted-26.4.0.tar.gz", hash = "sha256:dbfd0fe1ee409d0243fdd7a6a6ff14f4948cec1fd78e0376291f805e1501fae9", size = 3575095, upload-time = "2026-05-11T11:24:51.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/57/bcf4e2370dd218c9aa68a9140a65d86729c73f1d529f7e94786c2766fc72/twisted-26.4.0-py3-none-any.whl", hash = "sha256:dc25ea0ebf6511c24f03232ee9f4afa54b291c5d897990e3a39cc4d14a1ef4c0", size = 3230362, upload-time = "2026-05-11T11:24:49.5Z" }, +] + [[package]] name = "typer" version = "0.25.1" @@ -4052,6 +4341,15 @@ ark = [ { name = "pydantic" }, ] +[[package]] +name = "w3lib" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/91/b2eb59c2cf243de5de1e91c963655df78c015509f51297685a8c86a27b8c/w3lib-2.4.1.tar.gz", hash = "sha256:8dd69ee39ff6398d708c793abc779c334a69bac7cee1cdf71736c669ed6be864", size = 48494, upload-time = "2026-03-20T09:50:27.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/c3/f8b216cbd742e5b84c40f045204c764ccb7524d2aeab021054ec69446b0a/w3lib-2.4.1-py3-none-any.whl", hash = "sha256:40930132907e68de906a5b89331ab8c8ff4f01bd35b5539ef7896017d814138d", size = 21695, upload-time = "2026-03-20T09:50:26.187Z" }, +] + [[package]] name = "watchfiles" version = "1.2.0" @@ -4133,15 +4431,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -] - [[package]] name = "websocket-client" version = "1.9.0" @@ -4496,3 +4785,38 @@ sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0 wheels = [ { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] + +[[package]] +name = "zope-interface" +version = "8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/dc/50550cfcbb2ea3cbca5f1d7ed05c8aa840f831a0f2d63aec0a953f7c590e/zope_interface-8.5.tar.gz", hash = "sha256:7a3ba1c5877f0f3e3906b02ddf793abed2becc2948116414ce0e1dd820b68d6d", size = 257957, upload-time = "2026-05-26T06:50:14.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/43/9cd98bee951d23848de690ba2809f87e3b22c67c370987acc960da15ad37/zope_interface-8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0c8aa2bf8f3911ef37b87deb1bbe225a310e6eb6522a16d77f5d8330c4f6fbe", size = 210951, upload-time = "2026-05-26T06:49:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/17/0f/8f1a29966bcf863e3a2121edcafb81c55715de7886bcc9544749cc79e7da/zope_interface-8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:efe234a0fafb4b6b1602e9be9245b97c2bf06d67c07af5a4bc3c0438978b555c", size = 211309, upload-time = "2026-05-26T06:49:02.732Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9f/37e564eaaf85e3abc1ada40a79fa43f2ab45bdb67431b0ec0fe29e4763e2/zope_interface-8.5-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dabeb6fe1228d411994f300811edc6866fff0cdcbc9cef98a78f05ea0da42e37", size = 254881, upload-time = "2026-05-26T06:49:04.303Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/e6501d8ea7a2cac3217e03f404e1f98c1df7191d83cfe86b1895fbba5dac/zope_interface-8.5-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:147a9442dcc2b7339ecdb1be2b3cdb098e90462e39425054053ebfb50d99125a", size = 259811, upload-time = "2026-05-26T06:49:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/bfa25ef480b02af6e9452c478483fec75e87c9e2b60c407fd0b1f6054b9c/zope_interface-8.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a17e681224267880707c9ec9e730ad9a1ad2d65c371256843efba6cf48711b58", size = 260358, upload-time = "2026-05-26T06:49:08.317Z" }, + { url = "https://files.pythonhosted.org/packages/64/51/2b518072fea76242da64451d501c69b7b5ccdef9b57fead584ccf1c180d5/zope_interface-8.5-cp310-cp310-win_amd64.whl", hash = "sha256:d178968a1a611df30549a717d1624cb38ca810347339e3e37b7baa6f6781a170", size = 214822, upload-time = "2026-05-26T06:49:10.441Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/83ad110fb847413affe71609bb50e59e1aa082e1236030122227c7c283d3/zope_interface-8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:afc66ccaef2a3c0bef6ca02aad40d29a39276389dad16a8eac36f9f385e4d057", size = 211426, upload-time = "2026-05-26T06:49:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a7/6b6e0c31ac240cb9fc015ae9ed45ca54be886c18fcf7bfa2377a4d7a8785/zope_interface-8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28044972187245d7a309e4699319bfdbd2ffcbf7176d1d4ddf5adffb2dea80f", size = 211850, upload-time = "2026-05-26T06:49:14.474Z" }, + { url = "https://files.pythonhosted.org/packages/37/36/7599ecabcf80ce4fef2e1ef3c5ac0d4696b61f03f724cc44022f4d226af9/zope_interface-8.5-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03bbecc7982af713d7499d4084bc03916413d17ffd45f89009348cc0c1d9e376", size = 260711, upload-time = "2026-05-26T06:49:16.568Z" }, + { url = "https://files.pythonhosted.org/packages/03/3e/1774b0ee46ccbb5498ee3c33ece40315b6ef58bc71957be94bd345340bc1/zope_interface-8.5-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf917009a4a7457c7290225a019f4a0aa706d96accd2cfdba2418d3bc1fcde2f", size = 265277, upload-time = "2026-05-26T06:49:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/b6/09/e533b2ffabaae4e5d5730d6768a591cf335defe8e37bec2ad905d09be656/zope_interface-8.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31cff25b2aaedb5267e6e77b1e9be6b0ec4f622032de8a069202b8ffacda7dc2", size = 266369, upload-time = "2026-05-26T06:49:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/49/4a/3ebe6a4c122b2d5340db45cbe7e490663d3228b172710ec71060cd5d541e/zope_interface-8.5-cp311-cp311-win_amd64.whl", hash = "sha256:17a3114bbdddb5e75e5784cdf318944636190cbbc72d357ef9fb1a8b0351f955", size = 215161, upload-time = "2026-05-26T06:49:21.799Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/056ad97af5b16db1975ee98ec7ab03d2ce3f3355efad904ced1dbce0e39f/zope_interface-8.5-cp311-cp311-win_arm64.whl", hash = "sha256:aab6bb5bee10f38ea688b95ba054396b67f613552d2c8378be7fcb2d2fba7646", size = 213481, upload-time = "2026-05-26T06:49:25.085Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/b84123a948f3162a34623e188922827cd845244fdd043ed20f8d02228caa/zope_interface-8.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8e6ee90c2e6de7c37058d5fa41f123c8b13a312db8d1e0fb5840d7f4bcdff9c9", size = 212165, upload-time = "2026-05-26T06:49:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/cbceec44f1b27208a76c1a688c131302685852406a23df5aab68324109cc/zope_interface-8.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1adc90d3576b3b4c4de4953e6002c37bef28b78d7fa54c1bbfd0c50f022fe7c", size = 212341, upload-time = "2026-05-26T06:49:28.182Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c3/005032195ff3b210c139b7c560ed5c534e844b0907d8e44d2b3d8919305e/zope_interface-8.5-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e6347b8d8d12c5eca6502450a92be30079b7acfade2c4f693efa0deb8871b06e", size = 265296, upload-time = "2026-05-26T06:49:29.741Z" }, + { url = "https://files.pythonhosted.org/packages/c5/66/1036543d6a66bc04c19df3cf650f3ad938a002ab0a443c24e23e8de5e8b9/zope_interface-8.5-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e970dabea777a24b0b0bbf9dae3ab75ce8b2d8e948edf4875627034b21f3560", size = 270689, upload-time = "2026-05-26T06:49:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/30/4c/8b56259558cace4414e753ca6740396a1f59d4a95ddb55b4658600408670/zope_interface-8.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0b48ccadaa9839e09ff81e969703cecb3f402c813bfe8b958652e699bea69f5", size = 270280, upload-time = "2026-05-26T06:49:33.489Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/649908c83aa8fdb7faf2ddca4d3cf6fb8f2157121267dc56e8f72681e26c/zope_interface-8.5-cp312-cp312-win_amd64.whl", hash = "sha256:e0e311f1277468c08fd59a2b41f71b43d25dff639789d364747acd1705c0df6e", size = 215019, upload-time = "2026-05-26T06:49:35.607Z" }, + { url = "https://files.pythonhosted.org/packages/9f/97/da13037b4c563e4df32eedbc819f8c00b754af494f68211e3dffd48d52da/zope_interface-8.5-cp312-cp312-win_arm64.whl", hash = "sha256:652b73107a04159ec6c020db6c1543d4f1e8f4d069bd2aac88a947820923517b", size = 213569, upload-time = "2026-05-26T06:49:37.317Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/4c15755d701f2ec0e80d64a18e1ebaf5be2c584c0ec153fd516f5d13eada/zope_interface-8.5-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:28e80457c134d1fa57a7d758004dece348654e1b1467ac22dcdc20fc1d127c52", size = 212512, upload-time = "2026-05-26T06:49:38.996Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/4360c54c465db042cc8fbeeec92abac28b4cedbf6ba63c1f092fd08a190f/zope_interface-8.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09495ce9d559c06b70f2d4855b3e4f48a822a9ddc8be1d30c5b4e5be14ae1ace", size = 212541, upload-time = "2026-05-26T06:49:41.186Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a5/692a2b8d70f78e848793231d5fae5fecbf8d0cccd73430fdc34802a6d3c1/zope_interface-8.5-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7849ad8fa90763cc1087f4dda78ca3a233e950b3e08fac7079297c9cafbbd7bb", size = 265191, upload-time = "2026-05-26T06:49:43.449Z" }, + { url = "https://files.pythonhosted.org/packages/70/8d/454a9cfc7a050c394ab4f11b3371f7897828b7415e096afff724637e65e0/zope_interface-8.5-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5578c9421ca409a1f39f153d6f7803e4cde01da592ec75a9ac5e1b777d18d33b", size = 270626, upload-time = "2026-05-26T06:49:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/db8409cfa3575b8e9b4800babd7d49f8228433cd1f0c56814bd0ada49c33/zope_interface-8.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1bd7d96b4ca5fa311f54c9eac16dce4886b428c1531dbe06067763ccdf123b4", size = 270444, upload-time = "2026-05-26T06:49:47.025Z" }, + { url = "https://files.pythonhosted.org/packages/4a/df/a386940e41469ef615e100a216d8b386521e9e598817147f87932ca203c4/zope_interface-8.5-cp313-cp313-win_amd64.whl", hash = "sha256:0c8123d2a4dfde2a613c7cb772605477724782c20bc2e0ad1d9435376a6a44a3", size = 215021, upload-time = "2026-05-26T06:49:48.478Z" }, + { url = "https://files.pythonhosted.org/packages/89/75/477eb5669b6b2a7a843decd1a075e9b1971a8720017654143a7183abd3d9/zope_interface-8.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d02be14f3173c6c7288bc2fdf530090c01c3cf8764ad46c68024686f364278e", size = 213610, upload-time = "2026-05-26T06:49:50.01Z" }, +] From 51a26c4a23850876548c61340da2d2da3bc834ce Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Tue, 28 Jul 2026 12:17:47 +0800 Subject: [PATCH 13/22] fix: harden v2 durable execution and release gates --- .github/workflows/ci-build.yml | 4 +- .github/workflows/ci-lint.yml | 8 +- .github/workflows/ci-shellcheck.yml | 4 +- .github/workflows/ci-test-fast.yml | 8 +- .github/workflows/ci-test-opensandbox.yml | 4 +- .github/workflows/ci-test-openviking.yml | 4 +- .github/workflows/ci-v2-production.yml | 43 +- README.md | 29 + deploy/compose/compose.yaml | 6 +- deploy/docs/api-governance-v2.md | 31 +- deploy/docs/migration-v2.md | 29 + deploy/docs/openapi-v2.json | 2537 +++++++++++++++++ deploy/docs/operations-v2.md | 29 + deploy/kubernetes/openrath.yaml | 61 +- examples/v2_server_app.py | 20 +- pyproject.toml | 4 + scripts/capacity_v2.py | 23 +- scripts/export_openapi_v2.py | 35 + scripts/migrate_v1_to_v2.py | 43 +- src/rath/adapters/__init__.py | 18 +- src/rath/adapters/context.py | 82 +- src/rath/adapters/memory.py | 23 +- src/rath/adapters/provider.py | 23 +- src/rath/adapters/sandbox.py | 23 +- src/rath/adapters/schema.py | 15 +- src/rath/adapters/specs.py | 19 +- src/rath/adapters/tool.py | 102 +- src/rath/artifacts/store.py | 92 +- src/rath/client/__init__.py | 1 - src/rath/client/remote.py | 16 +- src/rath/context.py | 1 - src/rath/definition/__init__.py | 1 - src/rath/definition/compiler.py | 55 +- src/rath/definition/decorators.py | 1 - src/rath/definition/model.py | 15 +- src/rath/deployment/revisions.py | 41 +- src/rath/errors.py | 1 - src/rath/eval/models.py | 1 - src/rath/eval/runner.py | 1 - src/rath/eval/store.py | 7 +- src/rath/events.py | 1 - src/rath/observability/otel.py | 8 +- src/rath/observability/redaction.py | 1 - src/rath/runtime/__init__.py | 13 +- src/rath/runtime/effects.py | 147 +- src/rath/runtime/execution.py | 139 + src/rath/runtime/local.py | 272 +- .../postgres/0002_release_hardening.sql | 26 + src/rath/runtime/postgres.py | 321 ++- src/rath/runtime/signals.py | 15 +- src/rath/runtime/sqlite.py | 150 +- src/rath/runtime/store.py | 25 +- src/rath/security/context.py | 1 - src/rath/security/policy.py | 5 +- src/rath/server/app.py | 947 +++++- src/rath/server/auth.py | 4 +- src/rath/server/authorization.py | 34 + src/rath/server/cli.py | 24 +- src/rath/server/resources.py | 15 +- tests/artifacts/test_artifact_store.py | 63 +- tests/backends/test_local.py | 4 +- tests/backends/test_opensandbox_async.py | 13 +- .../conformance/v2/test_adapter_contracts.py | 163 +- tests/core/test_events.py | 1 - tests/definition/test_compiler_v2.py | 19 + .../test_definition_identity_regression.py | 38 + tests/definition/test_plan_serialization.py | 1 - tests/deployment/test_reference_manifests.py | 37 + tests/deployment/test_revisions.py | 23 + tests/eval/test_runner.py | 9 +- tests/integration/test_postgres_run_store.py | 19 +- .../integration/test_v1_migration_postgres.py | 1 + tests/memory/backends/conftest.py | 2 +- tests/migration/test_v1_to_v2.py | 36 + tests/runtime/test_effect_ledger.py | 60 +- tests/runtime/test_local_runtime.py | 5 +- tests/runtime/test_review_regressions.py | 209 ++ tests/runtime/test_run_state.py | 1 - tests/runtime/test_signals.py | 5 + tests/runtime/test_sqlite_run_store.py | 37 +- tests/security/test_context.py | 1 - tests/security/test_policy.py | 1 - tests/security/test_secrets_audit.py | 1 - tests/server/test_agent_server.py | 135 +- tests/server/test_openapi_contract.py | 30 + tests/unit/test_errors_v2.py | 1 - uv.lock | 289 +- 87 files changed, 6053 insertions(+), 759 deletions(-) create mode 100644 deploy/docs/openapi-v2.json create mode 100644 scripts/export_openapi_v2.py create mode 100644 src/rath/runtime/execution.py create mode 100644 src/rath/runtime/migrations/postgres/0002_release_hardening.sql create mode 100644 src/rath/server/authorization.py create mode 100644 tests/definition/test_definition_identity_regression.py create mode 100644 tests/deployment/test_reference_manifests.py create mode 100644 tests/runtime/test_review_regressions.py create mode 100644 tests/server/test_openapi_contract.py diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5a8f154..9ceb51e 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -22,8 +22,8 @@ jobs: name: Build Package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: '3.12' - run: uv build diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 14de1e9..1edeefe 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -30,8 +30,8 @@ jobs: name: Ruff runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: '3.12' - run: uv sync --dev --frozen @@ -42,8 +42,8 @@ jobs: name: MyPy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: '3.12' - run: uv sync --dev --extra postgres --extra redis --extra otel --frozen diff --git a/.github/workflows/ci-shellcheck.yml b/.github/workflows/ci-shellcheck.yml index 155c69c..f2ac0d8 100644 --- a/.github/workflows/ci-shellcheck.yml +++ b/.github/workflows/ci-shellcheck.yml @@ -20,9 +20,9 @@ jobs: name: ShellCheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Run ShellCheck - uses: ludeeus/action-shellcheck@master + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0 with: scandir: './scripts' env: diff --git a/.github/workflows/ci-test-fast.yml b/.github/workflows/ci-test-fast.yml index e5000be..92ec698 100644 --- a/.github/workflows/ci-test-fast.yml +++ b/.github/workflows/ci-test-fast.yml @@ -38,8 +38,8 @@ jobs: matrix: python-version: ['3.10', '3.13'] steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: ${{ matrix.python-version }} - run: uv sync --dev --frozen @@ -56,8 +56,8 @@ jobs: matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: ${{ matrix.python-version }} - run: uv sync --dev --frozen diff --git a/.github/workflows/ci-test-opensandbox.yml b/.github/workflows/ci-test-opensandbox.yml index 414c99a..76730a6 100644 --- a/.github/workflows/ci-test-opensandbox.yml +++ b/.github/workflows/ci-test-opensandbox.yml @@ -33,8 +33,8 @@ jobs: name: pytest (opensandbox) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: '3.12' # setup-uv's post-job "Pruning cache" step has been hanging for diff --git a/.github/workflows/ci-test-openviking.yml b/.github/workflows/ci-test-openviking.yml index d8458a8..a773e64 100644 --- a/.github/workflows/ci-test-openviking.yml +++ b/.github/workflows/ci-test-openviking.yml @@ -38,8 +38,8 @@ jobs: # VLM providers; allow failure in PRs until the CI environment is stable. continue-on-error: ${{ github.event_name == 'pull_request' }} steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 with: python-version: '3.12' # When repository secrets are absent we skip uv sync; without this diff --git a/.github/workflows/ci-v2-production.yml b/.github/workflows/ci-v2-production.yml index 8fcecaa..6ea9180 100644 --- a/.github/workflows/ci-v2-production.yml +++ b/.github/workflows/ci-v2-production.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: postgres:17-alpine + image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193 env: POSTGRES_HOST_AUTH_METHOD: trust ports: @@ -24,7 +24,7 @@ jobs: --health-timeout 3s --health-retries 12 redis: - image: redis:8-alpine + image: redis:8-alpine@sha256:8096655e437712b07503796fb64d81359256cfcff0ab29d95a7da72863786efb ports: - 56379:6379 env: @@ -34,18 +34,31 @@ jobs: OPENRATH_TEST_S3_ACCESS_KEY: openrathtest OPENRATH_TEST_S3_SECRET_KEY: openrath-test-secret steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v6 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 - name: Start S3-compatible object store run: >- docker run -d --name minio -p 59000:9000 -e MINIO_ROOT_USER=openrathtest -e MINIO_ROOT_PASSWORD=openrath-test-secret - minio/minio:RELEASE.2025-09-07T16-13-09Z server /data - - run: uv sync --extra postgres --extra server --extra s3 --extra redis --extra otel + minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e + server /data - run: uv lock --check + - run: uv sync --frozen --extra postgres --extra server --extra s3 --extra redis --extra otel - run: uv run ruff check src tests scripts examples + - run: uv run ruff format --check src tests example - run: uv run mypy src/rath + - name: Audit exact production dependency set + run: | + uv export --frozen --no-dev --no-emit-project \ + --extra server --extra postgres --extra s3 --extra redis --extra otel \ + --output-file production-requirements.txt + uvx pip-audit --no-deps --disable-pip -r production-requirements.txt + - name: Audit every published extra and development group + run: | + uv export --frozen --all-extras --all-groups --no-emit-project \ + --output-file all-requirements.txt + uvx pip-audit --no-deps --disable-pip -r all-requirements.txt - run: uv run pytest -q -n auto -m "not live_llm and not opensandbox and not openviking" - run: uv run python scripts/soak_v2.py --duration-seconds 10 --max-runs 500 - run: uv build @@ -58,29 +71,35 @@ jobs: docker compose -f deploy/compose/compose.yaml config --quiet docker run --rm \ -v "$PWD/deploy/kubernetes:/manifests:ro" \ - ghcr.io/yannh/kubeconform:v0.7.0 \ + ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c \ -strict -summary /manifests container: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: docker/Dockerfile push: false load: true tags: openrath:review - - uses: aquasecurity/trivy-action@0.32.0 + - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 with: image-ref: openrath:review severity: CRITICAL,HIGH exit-code: "1" ignore-unfixed: true - - uses: aquasecurity/trivy-action@0.32.0 + - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 with: image-ref: openrath:review format: cyclonedx output: openrath-v2-sbom.cdx.json + - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 + with: + scan-type: fs + scan-ref: . + scanners: secret + exit-code: "1" diff --git a/README.md b/README.md index 0e990d7..9a485de 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,35 @@ Most agent frameworks begin with an agent loop. OpenRath begins with **Session** OpenRath is designed for this: many agents collaborating across many branchable sessions, while still tracing every role, workspace, memory write, and final output. +## v2.0.0 durable runtime (unreleased) + +The v2 candidate adds explicit `@step` / `@router` execution plans, durable +Runs and checkpoints, effect reconciliation, tenant-scoped Agent Server APIs, +and governed Provider/Tool/Sandbox/Memory adapters. The HTTP contract is +currently **Beta**; v1 JSONL imports are historical and cannot resume an active +Run. + +Embedded mode is intended for a trusted process. Agent Server mode is the +strict durable profile: tokens need explicit action grants, object access is +tenant/project scoped, and synchronous steps cannot declare a preemptive +timeout. Use an async step or isolated executor for enforceable deadlines. + +```python +runtime = LocalRuntime( + store, + effect_ledger=ledger, + production_mode=True, +) +server = AgentServer(store, runtime, auth=auth, audit_sink=audit) +``` + +Production PostgreSQL schema migration is a separate operation: +`openrath-migrate` followed by `openrath-migrate --check`. Runtime identities +do not need DDL privileges. See +[`deploy/docs/operations-v2.md`](deploy/docs/operations-v2.md), +[`deploy/docs/migration-v2.md`](deploy/docs/migration-v2.md), and the generated +[`deploy/docs/openapi-v2.json`](deploy/docs/openapi-v2.json). +

Multi-Agent Multi-Session Map

diff --git a/deploy/compose/compose.yaml b/deploy/compose/compose.yaml index 594a4ba..83ee18f 100644 --- a/deploy/compose/compose.yaml +++ b/deploy/compose/compose.yaml @@ -78,7 +78,7 @@ services: cap_drop: [ALL] postgres: - image: postgres:17-alpine + image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193 environment: POSTGRES_DB: openrath POSTGRES_USER: openrath @@ -93,7 +93,7 @@ services: restart: unless-stopped redis: - image: redis:8-alpine + image: redis:8-alpine@sha256:8096655e437712b07503796fb64d81359256cfcff0ab29d95a7da72863786efb command: ["redis-server", "--save", "", "--appendonly", "no"] profiles: ["signals"] healthcheck: @@ -110,7 +110,7 @@ services: cap_drop: [ALL] minio: - image: minio/minio:RELEASE.2025-09-07T16-13-09Z + image: minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e command: ["server", "/data", "--console-address", ":9001"] profiles: ["artifacts"] environment: diff --git a/deploy/docs/api-governance-v2.md b/deploy/docs/api-governance-v2.md index c7fbd84..c3fbdda 100644 --- a/deploy/docs/api-governance-v2.md +++ b/deploy/docs/api-governance-v2.md @@ -14,8 +14,35 @@ when the repository owner approves the release. - **Experimental**: explicitly labelled research integrations and extension hooks. They may change in a minor release and must not be required for the durable Run, security, or storage contracts. -- Unlabelled public APIs are treated as Stable. Internal modules and names - beginning with `_` are not public contracts. +- Unlabelled v2 additions are not Stable. They remain Experimental until a + release note and this policy explicitly classify them. Internal modules and + names beginning with `_` are not public contracts. + +The Agent Server OpenAPI document currently labels `/v1` operations **Beta**. +Stable error codes and persisted fields may be promoted independently only +after the RC evidence gate passes. + +## Action and object authorization + +Authentication alone grants no access. Tokens carry explicit action grants; +`*` is an intentionally privileged reference-only grant. The service enforces +the following minimum actions: + +| Resource | Read | Mutate/control | +| --- | --- | --- | +| Assistant | `assistant.read` | `assistant.create` | +| Session | `session.read` | `session.create` | +| Run | `run.read` | `run.create`, `run.cancel`, `run.resume` | +| Interrupt | `interrupt.read` | `interrupt.decide` | +| Feedback | — | `feedback.create` | +| Memory | `memory.search` | `memory.put`, `memory.delete` | +| Metrics | `metrics.read` | — | + +Run, Session, Interrupt, Feedback, and Memory operations also verify tenant and +project scope. A user-scoped memory namespace cannot name another principal +unless the token has `memory.admin`. Cross-scope objects are returned as not +found to avoid disclosing their existence. Control-plane mutations emit +redacted security audit events when an `AuditSink` is configured. ## SemVer and deprecation diff --git a/deploy/docs/migration-v2.md b/deploy/docs/migration-v2.md index 4e065fd..486ceb2 100644 --- a/deploy/docs/migration-v2.md +++ b/deploy/docs/migration-v2.md @@ -37,6 +37,35 @@ active v2 Run. The import is idempotent per legacy Session ID. Imported content carries `provenance=legacy-import`, `trust=untrusted`, and `resumable=false`. Remote sandbox identities are not reattached. Credentials are not copied. +An invalid filename or malformed Session is isolated into the JSON report and +does not abort the remaining batch. + +On Windows PowerShell, use the same dry-run/apply split: + +```powershell +uv run python scripts/migrate_v1_to_v2.py ` + --source C:\data\v1\sessions ` + --report .\migration-inventory.json ` + --tenant TENANT_ID + +uv run python scripts/migrate_v1_to_v2.py ` + --source C:\data\v1\sessions ` + --report .\migration-result.json ` + --tenant TENANT_ID ` + --apply ` + --postgres-dsn $env:OPENRATH_POSTGRES_DSN ` + --artifact-root C:\data\openrath-artifacts +``` + +Run database schema migration separately with a DDL-capable identity: + +```bash +openrath-migrate +openrath-migrate --check +``` + +`PostgresRunStore` does not auto-migrate by default; API and worker roles need +only runtime DML privileges. ## Rollback diff --git a/deploy/docs/openapi-v2.json b/deploy/docs/openapi-v2.json new file mode 100644 index 0000000..4767f91 --- /dev/null +++ b/deploy/docs/openapi-v2.json @@ -0,0 +1,2537 @@ +{ + "components": { + "schemas": { + "Assistant": { + "properties": { + "id": { + "type": "string" + }, + "kind": { + "enum": [ + "template", + "alias" + ], + "type": "string" + }, + "revision_id": { + "format": "uuid", + "type": "string" + }, + "template_id": { + "type": "string" + } + }, + "required": [ + "id", + "template_id", + "revision_id", + "kind" + ], + "type": "object" + }, + "CreateAssistantRequest": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "template_id": { + "type": "string" + } + }, + "required": [ + "id", + "template_id" + ], + "type": "object" + }, + "CreateRunRequest": { + "additionalProperties": false, + "properties": { + "assistant_id": { + "type": "string" + }, + "priority": { + "type": "integer" + }, + "session_id": { + "format": "uuid", + "type": "string" + }, + "state": { + "type": "object" + } + }, + "required": [ + "assistant_id", + "session_id" + ], + "type": "object" + }, + "CreateSessionRunRequest": { + "additionalProperties": false, + "properties": { + "assistant_id": { + "type": "string" + }, + "priority": { + "type": "integer" + }, + "state": { + "type": "object" + } + }, + "required": [ + "assistant_id" + ], + "type": "object" + }, + "ErrorResponse": { + "properties": { + "error": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "Feedback": { + "additionalProperties": true, + "type": "object" + }, + "FeedbackRequest": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "run_id": { + "format": "uuid", + "type": "string" + }, + "score": { + "maximum": 1, + "minimum": -1, + "type": [ + "number", + "null" + ] + }, + "value": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "run_id", + "key" + ], + "type": "object" + }, + "Health": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Info": { + "additionalProperties": true, + "type": "object" + }, + "InterruptDecisionRequest": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "approve", + "edit", + "reject" + ], + "type": "string" + }, + "payload": { + "type": "object" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "kind", + "reason" + ], + "type": "object" + }, + "ItemPage": { + "properties": { + "items": { + "items": {}, + "type": "array" + }, + "next": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "items", + "next" + ], + "type": "object" + }, + "MemoryRequest": { + "additionalProperties": false, + "properties": { + "agent_id": { + "type": "string" + }, + "payload": { + "type": "object" + }, + "session_id": { + "type": "string" + }, + "tenant_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + }, + "type": "object" + }, + "ResumeRunRequest": { + "additionalProperties": false, + "properties": { + "confirm": { + "const": true + } + }, + "required": [ + "confirm" + ], + "type": "object" + }, + "Run": { + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "next_nodes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "plan_id": { + "format": "uuid", + "type": "string" + }, + "revision_id": { + "format": "uuid", + "type": "string" + }, + "session_id": { + "format": "uuid", + "type": "string" + }, + "state": { + "type": "object" + }, + "status": { + "enum": [ + "queued", + "running", + "waiting", + "succeeded", + "failed", + "cancelled", + "timed_out", + "needs_review" + ], + "type": "string" + }, + "version": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "id", + "plan_id", + "revision_id", + "session_id", + "status", + "version" + ], + "type": "object" + }, + "Session": { + "properties": { + "created_at": { + "format": "date-time", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "runs": { + "items": { + "$ref": "#/components/schemas/Run" + }, + "type": "array" + }, + "tenant_id": { + "type": "string" + } + }, + "required": [ + "id", + "tenant_id", + "created_at" + ], + "type": "object" + } + }, + "securitySchemes": { + "bearerAuth": { + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "description": "Beta v2 durable Agent Server API.", + "title": "OpenRath Agent Server", + "version": "2.0.0-unreleased" + }, + "openapi": "3.1.0", + "paths": { + "/health/live": { + "get": { + "operationId": "live", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [], + "summary": "Process liveness", + "x-openrath-action": null, + "x-openrath-stability": "beta" + } + }, + "/health/ready": { + "get": { + "operationId": "ready", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [], + "summary": "Dependency readiness", + "x-openrath-action": null, + "x-openrath-stability": "beta" + } + }, + "/info": { + "get": { + "operationId": "info", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Info" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [], + "summary": "Server capabilities", + "x-openrath-action": null, + "x-openrath-stability": "beta" + } + }, + "/metrics": { + "get": { + "operationId": "metrics", + "responses": { + "200": { + "content": { + "text/plain": { + "schema": { + "type": "object" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Prometheus metrics", + "x-openrath-action": "metrics.read", + "x-openrath-stability": "beta" + } + }, + "/v1/assistants": { + "get": { + "operationId": "listAssistants", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemPage" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "List deployment templates and tenant aliases", + "x-openrath-action": "assistant.read", + "x-openrath-stability": "beta" + }, + "post": { + "operationId": "createAssistantAlias", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAssistantRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Assistant" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Create a tenant assistant alias", + "x-openrath-action": "assistant.create", + "x-openrath-stability": "beta" + } + }, + "/v1/assistants/{assistant_id}": { + "get": { + "operationId": "getAssistant", + "parameters": [ + { + "in": "path", + "name": "assistant_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Assistant" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Get an assistant template or alias", + "x-openrath-action": "assistant.read", + "x-openrath-stability": "beta" + } + }, + "/v1/feedback": { + "post": { + "operationId": "createFeedback", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Feedback" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Create run feedback", + "x-openrath-action": "feedback.create", + "x-openrath-stability": "beta" + } + }, + "/v1/interrupts": { + "get": { + "operationId": "listInterrupts", + "parameters": [ + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 200, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemPage" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "List pending durable interrupts", + "x-openrath-action": "interrupt.read", + "x-openrath-stability": "beta" + } + }, + "/v1/interrupts/{interrupt_id}/decision": { + "post": { + "operationId": "decideInterrupt", + "parameters": [ + { + "in": "path", + "name": "interrupt_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InterruptDecisionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Submit a durable interrupt decision", + "x-openrath-action": "interrupt.decide", + "x-openrath-stability": "beta" + } + }, + "/v1/runs": { + "get": { + "operationId": "listRuns", + "parameters": [ + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 200, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemPage" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "List runs by cursor", + "x-openrath-action": "run.read", + "x-openrath-stability": "beta" + }, + "post": { + "operationId": "createRun", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRunRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Create a durable run", + "x-openrath-action": "run.create", + "x-openrath-stability": "beta" + } + }, + "/v1/runs/{run_id}": { + "get": { + "operationId": "getRun", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Get a durable run", + "x-openrath-action": "run.read", + "x-openrath-stability": "beta" + } + }, + "/v1/runs/{run_id}/cancel": { + "post": { + "operationId": "cancelRun", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Cancel a run", + "x-openrath-action": "run.cancel", + "x-openrath-stability": "beta" + } + }, + "/v1/runs/{run_id}/events": { + "get": { + "operationId": "listRunEvents", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 200, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemPage" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Replay ordered durable run events", + "x-openrath-action": "run.read", + "x-openrath-stability": "beta" + } + }, + "/v1/runs/{run_id}/resume": { + "post": { + "operationId": "resumeRun", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResumeRunRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Resume a run requiring operator review", + "x-openrath-action": "run.resume", + "x-openrath-stability": "beta" + } + }, + "/v1/runs/{run_id}/stream": { + "get": { + "operationId": "streamRunEvents", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 200, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "text/event-stream": { + "schema": { + "type": "object" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Stream ordered run events with cursor resume", + "x-openrath-action": "run.read", + "x-openrath-stability": "beta" + } + }, + "/v1/sessions": { + "post": { + "operationId": "createSession", + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Create a durable session", + "x-openrath-action": "session.create", + "x-openrath-stability": "beta" + } + }, + "/v1/sessions/{session_id}": { + "get": { + "operationId": "getSession", + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Get a durable session", + "x-openrath-action": "session.read", + "x-openrath-stability": "beta" + } + }, + "/v1/sessions/{session_id}/runs": { + "post": { + "operationId": "createSessionRun", + "parameters": [ + { + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSessionRunRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Create a run in an existing session", + "x-openrath-action": "run.create", + "x-openrath-stability": "beta" + } + }, + "/v1/store/items": { + "delete": { + "operationId": "deleteStoreItem", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Delete a governed memory item", + "x-openrath-action": "memory.delete", + "x-openrath-stability": "beta" + }, + "post": { + "operationId": "putStoreItem", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Write a governed memory item", + "x-openrath-action": "memory.put", + "x-openrath-stability": "beta" + } + }, + "/v1/store/search": { + "post": { + "operationId": "searchStore", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "Success" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthenticated" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource exhausted" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Search governed memory", + "x-openrath-action": "memory.search", + "x-openrath-stability": "beta" + } + } + } +} diff --git a/deploy/docs/operations-v2.md b/deploy/docs/operations-v2.md index 91ea3e1..e5c047f 100644 --- a/deploy/docs/operations-v2.md +++ b/deploy/docs/operations-v2.md @@ -3,12 +3,32 @@ ## Release and upgrade - Build immutable images by Git commit digest; never deploy a mutable `latest`. +- Replace every review tag in `deploy/kubernetes/openrath.yaml` with the exact + `image@sha256:...` produced for the candidate. A manifest containing a tag is + a template, not release evidence. - Run `openrath-migrate --check` before traffic and `openrath-migrate` as a single pre-deploy Job. - Database changes are additive in v2.0.0. Roll application pods back first; retain added columns/tables until the rollback window closes. - Take and restore-test PostgreSQL and artifact backups before an upgrade. +The server durable profile rejects synchronous steps that declare a timeout: +an in-process Python thread cannot be preempted safely. Use an async handler or +an isolated executor. Embedded compatibility mode waits for a synchronous +handler to return before recording timeout and must never be presented as a +preemptive cancellation guarantee. + +`LocalTrustedPolicy` is limited to an explicit local `trusted_host` grant. It +allows same-process filesystem/network behavior and is unsuitable for +untrusted tenants. Service deployments should supply a fail-closed policy, +governed adapter executors, durable effect ledger, and audit sink. + +The Kubernetes template is fail closed for egress. PostgreSQL, Redis, S3, and +an HTTPS egress gateway must run in a namespace labelled +`openrath.io/data-plane=allowed`; DNS is limited to `kube-system`. If the CNI +supports FQDN policies, restrict provider and object-store hostnames there. +Do not replace this with unrestricted `to: []` egress. + ## Incident runbooks ### PostgreSQL unavailable @@ -48,3 +68,12 @@ Quarterly, restore PostgreSQL and artifacts into an isolated environment, run `openrath-migrate --check`, fetch historical Runs and artifacts, requeue an expired lease, and verify a non-idempotent ambiguous invocation remains blocked for review. + +## RC evidence boundary + +Offline tests, a locally built wheel, a mutable image tag, or a Fake provider +do not approve a release. RC evidence must be generated from one immutable +commit and include real provider/sandbox/memory lifecycles, PostgreSQL/Redis/S3 +restart drills, backup/restore, rollback, scale, soak, SBOM, vulnerability scan, +and the published image digest. Missing infrastructure is recorded as a +release blocker rather than silently skipped. diff --git a/deploy/kubernetes/openrath.yaml b/deploy/kubernetes/openrath.yaml index bc01c40..c1e4271 100644 --- a/deploy/kubernetes/openrath.yaml +++ b/deploy/kubernetes/openrath.yaml @@ -5,6 +5,12 @@ metadata: automountServiceAccountToken: false --- apiVersion: v1 +kind: ServiceAccount +metadata: + name: openrath-migrate +automountServiceAccountToken: false +--- +apiVersion: v1 kind: ConfigMap metadata: name: openrath @@ -21,8 +27,11 @@ metadata: spec: backoffLimit: 3 template: + metadata: + labels: + app.kubernetes.io/name: openrath-migrate spec: - serviceAccountName: openrath + serviceAccountName: openrath-migrate restartPolicy: OnFailure securityContext: runAsNonRoot: true @@ -31,13 +40,14 @@ spec: containers: - name: migrate image: ghcr.io/rath-team/openrath:2.0.0-review - imagePullPolicy: IfNotPresent + # Release automation must replace the review tag with image@sha256. + imagePullPolicy: Always command: ["openrath-migrate"] envFrom: - configMapRef: name: openrath - secretRef: - name: openrath-runtime + name: openrath-migration securityContext: allowPrivilegeEscalation: false capabilities: @@ -67,7 +77,8 @@ spec: containers: - name: openrath image: ghcr.io/rath-team/openrath:2.0.0-review - imagePullPolicy: IfNotPresent + # Release automation must replace the review tag with image@sha256. + imagePullPolicy: Always envFrom: - configMapRef: name: openrath @@ -149,6 +160,8 @@ spec: containers: - name: worker image: ghcr.io/rath-team/openrath:2.0.0-review + # Release automation must replace the review tag with image@sha256. + imagePullPolicy: Always command: ["openrath-worker", "--app", "examples.v2_server_app:server"] envFrom: - configMapRef: @@ -162,6 +175,22 @@ spec: limits: cpu: "2" memory: 1Gi + readinessProbe: + exec: + command: ["openrath-migrate", "--check"] + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + exec: + command: ["openrath-migrate", "--check"] + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "sleep 5"] securityContext: allowPrivilegeEscalation: false capabilities: @@ -213,17 +242,35 @@ metadata: name: openrath-default-deny spec: podSelector: - matchLabels: - app.kubernetes.io/name: openrath + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: ["openrath", "openrath-worker", "openrath-migrate"] policyTypes: ["Ingress", "Egress"] ingress: - ports: - protocol: TCP port: 8000 egress: - - to: [] + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - namespaceSelector: + matchLabels: + openrath.io/data-plane: "allowed" ports: - protocol: TCP port: 5432 + - protocol: TCP + port: 6379 + - protocol: TCP + port: 9000 - protocol: TCP port: 443 diff --git a/examples/v2_server_app.py b/examples/v2_server_app.py index 282bc07..1a354cc 100644 --- a/examples/v2_server_app.py +++ b/examples/v2_server_app.py @@ -8,7 +8,7 @@ from rath.definition import EffectClass, step from rath.flow import Workflow -from rath.runtime import LocalRuntime, PostgresRunStore +from rath.runtime import LocalRuntime, PostgresEffectLedger, PostgresRunStore from rath.security import Principal, PrincipalKind, SecurityContext from rath.server import AgentServer, StaticTokenAuth from rath.session import Session @@ -37,8 +37,17 @@ def forward(self, session: Session) -> Session: dsn = os.environ["OPENRATH_POSTGRES_DSN"] token = os.environ["OPENRATH_TOKEN"] tenant_id = os.getenv("OPENRATH_TENANT_ID", "default") -store = PostgresRunStore(dsn, schema=os.getenv("OPENRATH_DB_SCHEMA", "openrath")) -runtime = LocalRuntime(store) +store = PostgresRunStore( + dsn, + schema=os.getenv("OPENRATH_DB_SCHEMA", "openrath"), + auto_migrate=False, + pool_max_size=int(os.getenv("OPENRATH_DB_POOL_MAX_SIZE", "20")), +) +effect_ledger = PostgresEffectLedger( + dsn, + schema=os.getenv("OPENRATH_DB_SCHEMA", "openrath"), +) +runtime = LocalRuntime(store, effect_ledger=effect_ledger, production_mode=True) server = AgentServer( store, runtime, @@ -47,6 +56,7 @@ def forward(self, session: Session) -> Session: token: SecurityContext( principal=Principal(id="reference-user", kind=PrincipalKind.SERVICE), tenant_id=tenant_id, + grants=frozenset({"*"}), ) } ), @@ -57,7 +67,9 @@ def forward(self, session: Session) -> Session: server.register_assistant( "echo", EchoWorkflow(), - revision_id=UUID(os.getenv("OPENRATH_REVISION_ID", "00000000-0000-4000-8000-000000000001")), + revision_id=UUID( + os.getenv("OPENRATH_REVISION_ID", "00000000-0000-4000-8000-000000000001") + ), ) server.register_assistant( "slow", diff --git a/pyproject.toml b/pyproject.toml index a19c37f..b0b1301 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,10 @@ opensandbox = [ ] openviking = [ "openviking>=0.4.11", + "aiohttp>=3.14.1", + "json-repair>=0.60.1", + "pillow>=12.3.0", + "soupsieve>=2.8.4", ] server = [ "starlette>=1.3.1,<2", diff --git a/scripts/capacity_v2.py b/scripts/capacity_v2.py index ec47a9c..0e721b2 100644 --- a/scripts/capacity_v2.py +++ b/scripts/capacity_v2.py @@ -17,18 +17,19 @@ def main() -> None: parser.add_argument("--worker-concurrency", type=int, default=16) parser.add_argument("--headroom", type=float, default=1.5) args = parser.parse_args() - if min( - args.peak_runs_per_second, - args.mean_run_seconds, - args.events_per_run, - args.event_kib, - args.worker_concurrency, - args.headroom, - ) <= 0: + if ( + min( + args.peak_runs_per_second, + args.mean_run_seconds, + args.events_per_run, + args.event_kib, + args.worker_concurrency, + args.headroom, + ) + <= 0 + ): parser.error("capacity inputs must be positive") - concurrent = ( - args.peak_runs_per_second * args.mean_run_seconds * args.headroom - ) + concurrent = args.peak_runs_per_second * args.mean_run_seconds * args.headroom workers = math.ceil(concurrent / args.worker_concurrency) events_per_day = args.peak_runs_per_second * 86400 * args.events_per_run storage_gib = ( diff --git a/scripts/export_openapi_v2.py b/scripts/export_openapi_v2.py new file mode 100644 index 0000000..52902ce --- /dev/null +++ b/scripts/export_openapi_v2.py @@ -0,0 +1,35 @@ +"""Export the deterministic v2 Agent Server OpenAPI contract.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from rath.server.app import _openapi_document + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--output", + type=Path, + default=Path("deploy/docs/openapi-v2.json"), + ) + parser.add_argument("--version", default="2.0.0-unreleased") + args = parser.parse_args() + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps( + _openapi_document(args.version, store_enabled=True), + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/migrate_v1_to_v2.py b/scripts/migrate_v1_to_v2.py index a0a8934..ccc5e68 100644 --- a/scripts/migrate_v1_to_v2.py +++ b/scripts/migrate_v1_to_v2.py @@ -36,10 +36,10 @@ def main() -> None: ] ) rows: list[dict[str, object]] = [] + if args.apply: + PostgresRunStore.migrate(args.postgres_dsn, schema=args.schema) runtime = ( - PostgresRunStore(args.postgres_dsn, schema=args.schema) - if args.apply - else None + PostgresRunStore(args.postgres_dsn, schema=args.schema) if args.apply else None ) resources = PostgresResourceStore(runtime) if runtime is not None else None artifacts = ( @@ -49,8 +49,9 @@ def main() -> None: ) try: for path in candidates: - session_id = UUID(path.name.split(".jsonl", 1)[0]) + session_id: UUID | None = None try: + session_id = UUID(path.name.split(".jsonl", 1)[0]) legacy = load_session(session_id, path=path) row: dict[str, object] = { "session_id": str(session_id), @@ -60,16 +61,21 @@ def main() -> None: "chunks": len(legacy.chunk_table.rows), "status": "ready", } - if runtime is not None and resources is not None and artifacts is not None: - artifact = artifacts.put( - args.tenant, - path.read_bytes(), - media_type="application/x-ndjson", - metadata={ - "provenance": "legacy-import", - "legacy_session_id": str(session_id), - }, - ) + if ( + runtime is not None + and resources is not None + and artifacts is not None + ): + with path.open("rb") as source_file: + artifact = artifacts.put( + args.tenant, + source_file, + media_type="application/x-ndjson", + metadata={ + "provenance": "legacy-import", + "legacy_session_id": str(session_id), + }, + ) resources.ensure_session( SessionRecord( id=session_id, @@ -110,9 +116,16 @@ def main() -> None: except Exception as exc: rows.append( { - "session_id": str(session_id), + "session_id": ( + str(session_id) if session_id is not None else None + ), "path": str(path), "status": "invalid", + "error_code": ( + "invalid_session_id" + if session_id is None + else "invalid_session" + ), "error": f"{type(exc).__name__}: {exc}", } ) diff --git a/src/rath/adapters/__init__.py b/src/rath/adapters/__init__.py index 8453627..3cff076 100644 --- a/src/rath/adapters/__init__.py +++ b/src/rath/adapters/__init__.py @@ -1,6 +1,10 @@ """Shared v2 adapter contracts.""" -from rath.adapters.context import AdapterRequestContext +from rath.adapters.context import ( + AdapterRequestContext, + merge_policy_constraints, + with_policy_constraints, +) from rath.adapters.memory import MemoryExecutor, MemoryHandler from rath.adapters.provider import ProviderExecutor, ProviderHandler from rath.adapters.sandbox import SandboxExecutor, SandboxHandler @@ -13,13 +17,22 @@ SandboxSpec, ToolSpec, ) -from rath.adapters.tool import ToolExecutor, ToolHandler, ToolOutputTooLarge +from rath.adapters.tool import ( + ApprovalGrant, + ApprovalValidator, + ToolExecutor, + ToolHandler, + ToolOutputTooLarge, +) __all__ = [ "AdapterRequestContext", + "ApprovalGrant", + "ApprovalValidator", "MemoryNamespace", "MemoryExecutor", "MemoryHandler", + "merge_policy_constraints", "ProviderCapability", "ProviderExecutor", "ProviderHandler", @@ -34,4 +47,5 @@ "ToolOutputTooLarge", "ToolSpec", "validate_json", + "with_policy_constraints", ] diff --git a/src/rath/adapters/context.py b/src/rath/adapters/context.py index 2346f73..e741757 100644 --- a/src/rath/adapters/context.py +++ b/src/rath/adapters/context.py @@ -2,14 +2,19 @@ from __future__ import annotations -from dataclasses import dataclass -from datetime import datetime +from dataclasses import dataclass, replace +from datetime import datetime, timezone from uuid import UUID from rath.context import TraceContext from rath.security import PolicyConstraints -__all__ = ["AdapterRequestContext"] +__all__ = [ + "AdapterRequestContext", + "effective_timeout_seconds", + "merge_policy_constraints", + "with_policy_constraints", +] @dataclass(frozen=True, slots=True) @@ -21,6 +26,7 @@ class AdapterRequestContext: trace_context: TraceContext idempotency_key: str | None policy_constraints: PolicyConstraints + checkpoint_sequence: int | None = None def __post_init__(self) -> None: if not self.node_id.strip(): @@ -29,4 +35,74 @@ def __post_init__(self) -> None: raise ValueError("adapter tenant_id must not be empty") if self.deadline is not None and self.deadline.tzinfo is None: raise ValueError("adapter deadline must be timezone-aware") + if self.checkpoint_sequence is not None and self.checkpoint_sequence < 1: + raise ValueError("checkpoint_sequence must be positive") + +def effective_timeout_seconds( + requested: float, + *, + adapter_context: AdapterRequestContext, + run_remaining_seconds: float | None, +) -> float: + """Resolve the strictest positive timeout from spec, policy and deadlines.""" + + candidates = [requested] + policy_timeout = adapter_context.policy_constraints.timeout_seconds + if policy_timeout is not None: + candidates.append(policy_timeout) + if run_remaining_seconds is not None: + candidates.append(run_remaining_seconds) + if adapter_context.deadline is not None: + candidates.append( + max( + 0.0, + (adapter_context.deadline - datetime.now(timezone.utc)).total_seconds(), + ) + ) + return min(candidates) + + +def merge_policy_constraints( + declared: PolicyConstraints, + decided: PolicyConstraints, +) -> PolicyConstraints: + """Combine caller and policy limits without weakening either side.""" + + timeouts = [ + value + for value in (declared.timeout_seconds, decided.timeout_seconds) + if value is not None + ] + output_limits = [ + value + for value in (declared.max_output_bytes, decided.max_output_bytes) + if value is not None + ] + if declared.allowed_network_hosts and decided.allowed_network_hosts: + allowed_hosts = declared.allowed_network_hosts & decided.allowed_network_hosts + else: + allowed_hosts = declared.allowed_network_hosts or decided.allowed_network_hosts + return PolicyConstraints( + timeout_seconds=min(timeouts) if timeouts else None, + max_output_bytes=min(output_limits) if output_limits else None, + allowed_network_hosts=allowed_hosts, + filesystem_root=decided.filesystem_root or declared.filesystem_root, + read_only=declared.read_only or decided.read_only, + redactions=declared.redactions | decided.redactions, + ) + + +def with_policy_constraints( + context: AdapterRequestContext, + constraints: PolicyConstraints, +) -> AdapterRequestContext: + """Return the adapter context carrying the evaluated policy constraints.""" + + return replace( + context, + policy_constraints=merge_policy_constraints( + context.policy_constraints, + constraints, + ), + ) diff --git a/src/rath/adapters/memory.py b/src/rath/adapters/memory.py index 2124e2e..b5d8c44 100644 --- a/src/rath/adapters/memory.py +++ b/src/rath/adapters/memory.py @@ -4,10 +4,15 @@ import asyncio import inspect +import time from collections.abc import Awaitable, Mapping from typing import Literal, Protocol, cast -from rath.adapters.context import AdapterRequestContext +from rath.adapters.context import ( + AdapterRequestContext, + effective_timeout_seconds, + with_policy_constraints, +) from rath.adapters.specs import MemoryNamespace from rath.context import RunContext from rath.security import Action, PolicyEngine, ResourceRef, authorize @@ -45,7 +50,7 @@ async def execute( tenant_id = run_context.security.tenant_id if namespace.tenant_id != tenant_id or adapter_context.tenant_id != tenant_id: raise PermissionError("memory namespace tenant mismatch") - await authorize( + decision = await authorize( self.policy, action=Action(f"memory.{operation}"), resource=ResourceRef( @@ -65,9 +70,21 @@ async def execute( ), context=run_context, ) + adapter_context = with_policy_constraints( + adapter_context, + decision.constraints, + ) + timeout = effective_timeout_seconds( + timeout_seconds, + adapter_context=adapter_context, + run_remaining_seconds=run_context.remaining_seconds(), + ) + started = time.monotonic() result = handler(operation, namespace, payload, adapter_context) if inspect.isawaitable(result): return await asyncio.wait_for( - cast(Awaitable[object], result), timeout=timeout_seconds + cast(Awaitable[object], result), timeout=timeout ) + if time.monotonic() - started > timeout: + raise TimeoutError("memory operation exceeded timeout") return result diff --git a/src/rath/adapters/provider.py b/src/rath/adapters/provider.py index 7d67ff2..3405203 100644 --- a/src/rath/adapters/provider.py +++ b/src/rath/adapters/provider.py @@ -4,10 +4,15 @@ import asyncio import inspect +import time from collections.abc import Awaitable, Mapping from typing import Protocol, cast -from rath.adapters.context import AdapterRequestContext +from rath.adapters.context import ( + AdapterRequestContext, + effective_timeout_seconds, + with_policy_constraints, +) from rath.adapters.specs import ProviderCapability, ProviderSpec from rath.context import RunContext from rath.security import Action, PolicyEngine, ResourceRef, authorize @@ -45,7 +50,7 @@ async def execute( ) if adapter_context.tenant_id != run_context.security.tenant_id: raise PermissionError("adapter and run tenant mismatch") - await authorize( + decision = await authorize( self.policy, action=Action("provider.invoke"), resource=ResourceRef( @@ -60,14 +65,26 @@ async def execute( ), context=run_context, ) + adapter_context = with_policy_constraints( + adapter_context, + decision.constraints, + ) semaphore = self._semaphores.setdefault( spec.id, asyncio.Semaphore(spec.max_concurrency) ) + timeout = effective_timeout_seconds( + spec.total_timeout_seconds, + adapter_context=adapter_context, + run_remaining_seconds=run_context.remaining_seconds(), + ) async with semaphore: + started = time.monotonic() result = handler(request, spec, adapter_context) if inspect.isawaitable(result): return await asyncio.wait_for( cast(Awaitable[object], result), - timeout=spec.total_timeout_seconds, + timeout=timeout, ) + if time.monotonic() - started > timeout: + raise TimeoutError(f"provider {spec.id!r} exceeded timeout") return result diff --git a/src/rath/adapters/sandbox.py b/src/rath/adapters/sandbox.py index 400b934..a06d213 100644 --- a/src/rath/adapters/sandbox.py +++ b/src/rath/adapters/sandbox.py @@ -4,10 +4,15 @@ import asyncio import inspect +import time from collections.abc import Awaitable, Mapping from typing import Protocol, cast -from rath.adapters.context import AdapterRequestContext +from rath.adapters.context import ( + AdapterRequestContext, + effective_timeout_seconds, + with_policy_constraints, +) from rath.adapters.specs import SandboxSpec from rath.context import RunContext from rath.security import Action, PolicyEngine, ResourceRef, authorize @@ -46,7 +51,7 @@ async def execute( raise ValueError("sandbox timeout must be positive") if adapter_context.tenant_id != run_context.security.tenant_id: raise PermissionError("adapter and run tenant mismatch") - await authorize( + decision = await authorize( self.policy, action=Action("sandbox.execute"), resource=ResourceRef( @@ -61,9 +66,21 @@ async def execute( ), context=run_context, ) + adapter_context = with_policy_constraints( + adapter_context, + decision.constraints, + ) + timeout = effective_timeout_seconds( + timeout_seconds, + adapter_context=adapter_context, + run_remaining_seconds=run_context.remaining_seconds(), + ) + started = time.monotonic() result = handler(operation, payload, spec, adapter_context) if inspect.isawaitable(result): return await asyncio.wait_for( - cast(Awaitable[object], result), timeout=timeout_seconds + cast(Awaitable[object], result), timeout=timeout ) + if time.monotonic() - started > timeout: + raise TimeoutError(f"sandbox {spec.id!r} exceeded timeout") return result diff --git a/src/rath/adapters/schema.py b/src/rath/adapters/schema.py index 820cb31..44fc375 100644 --- a/src/rath/adapters/schema.py +++ b/src/rath/adapters/schema.py @@ -11,7 +11,9 @@ class SchemaValidationError(ValueError): pass -def validate_json(value: object, schema: Mapping[str, object], *, path: str = "$") -> None: +def validate_json( + value: object, schema: Mapping[str, object], *, path: str = "$" +) -> None: expected = schema.get("type") if expected == "object": if not isinstance(value, Mapping): @@ -52,7 +54,16 @@ def validate_json(value: object, schema: Mapping[str, object], *, path: str = "$ raise SchemaValidationError(f"{path} must be a boolean") elif expected == "null" and value is not None: raise SchemaValidationError(f"{path} must be null") - elif expected not in (None, "object", "array", "string", "integer", "number", "boolean", "null"): + elif expected not in ( + None, + "object", + "array", + "string", + "integer", + "number", + "boolean", + "null", + ): raise SchemaValidationError(f"{path} uses unsupported schema type {expected!r}") enum = schema.get("enum") if isinstance(enum, Sequence) and value not in enum: diff --git a/src/rath/adapters/specs.py b/src/rath/adapters/specs.py index 7ec13d6..e985e01 100644 --- a/src/rath/adapters/specs.py +++ b/src/rath/adapters/specs.py @@ -43,11 +43,14 @@ class ProviderSpec: def __post_init__(self) -> None: if not self.id or not self.kind or not self.model: raise ValueError("provider id, kind, and model are required") - if min( - self.connect_timeout_seconds, - self.read_timeout_seconds, - self.total_timeout_seconds, - ) <= 0: + if ( + min( + self.connect_timeout_seconds, + self.read_timeout_seconds, + self.total_timeout_seconds, + ) + <= 0 + ): raise ValueError("provider timeouts must be positive") if self.max_concurrency < 1: raise ValueError("provider max_concurrency must be positive") @@ -101,7 +104,10 @@ def __post_init__(self) -> None: raise ValueError("sandbox id is required") if self.ttl_seconds <= 0: raise ValueError("sandbox ttl_seconds must be positive") - if self.isolation is not SandboxIsolation.TRUSTED_HOST and not self.image_digest: + if ( + self.isolation is not SandboxIsolation.TRUSTED_HOST + and not self.image_digest + ): raise ValueError("container sandboxes require an immutable image_digest") if self.network == "allowlist" and not self.allowed_hosts: raise ValueError("network allowlist requires at least one host") @@ -118,4 +124,3 @@ class MemoryNamespace: def __post_init__(self) -> None: if not self.tenant_id: raise ValueError("memory namespace tenant_id is required") - diff --git a/src/rath/adapters/tool.py b/src/rath/adapters/tool.py index 73c36f8..20975eb 100644 --- a/src/rath/adapters/tool.py +++ b/src/rath/adapters/tool.py @@ -2,17 +2,25 @@ from __future__ import annotations +import asyncio import inspect import json +import time from collections.abc import Awaitable, Mapping +from dataclasses import dataclass from typing import Protocol, cast from uuid import UUID -from rath.adapters.context import AdapterRequestContext +from rath.adapters.context import ( + AdapterRequestContext, + effective_timeout_seconds, + with_policy_constraints, +) from rath.adapters.schema import validate_json from rath.adapters.specs import ToolSpec from rath.artifacts import ArtifactStore from rath.context import RunContext +from rath.definition import EffectClass from rath.runtime.effects import ( EffectLedger, InvocationStatus, @@ -28,13 +36,34 @@ authorize, ) -__all__ = ["ToolExecutor", "ToolHandler", "ToolOutputTooLarge"] +__all__ = [ + "ApprovalGrant", + "ApprovalValidator", + "ToolExecutor", + "ToolHandler", + "ToolOutputTooLarge", +] class ToolOutputTooLarge(RuntimeError): pass +@dataclass(frozen=True, slots=True) +class ApprovalGrant: + decision_id: UUID + run_id: UUID + node_id: str + tenant_id: str + tool_id: str + arguments_digest: str + actor_id: str + + +class ApprovalValidator(Protocol): + def __call__(self, grant: ApprovalGrant) -> bool | Awaitable[bool]: ... + + class ToolHandler(Protocol): def __call__( self, @@ -50,10 +79,12 @@ def __init__( *, effect_ledger: EffectLedger | None = None, artifact_store: ArtifactStore | None = None, + approval_validator: ApprovalValidator | None = None, ) -> None: self.policy = policy self.effect_ledger = effect_ledger self.artifact_store = artifact_store + self.approval_validator = approval_validator async def execute( self, @@ -63,13 +94,34 @@ async def execute( *, adapter_context: AdapterRequestContext, run_context: RunContext, - approved: bool = False, + approval: ApprovalGrant | None = None, run_id: UUID | None = None, idempotency_key: str | None = None, ) -> object: validate_json(arguments, spec.input_schema) + if adapter_context.tenant_id != run_context.security.tenant_id: + raise PermissionError("adapter and run tenant mismatch") + digest = arguments_digest(arguments) + approval_valid = False + if approval is not None: + if ( + approval.run_id != adapter_context.run_id + or approval.node_id != adapter_context.node_id + or approval.tenant_id != adapter_context.tenant_id + or approval.tool_id != f"{spec.name}@{spec.version}" + or approval.arguments_digest != digest + ): + raise PermissionError("approval grant does not match tool invocation") + if self.approval_validator is None: + raise PermissionError("approval grant cannot be verified") + validated = self.approval_validator(approval) + approval_valid = ( + await validated if inspect.isawaitable(validated) else bool(validated) + ) + if not approval_valid: + raise PermissionError("approval grant is not valid") try: - await authorize( + decision = await authorize( self.policy, action=Action("tool.execute"), resource=ResourceRef( @@ -80,10 +132,15 @@ async def execute( ), context=run_context, ) - except ApprovalRequiredError: - if not approved: + except ApprovalRequiredError as exc: + if not approval_valid: raise - if spec.requires_approval and not approved: + decision = exc.decision + adapter_context = with_policy_constraints( + adapter_context, + decision.constraints, + ) + if spec.requires_approval and not approval_valid: raise ApprovalRequiredError( PolicyDecision( effect=PolicyEffect.REQUIRE_APPROVAL, @@ -93,6 +150,11 @@ async def execute( ) invocation = None ledger = self.effect_ledger + effective_idempotency_key = ( + idempotency_key + if idempotency_key is not None + else adapter_context.idempotency_key + ) if ledger is not None: if run_id is None: raise ValueError("run_id is required when effect ledger is enabled") @@ -100,8 +162,10 @@ async def execute( run_id=run_id, tool_name=f"{spec.name}@{spec.version}", effect_class=spec.effects, - arguments_digest=arguments_digest(arguments), - idempotency_key=idempotency_key, + arguments_digest=digest, + idempotency_key=effective_idempotency_key, + node_id=adapter_context.node_id, + checkpoint_sequence=adapter_context.checkpoint_sequence, ) if invocation.status is InvocationStatus.SUCCEEDED: return invocation.result @@ -113,12 +177,28 @@ async def execute( "tool invocation outcome is ambiguous and requires reconciliation" ) invocation = ledger.mark_dispatched(invocation.id) + effective_timeout = effective_timeout_seconds( + spec.timeout_seconds, + adapter_context=adapter_context, + run_remaining_seconds=run_context.remaining_seconds(), + ) + started = time.monotonic() try: result = handler(arguments, adapter_context) if inspect.isawaitable(result): - result = await cast(Awaitable[object], result) + result = await asyncio.wait_for( + cast(Awaitable[object], result), + timeout=effective_timeout, + ) + elif time.monotonic() - started > effective_timeout: + raise TimeoutError(f"tool {spec.name}@{spec.version} exceeded timeout") except BaseException as exc: - if invocation is not None and ledger is not None: + if ( + invocation is not None + and ledger is not None + and spec.effects is not EffectClass.NON_IDEMPOTENT + and not isinstance(exc, (TimeoutError, asyncio.TimeoutError)) + ): ledger.fail(invocation.id, f"{type(exc).__name__}: {exc}") raise if spec.output_schema is not None: diff --git a/src/rath/artifacts/store.py b/src/rath/artifacts/store.py index 7596945..3ca6b84 100644 --- a/src/rath/artifacts/store.py +++ b/src/rath/artifacts/store.py @@ -103,6 +103,22 @@ def _chunks(content: bytes | BinaryIO, size: int = 1024 * 1024) -> Iterator[byte yield chunk +def _read_response_bounded(body: object, maximum: int) -> bytes: + reader = getattr(body, "read", None) + if not callable(reader): + raise TypeError("artifact response body is not readable") + value = bytearray() + while True: + chunk = reader(min(1024 * 1024, maximum - len(value) + 1)) + if not isinstance(chunk, bytes): + raise TypeError("artifact response body did not return bytes") + if not chunk: + return bytes(value) + value.extend(chunk) + if len(value) > maximum: + raise ValueError("artifact exceeds configured size limit") + + def _manifest(artifact: Artifact) -> bytes: value = { "tenant_id": artifact.tenant_id, @@ -279,32 +295,47 @@ def put( media_type: str = "application/octet-stream", metadata: Mapping[str, object] | None = None, ) -> Artifact: - data = b"".join(_chunks(content)) - if len(data) > self.max_bytes: - raise ValueError("artifact exceeds configured size limit") - artifact = Artifact( - tenant_id=tenant_id, - digest=hashlib.sha256(data).hexdigest(), - size=len(data), - media_type=media_type, - created_at=datetime.now(timezone.utc), - metadata=freeze_mapping(metadata, field="artifact.metadata"), - ) - payload_key, manifest_key = self._keys(tenant_id, artifact.digest) - self.client.put_object( - Bucket=self.bucket, - Key=payload_key, - Body=data, - ContentType=media_type, - Metadata={"sha256": artifact.digest}, - ) - self.client.put_object( - Bucket=self.bucket, - Key=manifest_key, - Body=_manifest(artifact), - ContentType="application/json", - ) - return artifact + digest = hashlib.sha256() + total = 0 + with tempfile.TemporaryFile() as staged: + for chunk in _chunks(content): + total += len(chunk) + if total > self.max_bytes: + raise ValueError("artifact exceeds configured size limit") + digest.update(chunk) + staged.write(chunk) + artifact = Artifact( + tenant_id=tenant_id, + digest=digest.hexdigest(), + size=total, + media_type=media_type, + created_at=datetime.now(timezone.utc), + metadata=freeze_mapping(metadata, field="artifact.metadata"), + ) + payload_key, manifest_key = self._keys(tenant_id, artifact.digest) + staged.seek(0) + self.client.put_object( + Bucket=self.bucket, + Key=payload_key, + Body=staged, + ContentLength=total, + ContentType=media_type, + Metadata={"sha256": artifact.digest}, + ) + try: + self.client.put_object( + Bucket=self.bucket, + Key=manifest_key, + Body=_manifest(artifact), + ContentType="application/json", + ) + except BaseException: + self.client.delete_objects( + Bucket=self.bucket, + Delete={"Objects": [{"Key": payload_key}], "Quiet": True}, + ) + raise + return artifact def get(self, tenant_id: str, digest: str) -> bytes: payload_key, _ = self._keys(tenant_id, digest) @@ -314,7 +345,10 @@ def get(self, tenant_id: str, digest: str) -> bytes: if _not_found(exc): raise ArtifactNotFound(digest) from exc raise - value = cast(bytes, response["Body"].read()) + content_length = response.get("ContentLength") + if content_length is not None and int(content_length) > self.max_bytes: + raise ValueError("artifact exceeds configured size limit") + value = _read_response_bounded(response["Body"], self.max_bytes) if hashlib.sha256(value).hexdigest() != digest: raise IOError("artifact digest verification failed") return value @@ -327,7 +361,9 @@ def stat(self, tenant_id: str, digest: str) -> Artifact: if _not_found(exc): raise ArtifactNotFound(digest) from exc raise - artifact = _parse_manifest(response["Body"].read()) + artifact = _parse_manifest( + _read_response_bounded(response["Body"], 1024 * 1024) + ) if artifact.tenant_id != tenant_id or artifact.digest != digest: raise IOError("artifact manifest identity mismatch") return artifact diff --git a/src/rath/client/__init__.py b/src/rath/client/__init__.py index b781ad8..8a6b7b4 100644 --- a/src/rath/client/__init__.py +++ b/src/rath/client/__init__.py @@ -1,4 +1,3 @@ from rath.client.remote import AsyncRemoteClient, RemoteClient __all__ = ["AsyncRemoteClient", "RemoteClient"] - diff --git a/src/rath/client/remote.py b/src/rath/client/remote.py index ef0e7f3..130a4c1 100644 --- a/src/rath/client/remote.py +++ b/src/rath/client/remote.py @@ -86,18 +86,12 @@ def store( elif operation == "put": response = self._client.post("/v1/store/items", json=body) else: - response = self._client.request( - "DELETE", "/v1/store/items", json=body - ) + response = self._client.request("DELETE", "/v1/store/items", json=body) response.raise_for_status() return cast(dict[str, Any], response.json()) - def list_runs( - self, *, limit: int = 50, after: str | None = None - ) -> dict[str, Any]: - response = self._client.get( - "/v1/runs", params={"limit": limit, "after": after} - ) + def list_runs(self, *, limit: int = 50, after: str | None = None) -> dict[str, Any]: + response = self._client.get("/v1/runs", params={"limit": limit, "after": after}) response.raise_for_status() return cast(dict[str, Any], response.json()) @@ -307,7 +301,9 @@ async def create_feedback( response.raise_for_status() return cast(dict[str, Any], response.json()) - async def events(self, run_id: str, *, after: int = 0) -> AsyncIterator[dict[str, Any]]: + async def events( + self, run_id: str, *, after: int = 0 + ) -> AsyncIterator[dict[str, Any]]: response = await self._client.get( f"/v1/runs/{run_id}/events", params={"after": after}, diff --git a/src/rath/context.py b/src/rath/context.py index 8760c6e..cf36d4f 100644 --- a/src/rath/context.py +++ b/src/rath/context.py @@ -105,4 +105,3 @@ def ensure_active(self, *, now: datetime | None = None) -> None: remaining = self.remaining_seconds(now=now) if remaining is not None and remaining <= 0: raise DeadlineExceededError() - diff --git a/src/rath/definition/__init__.py b/src/rath/definition/__init__.py index e9a5002..38f1c9e 100644 --- a/src/rath/definition/__init__.py +++ b/src/rath/definition/__init__.py @@ -29,4 +29,3 @@ "WorkflowCompiler", "WorkflowDefinition", ] - diff --git a/src/rath/definition/compiler.py b/src/rath/definition/compiler.py index 06c3e73..03efba0 100644 --- a/src/rath/definition/compiler.py +++ b/src/rath/definition/compiler.py @@ -5,6 +5,7 @@ import hashlib import inspect import json +import textwrap from collections.abc import Callable, Mapping from uuid import NAMESPACE_URL, UUID, uuid5 @@ -37,6 +38,7 @@ def compile( workflow: object, *, revision_id: UUID, + production_durable: bool = False, input_schema: Mapping[str, JSONValue] | None = None, state_schema: Mapping[str, JSONValue] | None = None, policy_manifest: Mapping[str, JSONValue] | None = None, @@ -44,7 +46,11 @@ def compile( name = f"{type(workflow).__module__}.{type(workflow).__qualname__}" version = str(getattr(workflow, "workflow_version", "1")) nodes, entrypoint, durable, issues = self._nodes(workflow) - self._validate(nodes, entrypoint) + self._validate( + nodes, + entrypoint, + production_durable=production_durable, + ) edges = tuple( EdgeSpec(source=node.id, target=target) for node in nodes @@ -115,7 +121,12 @@ def _nodes( id="legacy.forward", kind=NodeKind.OPAQUE, handler=f"{type(workflow).__module__}.{type(workflow).__qualname__}.forward", - is_async=inspect.iscoroutinefunction(getattr(workflow, "forward", None)), + implementation_hash=self._implementation_hash( + getattr(type(workflow), "forward") + ), + is_async=inspect.iscoroutinefunction( + getattr(workflow, "forward", None) + ), retry=RetryPolicy(), effects=EffectClass.NON_IDEMPOTENT, checkpoint=False, @@ -141,6 +152,7 @@ def _nodes( id=name, kind=metadata.kind, handler=f"{type(workflow).__module__}.{type(workflow).__qualname__}.{name}", + implementation_hash=self._implementation_hash(function), is_async=inspect.iscoroutinefunction(function), retry=metadata.retry, effects=metadata.effects, @@ -156,11 +168,47 @@ def _nodes( ) return tuple(nodes), entries[0], True, () - def _validate(self, nodes: tuple[NodeSpec, ...], entrypoint: str) -> None: + @staticmethod + def _implementation_hash(function: Callable[..., object]) -> str: + """Fingerprint executable source so behavior changes alter plan identity.""" + try: + material = textwrap.dedent(inspect.getsource(function)).strip() + except (OSError, TypeError): + code = function.__code__ + material = json.dumps( + { + "bytecode": code.co_code.hex(), + "constants": repr(code.co_consts), + "names": code.co_names, + "varnames": code.co_varnames, + "argcount": code.co_argcount, + "kwonlyargcount": code.co_kwonlyargcount, + }, + sort_keys=True, + separators=(",", ":"), + ) + return hashlib.sha256(material.encode("utf-8")).hexdigest() + + def _validate( + self, + nodes: tuple[NodeSpec, ...], + entrypoint: str, + *, + production_durable: bool = False, + ) -> None: ids = {node.id for node in nodes} if len(ids) != len(nodes): raise DefinitionError("workflow node ids must be unique") for node in nodes: + if ( + production_durable + and not node.is_async + and node.timeout_seconds is not None + ): + raise DefinitionError( + "synchronous durable steps cannot guarantee preemptive timeout; " + "use an async handler or an isolated executor" + ) for successor in node.successors: if successor not in ids: raise DefinitionError( @@ -197,4 +245,3 @@ def _resources(self, workflow: object) -> ResourceManifestV2: ) ) return ResourceManifestV2(providers=tuple(providers)) - diff --git a/src/rath/definition/decorators.py b/src/rath/definition/decorators.py index d178fc9..10ec048 100644 --- a/src/rath/definition/decorators.py +++ b/src/rath/definition/decorators.py @@ -94,4 +94,3 @@ def decorator(function: F) -> F: def _metadata(function: Callable[..., Any]) -> _NodeMetadata | None: return cast(_NodeMetadata | None, getattr(function, _METADATA_ATTR, None)) - diff --git a/src/rath/definition/model.py b/src/rath/definition/model.py index ebdd3bc..7784b3f 100644 --- a/src/rath/definition/model.py +++ b/src/rath/definition/model.py @@ -48,7 +48,9 @@ def __post_init__(self) -> None: if self.base_seconds <= 0: raise ValueError("base_seconds must be greater than zero") if self.max_seconds < self.base_seconds: - raise ValueError("max_seconds must be greater than or equal to base_seconds") + raise ValueError( + "max_seconds must be greater than or equal to base_seconds" + ) def to_dict(self) -> dict[str, object]: return { @@ -64,6 +66,7 @@ class NodeSpec: kind: NodeKind handler: str is_async: bool + implementation_hash: str | None = None retry: RetryPolicy = field(default_factory=RetryPolicy) effects: EffectClass = EffectClass.NON_IDEMPOTENT idempotency_key: str | None = None @@ -76,6 +79,10 @@ def __post_init__(self) -> None: raise ValueError("node id must not be empty") if not self.handler.strip(): raise ValueError("node handler must not be empty") + if self.implementation_hash is not None: + if len(self.implementation_hash) != 64: + raise ValueError("node implementation_hash must be a SHA-256 digest") + int(self.implementation_hash, 16) if self.timeout_seconds is not None and self.timeout_seconds <= 0: raise ValueError("node timeout_seconds must be greater than zero") if ( @@ -83,15 +90,14 @@ def __post_init__(self) -> None: and self.retry.max_attempts > 1 and not self.idempotency_key ): - raise ValueError( - "non-idempotent retries require a stable idempotency key" - ) + raise ValueError("non-idempotent retries require a stable idempotency key") def to_dict(self) -> dict[str, object]: return { "id": self.id, "kind": self.kind.value, "handler": self.handler, + "implementation_hash": self.implementation_hash, "is_async": self.is_async, "retry": self.retry.to_dict(), "effects": self.effects.value, @@ -212,4 +218,3 @@ def canonical_json(self) -> str: sort_keys=True, separators=(",", ":"), ) - diff --git a/src/rath/deployment/revisions.py b/src/rath/deployment/revisions.py index e6ccad1..7a8bc48 100644 --- a/src/rath/deployment/revisions.py +++ b/src/rath/deployment/revisions.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import sqlite3 from collections.abc import Mapping @@ -72,9 +73,7 @@ class Revision: created_at: datetime @classmethod - def create( - cls, *, code_digest: str, manifest: DeploymentManifest - ) -> "Revision": + def create(cls, *, code_digest: str, manifest: DeploymentManifest) -> "Revision": if len(code_digest) != 64: raise ValueError("code_digest must be a SHA-256 digest") int(code_digest, 16) @@ -84,6 +83,20 @@ def create( ) return cls(identity, code_digest, manifest, datetime.now(timezone.utc)) + @property + def content_digest(self) -> str: + """SHA-256 identity covering executable code and deployment manifest.""" + + payload = json.dumps( + { + "code_digest": self.code_digest, + "manifest": json.loads(self.manifest.canonical_json()), + }, + sort_keys=True, + separators=(",", ":"), + ) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + @runtime_checkable class RevisionStore(Protocol): @@ -114,17 +127,22 @@ def put(self, revision: Revision) -> Revision: ).fetchone() if existing is not None: loaded = self.get(revision.id) - if loaded.code_digest != revision.code_digest or loaded.manifest != revision.manifest: + if ( + loaded.code_digest != revision.code_digest + or loaded.manifest != revision.manifest + ): raise RevisionConflict("revision identity is immutable") return loaded connection.execute( """ INSERT INTO revisions( - id, code_digest, plan_hash, manifest_json, created_at - ) VALUES (?, ?, ?, ?, ?) + id, content_digest, code_digest, plan_hash, + manifest_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?) """, ( str(revision.id), + revision.content_digest, revision.code_digest, revision.manifest.plan_hash, revision.manifest.canonical_json(), @@ -164,12 +182,14 @@ def put(self, revision: Revision) -> Revision: row = connection.execute( """ INSERT INTO revisions( - id, code_digest, plan_hash, manifest_json, created_at - ) VALUES (%s, %s, %s, %s, %s) + id, content_digest, code_digest, plan_hash, + manifest_json, created_at + ) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (id) DO NOTHING RETURNING id """, ( revision.id, + revision.content_digest, revision.code_digest, revision.manifest.plan_hash, Jsonb(manifest), @@ -178,7 +198,10 @@ def put(self, revision: Revision) -> Revision: ).fetchone() if row is None: loaded = self.get(revision.id) - if loaded.code_digest != revision.code_digest or loaded.manifest != revision.manifest: + if ( + loaded.code_digest != revision.code_digest + or loaded.manifest != revision.manifest + ): raise RevisionConflict("revision identity is immutable") return loaded return revision diff --git a/src/rath/errors.py b/src/rath/errors.py index 70e2043..a97fbb7 100644 --- a/src/rath/errors.py +++ b/src/rath/errors.py @@ -57,4 +57,3 @@ def to_dict(self) -> dict[str, Any]: "retryable": self.retryable, "details": thaw_json(self.details), } - diff --git a/src/rath/eval/models.py b/src/rath/eval/models.py index 7aa076e..fc956b1 100644 --- a/src/rath/eval/models.py +++ b/src/rath/eval/models.py @@ -96,4 +96,3 @@ def mean_score(self) -> float: class GateDecision(str, Enum): PASS = "pass" FAIL = "fail" - diff --git a/src/rath/eval/runner.py b/src/rath/eval/runner.py index 46b67bd..400bbdc 100644 --- a/src/rath/eval/runner.py +++ b/src/rath/eval/runner.py @@ -52,4 +52,3 @@ def regression_gate( if candidate.mean_score < baseline.mean_score - maximum_regression: return GateDecision.FAIL return GateDecision.PASS - diff --git a/src/rath/eval/store.py b/src/rath/eval/store.py index e44d288..ec57fa0 100644 --- a/src/rath/eval/store.py +++ b/src/rath/eval/store.py @@ -177,7 +177,12 @@ def save_dataset(self, dataset: Dataset) -> Dataset: ON CONFLICT(name, version) DO UPDATE SET examples_json = excluded.examples_json """, - (dataset.id, dataset.name, dataset.version, Jsonb(_dataset_json(dataset))), + ( + dataset.id, + dataset.name, + dataset.version, + Jsonb(_dataset_json(dataset)), + ), ) return dataset diff --git a/src/rath/events.py b/src/rath/events.py index ce1cd5e..87d175b 100644 --- a/src/rath/events.py +++ b/src/rath/events.py @@ -145,4 +145,3 @@ def append( events=(*self.events, event), parent_session_ids=self.parent_session_ids, ) - diff --git a/src/rath/observability/otel.py b/src/rath/observability/otel.py index a7157a8..f84aa27 100644 --- a/src/rath/observability/otel.py +++ b/src/rath/observability/otel.py @@ -29,12 +29,8 @@ def __init__( "OpenTelemetry support requires `pip install openrath[otel]`" ) from exc self._trace = trace - self._tracer = trace.get_tracer( - service_name, tracer_provider=tracer_provider - ) - self._meter = metrics.get_meter( - service_name, meter_provider=meter_provider - ) + self._tracer = trace.get_tracer(service_name, tracer_provider=tracer_provider) + self._meter = metrics.get_meter(service_name, meter_provider=meter_provider) self._counters: dict[str, Any] = {} @contextmanager diff --git a/src/rath/observability/redaction.py b/src/rath/observability/redaction.py index 21fc973..1507f00 100644 --- a/src/rath/observability/redaction.py +++ b/src/rath/observability/redaction.py @@ -24,4 +24,3 @@ def redact(value: object) -> object: if isinstance(value, (list, tuple)): return [redact(item) for item in value] return value - diff --git a/src/rath/runtime/__init__.py b/src/rath/runtime/__init__.py index 4031af0..a6137dd 100644 --- a/src/rath/runtime/__init__.py +++ b/src/rath/runtime/__init__.py @@ -10,7 +10,13 @@ arguments_digest, reconcile_stale_effects, ) -from rath.runtime.local import LocalRuntime, StepContext +from rath.runtime.execution import ( + ExecutionServices, + PythonStepExecutor, + StepExecutor, + StepSuspended, +) +from rath.runtime.local import LocalRuntime, PlanMismatchError, StepContext from rath.runtime.models import ( ApprovalDecision, ApprovalDecisionKind, @@ -46,6 +52,7 @@ "ClaimedRun", "ConflictError", "EffectLedger", + "ExecutionServices", "Interrupt", "InterruptKind", "InvocationStatus", @@ -53,7 +60,9 @@ "InMemorySignalBus", "InvalidRunTransition", "LocalRuntime", + "PlanMismatchError", "PostgresRunStore", + "PythonStepExecutor", "PostgresEffectLedger", "Reconciliation", "RedisSignalBus", @@ -68,6 +77,8 @@ "SignalKind", "SQLiteEffectLedger", "StepContext", + "StepExecutor", + "StepSuspended", "ToolInvocation", "arguments_digest", "reconcile_stale_effects", diff --git a/src/rath/runtime/effects.py b/src/rath/runtime/effects.py index 461c6c7..38b8aa8 100644 --- a/src/rath/runtime/effects.py +++ b/src/rath/runtime/effects.py @@ -47,6 +47,9 @@ class ToolInvocation: status: InvocationStatus created_at: datetime updated_at: datetime + node_id: str | None = None + checkpoint_sequence: int | None = None + invocation_sequence: int | None = None idempotency_key: str | None = None result: JSONValue | None = None error: str | None = None @@ -76,6 +79,8 @@ def prepare( effect_class: EffectClass, arguments_digest: str, idempotency_key: str | None, + node_id: str | None = None, + checkpoint_sequence: int | None = None, ) -> ToolInvocation: ... def get(self, invocation_id: UUID) -> ToolInvocation: ... @@ -90,6 +95,8 @@ def reconcile_stale( self, *, older_than: datetime ) -> tuple[ToolInvocation, ...]: ... + def watermark(self, run_id: UUID) -> int: ... + def arguments_digest(arguments: Mapping[str, object]) -> str: frozen = freeze_json(arguments, field="tool arguments") @@ -161,19 +168,10 @@ def prepare( effect_class: EffectClass, arguments_digest: str, idempotency_key: str | None, + node_id: str | None = None, + checkpoint_sequence: int | None = None, ) -> ToolInvocation: now = datetime.now(timezone.utc) - invocation = ToolInvocation( - id=uuid4(), - run_id=run_id, - tool_name=tool_name, - effect_class=effect_class, - arguments_digest=arguments_digest, - idempotency_key=idempotency_key, - status=InvocationStatus.PREPARED, - created_at=now, - updated_at=now, - ) connection = self._connect() try: connection.execute("BEGIN IMMEDIATE") @@ -196,12 +194,35 @@ def prepare( ) connection.commit() return existing + row = connection.execute( + """ + SELECT COALESCE(MAX(invocation_sequence), 0) AS value + FROM tool_invocations WHERE run_id = ? + """, + (str(run_id),), + ).fetchone() + invocation_sequence = int(row["value"]) + 1 + invocation = ToolInvocation( + id=uuid4(), + run_id=run_id, + tool_name=tool_name, + effect_class=effect_class, + arguments_digest=arguments_digest, + idempotency_key=idempotency_key, + status=InvocationStatus.PREPARED, + created_at=now, + updated_at=now, + node_id=node_id, + checkpoint_sequence=checkpoint_sequence, + invocation_sequence=invocation_sequence, + ) connection.execute( """ INSERT INTO tool_invocations( id, run_id, tool_name, effect_class, idempotency_key, - arguments_digest, status, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + arguments_digest, status, created_at, updated_at, + node_id, checkpoint_sequence, invocation_sequence + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( str(invocation.id), @@ -213,6 +234,9 @@ def prepare( invocation.status.value, now.isoformat(), now.isoformat(), + node_id, + checkpoint_sequence, + invocation_sequence, ), ) connection.commit() @@ -259,9 +283,7 @@ def fail(self, invocation_id: UUID, error: str) -> ToolInvocation: error=error, ) - def reconcile_stale( - self, *, older_than: datetime - ) -> tuple[ToolInvocation, ...]: + def reconcile_stale(self, *, older_than: datetime) -> tuple[ToolInvocation, ...]: connection = self._connect() try: connection.execute("BEGIN IMMEDIATE") @@ -279,8 +301,11 @@ def reconcile_stale( effect = EffectClass(row["effect_class"]) target = ( InvocationStatus.PREPARED - if effect - in {EffectClass.NONE, EffectClass.READ_ONLY, EffectClass.IDEMPOTENT} + if effect in {EffectClass.NONE, EffectClass.READ_ONLY} + or ( + effect is EffectClass.IDEMPOTENT + and row["idempotency_key"] is not None + ) else InvocationStatus.AMBIGUOUS ) connection.execute( @@ -307,6 +332,23 @@ def reconcile_stale( finally: connection.close() + def watermark(self, run_id: UUID) -> int: + connection = self._connect() + try: + row = connection.execute( + """ + SELECT COALESCE( + MAX(invocation_sequence), + COUNT(*) + ) AS value FROM tool_invocations + WHERE run_id = ? AND status = ? + """, + (str(run_id), InvocationStatus.SUCCEEDED.value), + ).fetchone() + finally: + connection.close() + return int(row["value"]) if row is not None else 0 + def _transition( self, invocation_id: UUID, @@ -367,6 +409,17 @@ def _from_row(row: Mapping[str, Any]) -> ToolInvocation: error=row["error"], created_at=datetime.fromisoformat(row["created_at"]), updated_at=datetime.fromisoformat(row["updated_at"]), + node_id=(row["node_id"] if "node_id" in row.keys() else None), + checkpoint_sequence=( + row["checkpoint_sequence"] + if "checkpoint_sequence" in row.keys() + else None + ), + invocation_sequence=( + row["invocation_sequence"] + if "invocation_sequence" in row.keys() + else None + ), ) @@ -374,10 +427,6 @@ class PostgresEffectLedger: """Effect ledger sharing a production Postgres Run schema.""" def __init__(self, dsn: str, *, schema: str = "openrath") -> None: - from rath.runtime.postgres import PostgresRunStore - - bootstrap = PostgresRunStore(dsn, schema=schema) - bootstrap.close() self.dsn = dsn self.schema = schema @@ -400,17 +449,32 @@ def prepare( effect_class: EffectClass, arguments_digest: str, idempotency_key: str | None, + node_id: str | None = None, + checkpoint_sequence: int | None = None, ) -> ToolInvocation: now = datetime.now(timezone.utc) invocation_id = uuid4() connection = self._connect() try: + connection.execute( + "SELECT id FROM runs WHERE id = %s FOR UPDATE", + (run_id,), + ).fetchone() + sequence_row = connection.execute( + """ + SELECT COALESCE(MAX(invocation_sequence), 0) AS value + FROM tool_invocations WHERE run_id = %s + """, + (run_id,), + ).fetchone() + invocation_sequence = int(sequence_row["value"]) + 1 row = connection.execute( """ INSERT INTO tool_invocations( id, run_id, tool_name, effect_class, idempotency_key, - arguments_digest, status, created_at, updated_at - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + arguments_digest, status, created_at, updated_at, + node_id, checkpoint_sequence, invocation_sequence + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (run_id, idempotency_key) DO NOTHING RETURNING * """, ( @@ -423,6 +487,9 @@ def prepare( InvocationStatus.PREPARED.value, now, now, + node_id, + checkpoint_sequence, + invocation_sequence, ), ).fetchone() if row is None: @@ -486,9 +553,7 @@ def fail(self, invocation_id: UUID, error: str) -> ToolInvocation: error=error, ) - def reconcile_stale( - self, *, older_than: datetime - ) -> tuple[ToolInvocation, ...]: + def reconcile_stale(self, *, older_than: datetime) -> tuple[ToolInvocation, ...]: connection = self._connect() try: rows = connection.execute( @@ -504,8 +569,11 @@ def reconcile_stale( effect = EffectClass(row["effect_class"]) target = ( InvocationStatus.PREPARED - if effect - in {EffectClass.NONE, EffectClass.READ_ONLY, EffectClass.IDEMPOTENT} + if effect in {EffectClass.NONE, EffectClass.READ_ONLY} + or ( + effect is EffectClass.IDEMPOTENT + and row["idempotency_key"] is not None + ) else InvocationStatus.AMBIGUOUS ) updated = connection.execute( @@ -530,6 +598,24 @@ def reconcile_stale( finally: connection.close() + def watermark(self, run_id: UUID) -> int: + connection = self._connect() + try: + row = connection.execute( + """ + SELECT COALESCE( + MAX(invocation_sequence), + COUNT(*) + ) AS value FROM tool_invocations + WHERE run_id = %s AND status = %s + """, + (run_id, InvocationStatus.SUCCEEDED.value), + ).fetchone() + connection.commit() + finally: + connection.close() + return int(row["value"]) if row is not None else 0 + def _transition( self, invocation_id: UUID, @@ -587,4 +673,7 @@ def _from_row(row: Mapping[str, Any]) -> ToolInvocation: error=row["error"], created_at=row["created_at"], updated_at=row["updated_at"], + node_id=row.get("node_id"), + checkpoint_sequence=row.get("checkpoint_sequence"), + invocation_sequence=row.get("invocation_sequence"), ) diff --git a/src/rath/runtime/execution.py b/src/rath/runtime/execution.py new file mode 100644 index 0000000..58f3f68 --- /dev/null +++ b/src/rath/runtime/execution.py @@ -0,0 +1,139 @@ +"""Single governed execution boundary used by durable runtime workers.""" + +from __future__ import annotations + +import asyncio +import inspect +import time +from concurrent.futures import TimeoutError as FutureTimeout +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Coroutine, Protocol, cast + +from rath.definition import NodeKind, NodeSpec +from rath.runtime.effects import EffectLedger +from rath.runtime.models import Run +from rath.runtime.store import RunStore + +if TYPE_CHECKING: + from rath.adapters import ( + MemoryExecutor, + ProviderExecutor, + SandboxExecutor, + ToolExecutor, + ) + from rath.runtime.local import StepContext + from rath.security import AuditSink, PolicyEngine + +__all__ = [ + "ExecutionServices", + "PythonStepExecutor", + "StepSuspended", + "StepExecutor", +] + + +@dataclass(frozen=True, slots=True) +class ExecutionServices: + """Governed capabilities made available to a workflow step.""" + + policy: PolicyEngine + tools: ToolExecutor + providers: ProviderExecutor + sandboxes: SandboxExecutor + memory: MemoryExecutor + effects: EffectLedger + audit: AuditSink + + +class StepExecutor(Protocol): + """Execute one compiled node through the durable worker boundary.""" + + def execute( + self, + *, + handler: object, + state: dict[str, object], + context: StepContext, + node: NodeSpec, + run: Run, + ) -> object: ... + + +class StepSuspended(RuntimeError): + """Internal control flow indicating a durable interrupt boundary.""" + + +class PythonStepExecutor: + """Reference in-process executor for embedded and async durable steps.""" + + def __init__(self, store: RunStore) -> None: + self.store = store + + def execute( + self, + *, + handler: object, + state: dict[str, object], + context: StepContext, + node: NodeSpec, + run: Run, + ) -> object: + last_error: BaseException | None = None + for attempt in range(1, node.retry.max_attempts + 1): + try: + callable_handler = cast(Any, handler) + if node.is_async: + value = ( + callable_handler(state, context) + if node.kind is not NodeKind.ROUTER + else callable_handler(state) + ) + if not inspect.isawaitable(value): + raise TypeError( + f"async step {node.id!r} did not return an awaitable" + ) + from rath._async.runtime import runtime as async_runtime + + coroutine = cast(Coroutine[Any, Any, object], value) + if node.timeout_seconds is not None: + coroutine = cast( + Coroutine[Any, Any, object], + asyncio.wait_for(coroutine, node.timeout_seconds), + ) + return async_runtime().run(coroutine) + + arguments = ( + (state,) if node.kind is NodeKind.ROUTER else (state, context) + ) + if node.timeout_seconds is None: + return callable_handler(*arguments) + started = time.monotonic() + result = callable_handler(*arguments) + if time.monotonic() - started > node.timeout_seconds: + raise FutureTimeout( + f"sync step {node.id!r} exceeded timeout; " + "the handler was allowed to stop before terminalization" + ) + return result + except StepSuspended: + raise + except BaseException as exc: + last_error = exc + self.store.append_run_event( + run.id, + "run.step.attempt.failed", + { + "node_id": node.id, + "attempt": attempt, + "error_type": type(exc).__name__, + }, + ) + if attempt >= node.retry.max_attempts: + raise + delay = min( + node.retry.max_seconds, + node.retry.base_seconds * (2 ** (attempt - 1)), + ) + time.sleep(delay) + assert last_error is not None + raise last_error diff --git a/src/rath/runtime/local.py b/src/rath/runtime/local.py index 7bb8cf9..224b66e 100644 --- a/src/rath/runtime/local.py +++ b/src/rath/runtime/local.py @@ -2,19 +2,17 @@ from __future__ import annotations -import inspect import threading -import time from asyncio import TimeoutError as AsyncTimeoutError from collections.abc import Callable, Mapping -from concurrent.futures import ThreadPoolExecutor from concurrent.futures import TimeoutError as FutureTimeout -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from datetime import datetime -from typing import Any, Coroutine, cast +from typing import cast from uuid import UUID -from rath._json import JSONValue, thaw_json +from rath._json import JSONValue, freeze_mapping, thaw_json +from rath.adapters.schema import validate_json from rath.context import DeadlineExceededError, RunContext, TraceContext from rath.definition import ExecutionPlan, NodeKind, WorkflowCompiler from rath.observability import ( @@ -23,6 +21,17 @@ StructuredLogger, Telemetry, ) +from rath.runtime.effects import ( + EffectLedger, + Reconciliation, + reconcile_stale_effects, +) +from rath.runtime.execution import ( + ExecutionServices, + PythonStepExecutor, + StepExecutor, + StepSuspended, +) from rath.runtime.models import ( TERMINAL_RUN_STATUSES, ApprovalDecision, @@ -37,7 +46,11 @@ from rath.runtime.store import RunStore from rath.security import Principal, PrincipalKind, SecurityContext -__all__ = ["LocalRuntime", "StepContext"] +__all__ = ["LocalRuntime", "PlanMismatchError", "StepContext"] + + +class PlanMismatchError(RuntimeError): + """A durable checkpoint does not belong to the registered executable plan.""" @dataclass(frozen=True, slots=True) @@ -46,6 +59,8 @@ class StepContext: request: RunContext worker_id: str fencing_token: int + policy_manifest: Mapping[str, JSONValue] + services: ExecutionServices | None _interrupt_handler: Callable[ [InterruptKind, Mapping[str, object], float | None], ApprovalDecision ] = field(repr=False) @@ -61,10 +76,6 @@ def interrupt( return self._interrupt_handler(kind, request, timeout_seconds) -class _RunSuspended(RuntimeError): - pass - - @dataclass(frozen=True, slots=True) class _Registration: workflow: object @@ -80,15 +91,44 @@ def __init__( *, telemetry: Telemetry | None = None, structured_logger: StructuredLogger | None = None, + effect_ledger: EffectLedger | None = None, + production_mode: bool = False, + execution_services: ExecutionServices | None = None, + step_executor: StepExecutor | None = None, ) -> None: self.store = store self.telemetry = GuardedTelemetry(telemetry or NoOpTelemetry()) self.structured_logger = structured_logger or StructuredLogger() + if ( + effect_ledger is not None + and execution_services is not None + and effect_ledger is not execution_services.effects + ): + raise ValueError( + "effect_ledger and execution_services.effects must be identical" + ) + self.execution_services = execution_services + self.effect_ledger = ( + effect_ledger + if effect_ledger is not None + else execution_services.effects + if execution_services is not None + else None + ) + self.production_mode = production_mode + self.step_executor = step_executor or PythonStepExecutor(store) self._registrations: dict[UUID, _Registration] = {} self._contexts: dict[UUID, RunContext] = {} def register(self, workflow: object, *, revision_id: UUID) -> ExecutionPlan: - plan = WorkflowCompiler().compile(workflow, revision_id=revision_id) + plan = WorkflowCompiler().compile( + workflow, + revision_id=revision_id, + production_durable=self.production_mode, + input_schema=getattr(workflow, "input_schema", None), + state_schema=getattr(workflow, "state_schema", None), + policy_manifest=getattr(workflow, "policy_manifest", None), + ) if not plan.durable: raise ValueError( "durable runtime requires explicit @step boundaries; " @@ -109,6 +149,9 @@ def submit( ) -> Run: context.ensure_active() plan = self.register(workflow, revision_id=context.revision_id) + input_state = state or {} + if plan.definition.input_schema: + validate_json(input_state, plan.definition.input_schema) run = Run.create( plan_id=plan.id, revision_id=plan.revision_id, @@ -131,7 +174,9 @@ def submit( "span_id": context.trace_context.span_id, "sampled": context.trace_context.sampled, "deadline": ( - context.deadline.isoformat() if context.deadline is not None else None + context.deadline.isoformat() + if context.deadline is not None + else None ), }, priority=priority, @@ -195,14 +240,12 @@ def work_once( finally: stop_heartbeat.set() heartbeat.join(timeout=min(1.0, lease_seconds)) - except _RunSuspended: + except StepSuspended: waiting = self.store.get_run(claim.run.id) self.structured_logger.emit( "run.suspended", context=( - claim_context.trace_context - if claim_context is not None - else None + claim_context.trace_context if claim_context is not None else None ), fields={ "run_id": str(waiting.id), @@ -229,24 +272,28 @@ def work_once( ) else RunStatus.FAILED ) - failed = self.store.finish_claim( - claim.run.id, - worker_id=worker_id, - fencing_token=claim.lease.fencing_token, - expected_run_version=current.version, - target=target, - event_type="run.execution.failed", - event_data={ - "error_type": type(exc).__name__, - "message": str(exc), - }, - ) + try: + failed = self.store.finish_claim( + claim.run.id, + worker_id=worker_id, + fencing_token=claim.lease.fencing_token, + expected_run_version=current.version, + target=target, + event_type="run.execution.failed", + event_data={ + "error_type": type(exc).__name__, + "message": str(exc), + }, + ) + except ConflictError: + # Lease loss/fencing means this worker no longer owns the right + # to publish a terminal result. Recovery will reconcile effects + # and requeue the durable Run under a new fencing token. + return self.store.get_run(claim.run.id) self.structured_logger.emit( "run.failed", context=( - claim_context.trace_context - if claim_context is not None - else None + claim_context.trace_context if claim_context is not None else None ), fields={ "run_id": str(failed.id), @@ -272,6 +319,14 @@ def _execute_claim( if context is None: context = self._restore_context(claim.run) run = claim.run + latest_checkpoint = self.store.latest_checkpoint(run.id) + if ( + latest_checkpoint is not None + and latest_checkpoint.plan_hash != registration.plan.definition_hash + ): + raise PlanMismatchError( + "checkpoint plan hash does not match registered execution plan" + ) steps = 0 by_id = {node.id: node for node in registration.plan.nodes} while run.next_nodes and (max_steps is None or steps < max_steps): @@ -301,13 +356,11 @@ def request_interrupt( if ( existing.run_id == run.id and existing.request.get("_openrath_node_id") == node.id - and existing.request.get( - "_openrath_checkpoint_sequence" - ) + and existing.request.get("_openrath_checkpoint_sequence") == checkpoint_sequence ): if existing.decision is None: - raise _RunSuspended("run is waiting for a decision") + raise StepSuspended("run is waiting for a decision") return existing.decision interrupt = Interrupt.create( run_id=run.id, @@ -323,13 +376,15 @@ def request_interrupt( interrupt, expected_run_version=run.version, ) - raise _RunSuspended("run was suspended for a decision") + raise StepSuspended("run was suspended for a decision") step_context = StepContext( run_id=run.id, request=context, worker_id=claim.lease.holder_worker_id, fencing_token=claim.lease.fencing_token, + policy_manifest=registration.plan.policy_manifest, + services=self.execution_services, _interrupt_handler=request_interrupt, ) with self.telemetry.span( @@ -337,10 +392,10 @@ def request_interrupt( context=context.trace_context, attributes={"run_id": str(run.id), "node_id": node.id}, ): - result = self._invoke_with_retry( - handler, - state_value, - step_context, + result = self.step_executor.execute( + handler=handler, + state=state_value, + context=step_context, node=node, run=run, ) @@ -359,29 +414,44 @@ def request_interrupt( elif isinstance(result, Mapping): next_state = dict(result) else: - raise TypeError( - f"step {node.id!r} must return a mapping or None" - ) + raise TypeError(f"step {node.id!r} must return a mapping or None") if len(node.successors) > 1: raise ValueError( f"step {node.id!r} has multiple successors; use @router" ) next_nodes = node.successors - checkpoint = Checkpoint.create( - run_id=run.id, - sequence=checkpoint_sequence, - plan_hash=registration.plan.definition_hash, - state=next_state, - next_nodes=next_nodes, - effect_watermark=0, - ) - run = self.store.commit_checkpoint( - checkpoint, - worker_id=claim.lease.holder_worker_id, - fencing_token=claim.lease.fencing_token, - expected_run_version=run.version, - ) + if registration.plan.definition.state_schema: + validate_json( + next_state, + registration.plan.definition.state_schema, + ) + + if node.checkpoint or not next_nodes: + checkpoint = Checkpoint.create( + run_id=run.id, + sequence=checkpoint_sequence, + plan_hash=registration.plan.definition_hash, + state=next_state, + next_nodes=next_nodes, + effect_watermark=( + self.effect_ledger.watermark(run.id) + if self.effect_ledger is not None + else 0 + ), + ) + run = self.store.commit_checkpoint( + checkpoint, + worker_id=claim.lease.holder_worker_id, + fencing_token=claim.lease.fencing_token, + expected_run_version=run.version, + ) + else: + run = replace( + run, + state=freeze_mapping(next_state, field="run.state"), + next_nodes=next_nodes, + ) steps += 1 self.telemetry.increment( "openrath.node.completed", @@ -408,74 +478,20 @@ def request_interrupt( return completed return run - def _invoke_with_retry( + def reconcile_effects( self, - handler: object, - state: dict[str, object], - step_context: StepContext, *, - node: object, - run: Run, - ) -> object: - from rath.definition import NodeSpec - - spec = cast(NodeSpec, node) - last_error: BaseException | None = None - for attempt in range(1, spec.retry.max_attempts + 1): - try: - callable_handler = cast(Any, handler) - if spec.is_async: - value = callable_handler( - state, - step_context, - ) if spec.kind is not NodeKind.ROUTER else callable_handler(state) - assert inspect.isawaitable(value) - from rath._async.runtime import runtime as async_runtime - - coroutine = cast(Coroutine[Any, Any, object], value) - if spec.timeout_seconds is not None: - import asyncio - - coroutine = cast( - Coroutine[Any, Any, object], - asyncio.wait_for(coroutine, spec.timeout_seconds), - ) - return async_runtime().run(coroutine) - arguments = ( - (state,) - if spec.kind is NodeKind.ROUTER - else (state, step_context) - ) - if spec.timeout_seconds is None: - return callable_handler(*arguments) - pool = ThreadPoolExecutor(max_workers=1) - try: - future = pool.submit(callable_handler, *arguments) - return future.result(timeout=spec.timeout_seconds) - finally: - pool.shutdown(wait=False, cancel_futures=True) - except _RunSuspended: - raise - except BaseException as exc: - last_error = exc - self.store.append_run_event( - run.id, - "run.step.attempt.failed", - { - "node_id": spec.id, - "attempt": attempt, - "error_type": type(exc).__name__, - }, - ) - if attempt >= spec.retry.max_attempts: - raise - delay = min( - spec.retry.max_seconds, - spec.retry.base_seconds * (2 ** (attempt - 1)), - ) - time.sleep(delay) - assert last_error is not None - raise last_error + grace_seconds: float = 30.0, + now: datetime | None = None, + ) -> Reconciliation: + if self.effect_ledger is None: + return Reconciliation((), ()) + return reconcile_stale_effects( + self.effect_ledger, + self.store, + grace_seconds=grace_seconds, + now=now, + ) def _heartbeat( self, @@ -522,9 +538,7 @@ def _restore_context(run: Run) -> RunContext: principal=Principal( id=str(principal["id"]), kind=PrincipalKind(str(principal["kind"])), - claims=cast( - Mapping[str, JSONValue], principal.get("claims") or {} - ), + claims=cast(Mapping[str, JSONValue], principal.get("claims") or {}), ), tenant_id=run.tenant_id, project_id=( @@ -533,9 +547,7 @@ def _restore_context(run: Run) -> RunContext: else None ), grants=frozenset(str(item) for item in raw.get("grants", [])), - attributes=cast( - Mapping[str, JSONValue], raw.get("attributes") or {} - ), + attributes=cast(Mapping[str, JSONValue], raw.get("attributes") or {}), ), revision_id=run.revision_id, request_id=UUID(str(raw["request_id"])), diff --git a/src/rath/runtime/migrations/postgres/0002_release_hardening.sql b/src/rath/runtime/migrations/postgres/0002_release_hardening.sql new file mode 100644 index 0000000..67d7254 --- /dev/null +++ b/src/rath/runtime/migrations/postgres/0002_release_hardening.sql @@ -0,0 +1,26 @@ +ALTER TABLE schema_migrations + ADD COLUMN IF NOT EXISTS filename TEXT; +ALTER TABLE schema_migrations + ADD COLUMN IF NOT EXISTS checksum TEXT; + +ALTER TABLE tool_invocations + ADD COLUMN IF NOT EXISTS node_id TEXT; +ALTER TABLE tool_invocations + ADD COLUMN IF NOT EXISTS checkpoint_sequence BIGINT; +ALTER TABLE tool_invocations + ADD COLUMN IF NOT EXISTS invocation_sequence BIGINT; + +CREATE UNIQUE INDEX IF NOT EXISTS tool_invocations_run_sequence_idx + ON tool_invocations (run_id, invocation_sequence) + WHERE invocation_sequence IS NOT NULL; + +CREATE INDEX IF NOT EXISTS run_events_created_idx + ON run_events (run_id, created_at, sequence); + +ALTER TABLE revisions + ADD COLUMN IF NOT EXISTS content_digest TEXT; +UPDATE revisions + SET content_digest = code_digest || ':' || id::text + WHERE content_digest IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS revisions_content_digest_idx + ON revisions (content_digest); diff --git a/src/rath/runtime/postgres.py b/src/rath/runtime/postgres.py index 9c1068c..9692017 100644 --- a/src/rath/runtime/postgres.py +++ b/src/rath/runtime/postgres.py @@ -45,7 +45,14 @@ def _json(value: object) -> object: class PostgresRunStore: """Transactional Postgres store using row locks and fencing tokens.""" - def __init__(self, dsn: str, *, schema: str = "openrath") -> None: + def __init__( + self, + dsn: str, + *, + schema: str = "openrath", + auto_migrate: bool = False, + pool_max_size: int = 20, + ) -> None: if not dsn.strip(): raise ValueError("dsn must not be empty") if not _SCHEMA_NAME.fullmatch(schema): @@ -53,7 +60,10 @@ def __init__(self, dsn: str, *, schema: str = "openrath") -> None: self.dsn = dsn self.schema = schema self._closed = False - self._migrate() + if pool_max_size < 1: + raise ValueError("pool_max_size must be positive") + if auto_migrate: + self.migrate(dsn, schema=schema) try: from psycopg import sql from psycopg.rows import dict_row @@ -66,7 +76,7 @@ def __init__(self, dsn: str, *, schema: str = "openrath") -> None: self._pool = ConnectionPool( self.dsn, min_size=1, - max_size=20, + max_size=pool_max_size, timeout=10, kwargs={"row_factory": dict_row}, configure=self._configure_connection, @@ -81,7 +91,35 @@ def _configure_connection(self, connection: Any) -> None: ) connection.commit() - def _migrate(self) -> None: + @staticmethod + def _migrations() -> tuple[tuple[int, str, str, str], ...]: + root = files("rath.runtime").joinpath("migrations/postgres") + output: list[tuple[int, str, str, str]] = [] + for item in root.iterdir(): + match = re.fullmatch(r"(\d{4})_[a-z0-9_]+\.sql", item.name) + if match is None: + continue + migration = item.read_text(encoding="utf-8") + output.append( + ( + int(match.group(1)), + item.name, + migration, + hashlib.sha256(migration.encode("utf-8")).hexdigest(), + ) + ) + output.sort(key=lambda item: item[0]) + expected = list(range(1, len(output) + 1)) + if [item[0] for item in output] != expected: + raise RuntimeError("PostgreSQL migrations must be contiguous from 0001") + return tuple(output) + + @classmethod + def migrate(cls, dsn: str, *, schema: str = "openrath") -> None: + if not dsn.strip(): + raise ValueError("dsn must not be empty") + if not _SCHEMA_NAME.fullmatch(schema): + raise ValueError("schema must be a safe lowercase PostgreSQL identifier") try: import psycopg from psycopg import sql @@ -89,27 +127,134 @@ def _migrate(self) -> None: raise RuntimeError( "Postgres support requires `pip install openrath[postgres]`" ) from exc - migration = ( - files("rath.runtime") - .joinpath("migrations/postgres/0001_initial.sql") - .read_text(encoding="utf-8") - ) - with psycopg.connect(self.dsn) as connection: + migrations = cls._migrations() + with psycopg.connect(dsn) as connection: connection.execute( - sql.SQL("CREATE SCHEMA IF NOT EXISTS {}").format( - sql.Identifier(self.schema) - ) + sql.SQL("CREATE SCHEMA IF NOT EXISTS {}").format(sql.Identifier(schema)) ) connection.execute( - sql.SQL("SET search_path TO {}").format(sql.Identifier(self.schema)) + sql.SQL("SET search_path TO {}").format(sql.Identifier(schema)) ) - connection.execute(migration) + table_row = connection.execute( + "SELECT to_regclass('schema_migrations')" + ).fetchone() + table_exists = table_row[0] if table_row is not None else None + for migration_version, filename, migration, checksum in migrations: + existing = ( + connection.execute( + "SELECT version FROM schema_migrations WHERE version = %s", + (migration_version,), + ).fetchone() + if table_exists is not None + else None + ) + if existing is not None: + continue + connection.execute(migration) + table_exists = "schema_migrations" + if migration_version == 1: + connection.execute( + """ + INSERT INTO schema_migrations(version, applied_at) + VALUES (%s, %s) + """, + (migration_version, _now()), + ) + else: + connection.execute( + """ + INSERT INTO schema_migrations( + version, filename, checksum, applied_at + ) VALUES (%s, %s, %s, %s) + """, + (migration_version, filename, checksum, _now()), + ) + # Migration 0002 introduces checksum metadata. Backfill the + # immutable 0001 identity for legacy schemas after it is applied. + first = migrations[0] connection.execute( """ - INSERT INTO schema_migrations(version, applied_at) - VALUES (1, %s) ON CONFLICT (version) DO NOTHING + UPDATE schema_migrations + SET filename = COALESCE(filename, %s), + checksum = COALESCE(checksum, %s) + WHERE version = 1 """, - (_now(),), + (first[1], first[3]), + ) + for migration_version, filename, _, checksum in migrations: + row = connection.execute( + """ + SELECT filename, checksum FROM schema_migrations + WHERE version = %s + """, + (migration_version,), + ).fetchone() + if row != (filename, checksum): + raise RuntimeError(f"migration {filename} checksum mismatch") + + @classmethod + def verify_schema(cls, dsn: str, *, schema: str = "openrath") -> None: + if not _SCHEMA_NAME.fullmatch(schema): + raise ValueError("schema must be a safe lowercase PostgreSQL identifier") + import psycopg + from psycopg import sql + + migrations = cls._migrations() + required_tables = { + "runs", + "run_events", + "checkpoints", + "interrupts", + "run_leases", + "tool_invocations", + } + with psycopg.connect(dsn) as connection: + migration_rows = connection.execute( + sql.SQL( + """ + SELECT version, filename, checksum + FROM {}.schema_migrations ORDER BY version + """ + ).format(sql.Identifier(schema)) + ).fetchall() + rows = connection.execute( + """ + SELECT table_name FROM information_schema.tables + WHERE table_schema = %s + """, + (schema,), + ).fetchall() + column_rows = connection.execute( + """ + SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema = %s + AND table_name IN ('schema_migrations', 'tool_invocations') + """, + (schema,), + ).fetchall() + expected_rows = [ + (migration_version, filename, checksum) + for migration_version, filename, _, checksum in migrations + ] + if migration_rows != expected_rows: + raise RuntimeError("OpenRath schema migration checksums are not current") + present = {str(value[0]) for value in rows} + missing = required_tables - present + if missing: + raise RuntimeError(f"OpenRath schema is missing tables: {sorted(missing)}") + present_columns = {(str(row[0]), str(row[1])) for row in column_rows} + required_columns = { + ("schema_migrations", "filename"), + ("schema_migrations", "checksum"), + ("tool_invocations", "node_id"), + ("tool_invocations", "checkpoint_sequence"), + ("tool_invocations", "invocation_sequence"), + } + missing_columns = required_columns - present_columns + if missing_columns: + raise RuntimeError( + f"OpenRath schema is missing columns: {sorted(missing_columns)}" ) @contextmanager @@ -149,7 +294,7 @@ def create_run(self, run: Run) -> Run: ) return self._run_from_row(existing) inserted = connection.execute( - """ + """ INSERT INTO runs( id, plan_id, revision_id, session_id, tenant_id, status, state_json, next_nodes_json, idempotency_key, context_json, @@ -161,24 +306,24 @@ def create_run(self, run: Run) -> Run: ON CONFLICT DO NOTHING RETURNING id """, - ( - run.id, - run.plan_id, - run.revision_id, - run.session_id, - run.tenant_id, - run.status.value, - _json(run.state), - _json(run.next_nodes), - run.idempotency_key, - _json(run.context), - run.priority, - fingerprint, - run.created_at, - run.updated_at, - run.version, - ), - ).fetchone() + ( + run.id, + run.plan_id, + run.revision_id, + run.session_id, + run.tenant_id, + run.status.value, + _json(run.state), + _json(run.next_nodes), + run.idempotency_key, + _json(run.context), + run.priority, + fingerprint, + run.created_at, + run.updated_at, + run.version, + ), + ).fetchone() if inserted is None: if run.idempotency_key is not None: existing = connection.execute( @@ -208,17 +353,72 @@ def get_run(self, run_id: UUID) -> Run: raise KeyError(str(run_id)) return self._run_from_row(row) - def list_runs(self, *, tenant_id: str) -> tuple[Run, ...]: + def list_runs( + self, + *, + tenant_id: str, + after: UUID | None = None, + limit: int | None = None, + session_id: UUID | None = None, + statuses: tuple[RunStatus, ...] | None = None, + ) -> tuple[Run, ...]: + if limit is not None and limit < 1: + raise ValueError("limit must be positive") with self._transaction() as connection: + clauses = ["tenant_id = %s"] + parameters: list[object] = [tenant_id] + if after is not None: + cursor = connection.execute( + """ + SELECT created_at, id FROM runs + WHERE id = %s AND tenant_id = %s + """, + (after, tenant_id), + ).fetchone() + if cursor is None: + raise KeyError(str(after)) + clauses.append("(created_at, id) > (%s, %s)") + parameters.extend((cursor["created_at"], cursor["id"])) + if session_id is not None: + clauses.append("session_id = %s") + parameters.append(session_id) + if statuses: + clauses.append("status = ANY(%s)") + parameters.append([item.value for item in statuses]) + suffix = " LIMIT %s" if limit is not None else "" + if limit is not None: + parameters.append(limit) rows = connection.execute( - """ - SELECT * FROM runs WHERE tenant_id = %s - ORDER BY created_at, id + f""" + SELECT * FROM runs WHERE {" AND ".join(clauses)} + ORDER BY created_at, id{suffix} """, - (tenant_id,), + parameters, ).fetchall() return tuple(self._run_from_row(row) for row in rows) + def count_runs( + self, + *, + status: RunStatus, + tenant_id: str | None = None, + ) -> int: + with self._transaction() as connection: + if tenant_id is None: + row = connection.execute( + "SELECT COUNT(*) AS value FROM runs WHERE status = %s", + (status.value,), + ).fetchone() + else: + row = connection.execute( + """ + SELECT COUNT(*) AS value FROM runs + WHERE status = %s AND tenant_id = %s + """, + (status.value, tenant_id), + ).fetchone() + return int(row["value"]) + def transition_run( self, run_id: UUID, @@ -258,14 +458,28 @@ def transition_run( ) return self._run_from_row(row) - def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: + def list_run_events( + self, + run_id: UUID, + *, + after_sequence: int = 0, + limit: int | None = None, + ) -> tuple[RunEvent, ...]: + if after_sequence < 0: + raise ValueError("after_sequence must not be negative") + if limit is not None and limit < 1: + raise ValueError("limit must be positive") with self._transaction() as connection: + suffix = " LIMIT %s" if limit is not None else "" + parameters: list[object] = [run_id, after_sequence] + if limit is not None: + parameters.append(limit) rows = connection.execute( - """ + f""" SELECT * FROM run_events - WHERE run_id = %s ORDER BY sequence + WHERE run_id = %s AND sequence > %s ORDER BY sequence{suffix} """, - (run_id,), + parameters, ).fetchall() return tuple( RunEvent( @@ -535,9 +749,7 @@ def expire_interrupts( ) if RunStatus(row["run_status"]) is RunStatus.WAITING: current = self._run_from_row( - self._required_run_row( - connection, row["run_id"], lock=True - ) + self._required_run_row(connection, row["run_id"], lock=True) ) self._update_status( connection, @@ -643,9 +855,7 @@ def claim_next( lease_id = ( previous["id"] if previous is not None - else UUID( - bytes=hashlib.sha256(str(current.id).encode()).digest()[:16] - ) + else UUID(bytes=hashlib.sha256(str(current.id).encode()).digest()[:16]) ) token = int(previous["fencing_token"]) + 1 if previous else 1 created_at = previous["created_at"] if previous else claimed_at @@ -824,6 +1034,7 @@ def _required_lease( or not row["active"] or row["holder_worker_id"] != worker_id or int(row["fencing_token"]) != fencing_token + or row["expires_at"] <= _now() ): raise ConflictError("lease fencing token is stale or not owned") return cast(Mapping[str, Any], row) @@ -892,6 +1103,12 @@ def _append_event( type: str, data: Mapping[str, object], ) -> RunEvent: + # Serialize sequence allocation with every other event writer for this + # Run. MAX(sequence)+1 is safe only while the parent row is locked. + connection.execute( + "SELECT id FROM runs WHERE id = %s FOR UPDATE", + (run_id,), + ).fetchone() row = connection.execute( """ SELECT COALESCE(MAX(sequence), 0) AS sequence diff --git a/src/rath/runtime/signals.py b/src/rath/runtime/signals.py index d7e51e5..941e9c1 100644 --- a/src/rath/runtime/signals.py +++ b/src/rath/runtime/signals.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import math import queue from dataclasses import dataclass from datetime import datetime @@ -102,11 +103,17 @@ def publish(self, signal: RunSignal) -> None: self.client.ltrim(self.key, 0, 9999) # type: ignore[attr-defined] def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: - timeout = max(0, int(timeout_seconds)) - result = self.client.brpop(self.key, timeout=timeout) # type: ignore[attr-defined] - if result is None: + if timeout_seconds <= 0: + payload = self.client.rpop(self.key) # type: ignore[attr-defined] + else: + timeout = max(1, math.ceil(timeout_seconds)) + result = self.client.brpop( # type: ignore[attr-defined] + self.key, + timeout=timeout, + ) + payload = result[1] if result is not None else None + if payload is None: return None - _, payload = result data = json.loads(payload) return RunSignal( kind=SignalKind(data["kind"]), diff --git a/src/rath/runtime/sqlite.py b/src/rath/runtime/sqlite.py index c13d4da..7adaf6f 100644 --- a/src/rath/runtime/sqlite.py +++ b/src/rath/runtime/sqlite.py @@ -128,6 +128,9 @@ error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, + node_id TEXT, + checkpoint_sequence INTEGER, + invocation_sequence INTEGER, UNIQUE (run_id, idempotency_key) ); @@ -185,6 +188,7 @@ CREATE TABLE IF NOT EXISTS revisions ( id TEXT PRIMARY KEY, + content_digest TEXT NOT NULL UNIQUE, code_digest TEXT NOT NULL, plan_hash TEXT NOT NULL, manifest_json TEXT NOT NULL, @@ -259,6 +263,52 @@ def _migrate(self) -> None: connection.execute( "ALTER TABLE runs ADD COLUMN priority INTEGER NOT NULL DEFAULT 0" ) + revision_columns = { + row[1] + for row in connection.execute( + "PRAGMA table_info(revisions)" + ).fetchall() + } + if "content_digest" not in revision_columns: + connection.execute( + "ALTER TABLE revisions ADD COLUMN content_digest TEXT" + ) + connection.execute( + """ + UPDATE revisions + SET content_digest = code_digest || ':' || id + WHERE content_digest IS NULL + """ + ) + invocation_columns = { + row[1] + for row in connection.execute( + "PRAGMA table_info(tool_invocations)" + ).fetchall() + } + for name, kind in ( + ("node_id", "TEXT"), + ("checkpoint_sequence", "INTEGER"), + ("invocation_sequence", "INTEGER"), + ): + if name not in invocation_columns: + connection.execute( + f"ALTER TABLE tool_invocations ADD COLUMN {name} {kind}" + ) + connection.execute( + """ + CREATE UNIQUE INDEX IF NOT EXISTS + tool_invocations_run_sequence_idx + ON tool_invocations (run_id, invocation_sequence) + WHERE invocation_sequence IS NOT NULL + """ + ) + connection.execute( + """ + CREATE UNIQUE INDEX IF NOT EXISTS revisions_content_digest_idx + ON revisions (content_digest) + """ + ) interrupt_columns = { row[1] for row in connection.execute( @@ -369,17 +419,76 @@ def get_run(self, run_id: UUID) -> Run: raise KeyError(str(run_id)) return self._run_from_row(row) - def list_runs(self, *, tenant_id: str) -> tuple[Run, ...]: + def list_runs( + self, + *, + tenant_id: str, + after: UUID | None = None, + limit: int | None = None, + session_id: UUID | None = None, + statuses: tuple[RunStatus, ...] | None = None, + ) -> tuple[Run, ...]: + if limit is not None and limit < 1: + raise ValueError("limit must be positive") connection = self._connect() try: + clauses = ["tenant_id = ?"] + parameters: list[object] = [tenant_id] + if after is not None: + cursor = connection.execute( + "SELECT created_at, id FROM runs WHERE id = ? AND tenant_id = ?", + (str(after), tenant_id), + ).fetchone() + if cursor is None: + raise KeyError(str(after)) + clauses.append("(created_at, id) > (?, ?)") + parameters.extend((cursor["created_at"], cursor["id"])) + if session_id is not None: + clauses.append("session_id = ?") + parameters.append(str(session_id)) + if statuses: + placeholders = ", ".join("?" for _ in statuses) + clauses.append(f"status IN ({placeholders})") + parameters.extend(item.value for item in statuses) + suffix = " LIMIT ?" if limit is not None else "" + if limit is not None: + parameters.append(limit) rows = connection.execute( - "SELECT * FROM runs WHERE tenant_id = ? ORDER BY created_at, id", - (tenant_id,), + f""" + SELECT * FROM runs WHERE {" AND ".join(clauses)} + ORDER BY created_at, id{suffix} + """, + parameters, ).fetchall() finally: connection.close() return tuple(self._run_from_row(row) for row in rows) + def count_runs( + self, + *, + status: RunStatus, + tenant_id: str | None = None, + ) -> int: + connection = self._connect() + try: + if tenant_id is None: + row = connection.execute( + "SELECT COUNT(*) AS value FROM runs WHERE status = ?", + (status.value,), + ).fetchone() + else: + row = connection.execute( + """ + SELECT COUNT(*) AS value FROM runs + WHERE status = ? AND tenant_id = ? + """, + (status.value, tenant_id), + ).fetchone() + finally: + connection.close() + return int(row["value"]) + def transition_run( self, run_id: UUID, @@ -431,15 +540,29 @@ def transition_run( updated_row = self._required_run_row(connection, run_id) return self._run_from_row(updated_row) - def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: + def list_run_events( + self, + run_id: UUID, + *, + after_sequence: int = 0, + limit: int | None = None, + ) -> tuple[RunEvent, ...]: + if after_sequence < 0: + raise ValueError("after_sequence must not be negative") + if limit is not None and limit < 1: + raise ValueError("limit must be positive") connection = self._connect() try: + suffix = " LIMIT ?" if limit is not None else "" + parameters: list[object] = [str(run_id), after_sequence] + if limit is not None: + parameters.append(limit) rows = connection.execute( - """ + f""" SELECT * FROM run_events - WHERE run_id = ? ORDER BY sequence + WHERE run_id = ? AND sequence > ? ORDER BY sequence{suffix} """, - (str(run_id),), + parameters, ).fetchall() finally: connection.close() @@ -894,12 +1017,14 @@ def claim_next( "SELECT * FROM run_leases WHERE run_id = ?", (str(current.id),), ).fetchone() - lease_id = UUID(previous["id"]) if previous is not None else UUID( - bytes=hashlib.sha256(str(current.id).encode("utf-8")).digest()[:16] - ) - token = ( - int(previous["fencing_token"]) + 1 if previous is not None else 1 + lease_id = ( + UUID(previous["id"]) + if previous is not None + else UUID( + bytes=hashlib.sha256(str(current.id).encode("utf-8")).digest()[:16] + ) ) + token = int(previous["fencing_token"]) + 1 if previous is not None else 1 created_at = ( _parse_time(previous["created_at"]) if previous is not None @@ -1176,6 +1301,7 @@ def _required_lease( or not bool(row["active"]) or row["holder_worker_id"] != worker_id or int(row["fencing_token"]) != fencing_token + or _parse_time(row["expires_at"]) <= _now() ): raise ConflictError("lease fencing token is stale or not owned") return cast(sqlite3.Row, row) diff --git a/src/rath/runtime/store.py b/src/rath/runtime/store.py index b168dd5..3279505 100644 --- a/src/rath/runtime/store.py +++ b/src/rath/runtime/store.py @@ -27,7 +27,22 @@ def create_run(self, run: Run) -> Run: ... def get_run(self, run_id: UUID) -> Run: ... - def list_runs(self, *, tenant_id: str) -> tuple[Run, ...]: ... + def list_runs( + self, + *, + tenant_id: str, + after: UUID | None = None, + limit: int | None = None, + session_id: UUID | None = None, + statuses: tuple[RunStatus, ...] | None = None, + ) -> tuple[Run, ...]: ... + + def count_runs( + self, + *, + status: RunStatus, + tenant_id: str | None = None, + ) -> int: ... def transition_run( self, @@ -39,7 +54,13 @@ def transition_run( next_nodes: tuple[str, ...] | None = None, ) -> Run: ... - def list_run_events(self, run_id: UUID) -> tuple[RunEvent, ...]: ... + def list_run_events( + self, + run_id: UUID, + *, + after_sequence: int = 0, + limit: int | None = None, + ) -> tuple[RunEvent, ...]: ... def append_run_event( self, diff --git a/src/rath/security/context.py b/src/rath/security/context.py index 298e4d3..57ffd3c 100644 --- a/src/rath/security/context.py +++ b/src/rath/security/context.py @@ -133,4 +133,3 @@ def __post_init__(self) -> None: "metadata", freeze_mapping(self.metadata, field="provenance.metadata"), ) - diff --git a/src/rath/security/policy.py b/src/rath/security/policy.py index 9ffb8f0..6b392ad 100644 --- a/src/rath/security/policy.py +++ b/src/rath/security/policy.py @@ -190,9 +190,8 @@ async def evaluate( resource: ResourceRef, context: RunContext, ) -> PolicyDecision: - allowed = ( - context.security.tenant_id == "local" - and context.security.has_grant("trusted_host") + allowed = context.security.tenant_id == "local" and context.security.has_grant( + "trusted_host" ) return PolicyDecision( effect=PolicyEffect.ALLOW if allowed else PolicyEffect.DENY, diff --git a/src/rath/server/app.py b/src/rath/server/app.py index 872194e..2b53c1b 100644 --- a/src/rath/server/app.py +++ b/src/rath/server/app.py @@ -7,6 +7,7 @@ from collections.abc import AsyncIterator, Mapping from contextlib import asynccontextmanager from dataclasses import dataclass +from datetime import datetime, timezone from importlib.metadata import PackageNotFoundError, version from typing import Any, cast from uuid import UUID, uuid4 @@ -38,8 +39,16 @@ SignalBus, SignalKind, ) -from rath.security import PolicyConstraints, SecurityContext, TrustLevel +from rath.security import ( + AuditEvent, + AuditKind, + AuditSink, + PolicyConstraints, + SecurityContext, + TrustLevel, +) from rath.server.auth import AuthProvider +from rath.server.authorization import allows, project_allows from rath.server.resources import ResourceStore, default_resource_store __all__ = ["AgentServer", "create_app"] @@ -93,9 +102,7 @@ def _interrupt_json(value: Interrupt) -> dict[str, object]: "request": thaw_json(value.request), "created_at": value.created_at.isoformat(), "expires_at": ( - value.expires_at.isoformat() - if value.expires_at is not None - else None + value.expires_at.isoformat() if value.expires_at is not None else None ), "decision": ( { @@ -108,9 +115,7 @@ def _interrupt_json(value: Interrupt) -> dict[str, object]: else None ), "decided_at": ( - value.decided_at.isoformat() - if value.decided_at is not None - else None + value.decided_at.isoformat() if value.decided_at is not None else None ), } @@ -122,6 +127,443 @@ class _Assistant: revision_id: UUID +def _openapi_document( + package_version: str, *, store_enabled: bool +) -> dict[str, object]: + secured: list[dict[str, list[object]]] = [{"bearerAuth": []}] + error_content = { + "application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}} + } + + def operation( + operation_id: str, + summary: str, + *, + action: str | None = None, + success: str = "200", + response_schema: str | None = None, + request_schema: str | None = None, + parameters: list[dict[str, object]] | None = None, + media_type: str = "application/json", + ) -> dict[str, object]: + success_schema: dict[str, object] = ( + {"$ref": f"#/components/schemas/{response_schema}"} + if response_schema + else {"type": "object"} + ) + value: dict[str, object] = { + "operationId": operation_id, + "summary": summary, + "x-openrath-stability": "beta", + "x-openrath-action": action, + "security": secured if action is not None else [], + "responses": { + success: { + "description": "Success", + "content": {media_type: {"schema": success_schema}}, + }, + "400": {"description": "Invalid request", "content": error_content}, + "401": {"description": "Unauthenticated", "content": error_content}, + "403": {"description": "Forbidden", "content": error_content}, + "404": {"description": "Not found", "content": error_content}, + "409": {"description": "Conflict", "content": error_content}, + "429": {"description": "Resource exhausted", "content": error_content}, + }, + } + if request_schema is not None: + value["requestBody"] = { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": f"#/components/schemas/{request_schema}", + } + } + }, + } + if parameters: + value["parameters"] = parameters + return value + + def path_parameter(name: str) -> dict[str, object]: + schema: dict[str, object] = {"type": "string"} + if name in {"session_id", "run_id", "interrupt_id"}: + schema["format"] = "uuid" + return { + "name": name, + "in": "path", + "required": True, + "schema": schema, + } + + cursor_parameters = [ + { + "name": "after", + "in": "query", + "required": False, + "schema": {"type": "string"}, + }, + { + "name": "limit", + "in": "query", + "required": False, + "schema": {"type": "integer", "minimum": 1, "maximum": 200}, + }, + ] + paths: dict[str, object] = { + "/health/live": { + "get": operation("live", "Process liveness", response_schema="Health") + }, + "/health/ready": { + "get": operation("ready", "Dependency readiness", response_schema="Health") + }, + "/info": { + "get": operation("info", "Server capabilities", response_schema="Info") + }, + "/metrics": { + "get": operation( + "metrics", + "Prometheus metrics", + action="metrics.read", + media_type="text/plain", + ) + }, + "/v1/assistants": { + "get": operation( + "listAssistants", + "List deployment templates and tenant aliases", + action="assistant.read", + response_schema="ItemPage", + ), + "post": operation( + "createAssistantAlias", + "Create a tenant assistant alias", + action="assistant.create", + success="201", + request_schema="CreateAssistantRequest", + response_schema="Assistant", + ), + }, + "/v1/assistants/{assistant_id}": { + "get": operation( + "getAssistant", + "Get an assistant template or alias", + action="assistant.read", + response_schema="Assistant", + parameters=[path_parameter("assistant_id")], + ) + }, + "/v1/sessions": { + "post": operation( + "createSession", + "Create a durable session", + action="session.create", + success="201", + response_schema="Session", + ) + }, + "/v1/sessions/{session_id}": { + "get": operation( + "getSession", + "Get a durable session", + action="session.read", + response_schema="Session", + parameters=[path_parameter("session_id")], + ) + }, + "/v1/sessions/{session_id}/runs": { + "post": operation( + "createSessionRun", + "Create a run in an existing session", + action="run.create", + success="201", + request_schema="CreateSessionRunRequest", + response_schema="Run", + parameters=[path_parameter("session_id")], + ) + }, + "/v1/runs": { + "get": operation( + "listRuns", + "List runs by cursor", + action="run.read", + response_schema="ItemPage", + parameters=cursor_parameters, + ), + "post": operation( + "createRun", + "Create a durable run", + action="run.create", + success="201", + request_schema="CreateRunRequest", + response_schema="Run", + ), + }, + "/v1/runs/{run_id}": { + "get": operation( + "getRun", + "Get a durable run", + action="run.read", + response_schema="Run", + parameters=[path_parameter("run_id")], + ) + }, + "/v1/runs/{run_id}/cancel": { + "post": operation( + "cancelRun", + "Cancel a run", + action="run.cancel", + response_schema="Run", + parameters=[path_parameter("run_id")], + ) + }, + "/v1/runs/{run_id}/resume": { + "post": operation( + "resumeRun", + "Resume a run requiring operator review", + action="run.resume", + request_schema="ResumeRunRequest", + response_schema="Run", + parameters=[path_parameter("run_id")], + ) + }, + "/v1/runs/{run_id}/events": { + "get": operation( + "listRunEvents", + "Replay ordered durable run events", + action="run.read", + response_schema="ItemPage", + parameters=[path_parameter("run_id"), *cursor_parameters], + ) + }, + "/v1/runs/{run_id}/stream": { + "get": operation( + "streamRunEvents", + "Stream ordered run events with cursor resume", + action="run.read", + media_type="text/event-stream", + parameters=[path_parameter("run_id"), *cursor_parameters], + ) + }, + "/v1/interrupts": { + "get": operation( + "listInterrupts", + "List pending durable interrupts", + action="interrupt.read", + response_schema="ItemPage", + parameters=cursor_parameters, + ) + }, + "/v1/interrupts/{interrupt_id}/decision": { + "post": operation( + "decideInterrupt", + "Submit a durable interrupt decision", + action="interrupt.decide", + request_schema="InterruptDecisionRequest", + response_schema="Run", + parameters=[path_parameter("interrupt_id")], + ) + }, + "/v1/feedback": { + "post": operation( + "createFeedback", + "Create run feedback", + action="feedback.create", + success="201", + request_schema="FeedbackRequest", + response_schema="Feedback", + ) + }, + } + if store_enabled: + paths.update( + { + "/v1/store/items": { + "post": operation( + "putStoreItem", + "Write a governed memory item", + action="memory.put", + request_schema="MemoryRequest", + ), + "delete": operation( + "deleteStoreItem", + "Delete a governed memory item", + action="memory.delete", + request_schema="MemoryRequest", + ), + }, + "/v1/store/search": { + "post": operation( + "searchStore", + "Search governed memory", + action="memory.search", + request_schema="MemoryRequest", + ) + }, + } + ) + identifier = {"type": "string", "format": "uuid"} + schemas: dict[str, object] = { + "Health": { + "type": "object", + "required": ["status"], + "properties": {"status": {"type": "string"}}, + }, + "Info": {"type": "object", "additionalProperties": True}, + "ErrorResponse": { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": {"type": "string"}, + "message": {"type": "string"}, + }, + } + }, + }, + "Assistant": { + "type": "object", + "required": ["id", "template_id", "revision_id", "kind"], + "properties": { + "id": {"type": "string"}, + "template_id": {"type": "string"}, + "revision_id": identifier, + "kind": {"type": "string", "enum": ["template", "alias"]}, + }, + }, + "Session": { + "type": "object", + "required": ["id", "tenant_id", "created_at"], + "properties": { + "id": identifier, + "tenant_id": {"type": "string"}, + "created_at": {"type": "string", "format": "date-time"}, + "runs": { + "type": "array", + "items": {"$ref": "#/components/schemas/Run"}, + }, + }, + }, + "Run": { + "type": "object", + "required": [ + "id", + "plan_id", + "revision_id", + "session_id", + "status", + "version", + ], + "properties": { + "id": identifier, + "plan_id": identifier, + "revision_id": identifier, + "session_id": identifier, + "status": { + "type": "string", + "enum": [item.value for item in RunStatus], + }, + "state": {"type": "object"}, + "next_nodes": {"type": "array", "items": {"type": "string"}}, + "version": {"type": "integer", "minimum": 0}, + }, + }, + "Feedback": {"type": "object", "additionalProperties": True}, + "ItemPage": { + "type": "object", + "required": ["items", "next"], + "properties": { + "items": {"type": "array", "items": {}}, + "next": {"type": ["string", "null"]}, + }, + }, + "CreateAssistantRequest": { + "type": "object", + "required": ["id", "template_id"], + "properties": { + "id": {"type": "string"}, + "template_id": {"type": "string"}, + }, + "additionalProperties": False, + }, + "CreateRunRequest": { + "type": "object", + "required": ["assistant_id", "session_id"], + "properties": { + "assistant_id": {"type": "string"}, + "session_id": identifier, + "state": {"type": "object"}, + "priority": {"type": "integer"}, + }, + "additionalProperties": False, + }, + "CreateSessionRunRequest": { + "type": "object", + "required": ["assistant_id"], + "properties": { + "assistant_id": {"type": "string"}, + "state": {"type": "object"}, + "priority": {"type": "integer"}, + }, + "additionalProperties": False, + }, + "ResumeRunRequest": { + "type": "object", + "required": ["confirm"], + "properties": {"confirm": {"const": True}}, + "additionalProperties": False, + }, + "InterruptDecisionRequest": { + "type": "object", + "required": ["kind", "reason"], + "properties": { + "kind": {"type": "string", "enum": ["approve", "edit", "reject"]}, + "reason": {"type": "string"}, + "payload": {"type": "object"}, + }, + "additionalProperties": False, + }, + "FeedbackRequest": { + "type": "object", + "required": ["run_id", "key"], + "properties": { + "run_id": identifier, + "key": {"type": "string"}, + "score": {"type": ["number", "null"], "minimum": -1, "maximum": 1}, + "value": {"type": ["string", "null"]}, + }, + "additionalProperties": False, + }, + "MemoryRequest": { + "type": "object", + "properties": { + "tenant_id": {"type": "string"}, + "user_id": {"type": "string"}, + "agent_id": {"type": "string"}, + "session_id": {"type": "string"}, + "payload": {"type": "object"}, + }, + "additionalProperties": False, + }, + } + return { + "openapi": "3.1.0", + "info": { + "title": "OpenRath Agent Server", + "version": package_version, + "description": "Beta v2 durable Agent Server API.", + }, + "paths": paths, + "components": { + "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}, + "schemas": schemas, + }, + } + + class AgentServer: def __init__( self, @@ -137,9 +579,13 @@ def __init__( max_queued_runs_per_tenant: int = 1000, memory_executor: MemoryExecutor | None = None, memory_handler: MemoryHandler | None = None, + audit_sink: AuditSink | None = None, ) -> None: self.store = store self.runtime = runtime + # Agent Server is the durable service profile. It must reject timeout + # declarations that cannot be enforced before an assistant is exposed. + self.runtime.production_mode = True self.auth = auth self.assistants: dict[str, _Assistant] = {} self.resources = resources or default_resource_store(store) @@ -158,6 +604,7 @@ def __init__( ) self.memory_executor = memory_executor self.memory_handler = memory_handler + self.audit_sink = audit_sink self.app = create_app(self) def register_assistant( @@ -188,6 +635,38 @@ def error_response(code: str, message: str, status: int) -> JSONResponse: {"error": {"code": code, "message": message}}, status_code=status ) + async def audit_action( + request: Request, + context: SecurityContext, + *, + kind: AuditKind, + action: str, + resource_kind: str, + resource_id: str, + ) -> None: + if server.audit_sink is None: + return + request_id = UUID(str(request.state.request_id)) + await server.audit_sink.emit( + AuditEvent( + id=uuid4(), + kind=kind, + occurred_at=datetime.now(timezone.utc), + tenant_id=context.tenant_id, + principal_id=context.principal.id, + request_id=request_id, + trace_id=request_id.hex, + action=action, + resource_kind=resource_kind, + resource_id=resource_id, + outcome="allowed", + reason="endpoint action grant and object scope validated", + attributes={ + "project_id": context.project_id, + }, + ) + ) + async def json_body(request: Request) -> dict[str, object]: maximum = 1024 * 1024 content_length = request.headers.get("content-length") @@ -205,15 +684,40 @@ async def json_body(request: Request) -> dict[str, object]: async def authenticate( request: Request, + action: str | None = None, ) -> tuple[SecurityContext | None, JSONResponse | None]: context = await server.auth.authenticate(request.headers.get("authorization")) if context is None: return None, JSONResponse( - {"error": {"code": "security.unauthenticated", "message": "unauthenticated"}}, + { + "error": { + "code": "security.unauthenticated", + "message": "unauthenticated", + } + }, status_code=401, ) + if action is not None and not allows(context, action): + return None, JSONResponse( + { + "error": { + "code": "security.forbidden", + "message": f"missing grant: {action}", + } + }, + status_code=403, + ) return context, None + def run_visible(context: SecurityContext, run: Run) -> bool: + if run.tenant_id != context.tenant_id: + return False + run_context = thaw_json(run.context) + return not isinstance(run_context, Mapping) or project_allows( + context, + run_context.get("project_id"), + ) + async def live(request: Request) -> Response: return JSONResponse({"status": "ok"}) @@ -244,6 +748,39 @@ async def info(request: Request) -> Response: ) async def openapi(request: Request) -> Response: + return JSONResponse( + _openapi_document( + package_version, + store_enabled=server.memory_handler is not None, + ) + ) + + secured: list[dict[str, list[object]]] = [{"bearerAuth": []}] + + def operation( + operation_id: str, + summary: str, + *, + success: str = "200", + media_type: str = "application/json", + ) -> dict[str, object]: + return { + "operationId": operation_id, + "summary": summary, + "security": secured, + "responses": { + success: { + "description": "Success", + "content": {media_type: {"schema": {"type": "object"}}}, + }, + "400": {"description": "Invalid request"}, + "401": {"description": "Unauthenticated"}, + "403": {"description": "Forbidden"}, + "404": {"description": "Not found"}, + "409": {"description": "Conflict"}, + }, + } + return JSONResponse( { "openapi": "3.1.0", @@ -252,26 +789,129 @@ async def openapi(request: Request) -> Response: "version": package_version, }, "paths": { - "/v1/assistants": {}, - "/v1/sessions": {}, - "/v1/runs": {}, - "/v1/runs/{run_id}/events": {}, - "/v1/runs/{run_id}/stream": {}, - "/v1/interrupts/{interrupt_id}/decision": {}, - "/v1/interrupts": {}, - "/v1/feedback": {}, - "/v1/store/items": {}, - "/v1/store/search": {}, + "/v1/assistants": { + "get": operation( + "listAssistants", + "List deployment templates and tenant aliases", + ), + "post": operation( + "createAssistantAlias", + "Create a tenant assistant alias", + success="201", + ), + }, + "/v1/assistants/{assistant_id}": { + "get": operation( + "getAssistant", + "Get an assistant template or alias", + ) + }, + "/v1/sessions": { + "post": operation( + "createSession", + "Create a durable session", + success="201", + ) + }, + "/v1/sessions/{session_id}": { + "get": operation("getSession", "Get a durable session") + }, + "/v1/sessions/{session_id}/runs": { + "post": operation( + "createSessionRun", + "Create a run in an existing session", + success="201", + ) + }, + "/v1/runs": { + "get": operation("listRuns", "List runs by cursor"), + "post": operation( + "createRun", + "Create a durable run", + success="201", + ), + }, + "/v1/runs/{run_id}": { + "get": operation("getRun", "Get a durable run") + }, + "/v1/runs/{run_id}/cancel": { + "post": operation("cancelRun", "Cancel a run") + }, + "/v1/runs/{run_id}/resume": { + "post": operation( + "resumeRun", + "Resume a run requiring operator review", + ) + }, + "/v1/runs/{run_id}/events": { + "get": operation( + "listRunEvents", + "Replay ordered durable run events", + ) + }, + "/v1/runs/{run_id}/stream": { + "get": operation( + "streamRunEvents", + "Stream ordered run events with cursor resume", + media_type="text/event-stream", + ) + }, + "/v1/interrupts/{interrupt_id}/decision": { + "post": operation( + "decideInterrupt", + "Submit a durable interrupt decision", + ) + }, + "/v1/interrupts": { + "get": operation( + "listInterrupts", + "List pending durable interrupts", + ) + }, + "/v1/feedback": { + "post": operation( + "createFeedback", + "Create run feedback", + success="201", + ) + }, + "/v1/store/items": { + "post": operation( + "putStoreItem", + "Write a governed memory item", + ), + "delete": operation( + "deleteStoreItem", + "Delete a governed memory item", + ), + }, + "/v1/store/search": { + "post": operation( + "searchStore", + "Search governed memory", + ) + }, + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + } + } }, } ) async def metrics(request: Request) -> Response: - queued = running = 0 - for tenant_id in server.resources.count_tenants(): - for run in server.store.list_runs(tenant_id=tenant_id): - queued += int(run.status is RunStatus.QUEUED) - running += int(run.status is RunStatus.RUNNING) + context, error = await authenticate(request, "metrics.read") + if error: + return error + assert context is not None + queued, running = await asyncio.gather( + asyncio.to_thread(server.store.count_runs, status=RunStatus.QUEUED), + asyncio.to_thread(server.store.count_runs, status=RunStatus.RUNNING), + ) body = ( "# TYPE openrath_runs gauge\n" f'openrath_runs{{status="queued"}} {queued}\n' @@ -280,7 +920,7 @@ async def metrics(request: Request) -> Response: return Response(body, media_type="text/plain; version=0.0.4") async def list_assistants(request: Request) -> Response: - context, error = await authenticate(request) + context, error = await authenticate(request, "assistant.read") if error: return error assert context is not None @@ -302,12 +942,10 @@ async def list_assistants(request: Request) -> Response: } for item in server.resources.list_assistants(context.tenant_id) ] - return JSONResponse( - {"items": templates + aliases} - ) + return JSONResponse({"items": templates + aliases}) async def create_assistant(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "assistant.create") if auth_error: return auth_error assert context is not None @@ -320,6 +958,14 @@ async def create_assistant(request: Request) -> Response: if assistant_id in server.assistants: raise ValueError("assistant id is reserved by a deployment template") template = server.assistants[template_id] + await audit_action( + request, + context, + kind=AuditKind.RUN_CONTROL, + action="assistant.create", + resource_kind="assistant", + resource_id=assistant_id, + ) item = server.resources.create_assistant( tenant_id=context.tenant_id, id=assistant_id, @@ -346,7 +992,7 @@ async def create_assistant(request: Request) -> Response: return error_response("request.invalid_argument", str(exc), 400) async def get_assistant(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "assistant.read") if auth_error: return auth_error assert context is not None @@ -363,13 +1009,9 @@ async def get_assistant(request: Request) -> Response: ) except KeyError: try: - alias = server.resources.get_assistant( - context.tenant_id, assistant_id - ) + alias = server.resources.get_assistant(context.tenant_id, assistant_id) except KeyError: - return error_response( - "resource.not_found", "assistant not found", 404 - ) + return error_response("resource.not_found", "assistant not found", 404) return JSONResponse( { "id": alias.id, @@ -381,7 +1023,7 @@ async def get_assistant(request: Request) -> Response: ) async def create_session(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "session.create") if auth_error: return auth_error assert context is not None @@ -396,7 +1038,7 @@ async def create_session(request: Request) -> Response: ) async def get_session(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "session.read") if auth_error: return auth_error assert context is not None @@ -410,8 +1052,11 @@ async def get_session(request: Request) -> Response: return error_response("resource.not_found", "session not found", 404) runs = [ _run_json(run) - for run in server.store.list_runs(tenant_id=context.tenant_id) - if run.session_id == session.id + for run in server.store.list_runs( + tenant_id=context.tenant_id, + session_id=session.id, + limit=201, + ) ] return JSONResponse( { @@ -423,7 +1068,7 @@ async def get_session(request: Request) -> Response: ) async def create_run(request: Request) -> Response: - context, error = await authenticate(request) + context, error = await authenticate(request, "run.create") if error: return error assert context is not None @@ -432,23 +1077,16 @@ async def create_run(request: Request) -> Response: assistant_id = str(body["assistant_id"]) assistant = server.assistants.get(assistant_id) if assistant is None: - alias = server.resources.get_assistant( - context.tenant_id, assistant_id - ) + alias = server.resources.get_assistant(context.tenant_id, assistant_id) assistant = server.assistants.get(alias.template_id) if assistant is None or assistant.revision_id != alias.revision_id: - raise ValueError( - "assistant deployment revision is unavailable" - ) + raise ValueError("assistant deployment revision is unavailable") session_id = UUID(str(body["session_id"])) try: known_session = server.resources.get_session(session_id) except KeyError: - known_session = None - if ( - known_session is not None - and known_session.tenant_id != context.tenant_id - ): + raise KeyError("session_id") from None + if known_session.tenant_id != context.tenant_id: raise KeyError("session_id") run_context = RunContext( security=context, @@ -457,9 +1095,12 @@ async def create_run(request: Request) -> Response: state = body.get("state") or {} if not isinstance(state, Mapping): raise ValueError("state must be a JSON object") - queued = sum( - run.status is RunStatus.QUEUED - for run in server.store.list_runs(tenant_id=context.tenant_id) + queued = len( + server.store.list_runs( + tenant_id=context.tenant_id, + statuses=(RunStatus.QUEUED,), + limit=server.max_queued_runs_per_tenant, + ) ) if queued >= server.max_queued_runs_per_tenant: return error_response( @@ -475,6 +1116,15 @@ async def create_run(request: Request) -> Response: idempotency_key=request.headers.get("idempotency-key"), priority=int(str(body.get("priority", 0))), ) + if server.signals is not None: + server.signals.publish( + RunSignal( + kind=SignalKind.WAKE, + run_id=run.id, + tenant_id=run.tenant_id, + created_at=run.updated_at, + ) + ) return JSONResponse(_run_json(run), status_code=201) except KeyError as exc: return JSONResponse( @@ -502,7 +1152,7 @@ async def create_session_run(request: Request) -> Response: return await create_run(request) async def list_runs(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "run.read") if auth_error: return auth_error assert context is not None @@ -514,15 +1164,17 @@ async def list_runs(request: Request) -> Response: return error_response( "request.invalid_argument", "invalid pagination cursor", 400 ) - values = list(server.store.list_runs(tenant_id=context.tenant_id)) - if after_id is not None: - try: - offset = next(i for i, item in enumerate(values) if item.id == after_id) + 1 - except StopIteration: - return error_response( - "request.invalid_argument", "unknown pagination cursor", 400 - ) - values = values[offset:] + try: + values = await asyncio.to_thread( + server.store.list_runs, + tenant_id=context.tenant_id, + after=after_id, + limit=limit + 1, + ) + except KeyError: + return error_response( + "request.invalid_argument", "unknown pagination cursor", 400 + ) page = values[:limit] next_cursor = str(page[-1].id) if len(values) > limit else None return JSONResponse( @@ -530,7 +1182,7 @@ async def list_runs(request: Request) -> Response: ) async def get_run(request: Request) -> Response: - context, error = await authenticate(request) + context, error = await authenticate(request, "run.read") if error: return error assert context is not None @@ -541,7 +1193,7 @@ async def get_run(request: Request) -> Response: {"error": {"code": "resource.not_found", "message": "run not found"}}, status_code=404, ) - if run.tenant_id != context.tenant_id: + if not run_visible(context, run): return JSONResponse( {"error": {"code": "resource.not_found", "message": "run not found"}}, status_code=404, @@ -549,14 +1201,22 @@ async def get_run(request: Request) -> Response: return JSONResponse(_run_json(run)) async def cancel_run(request: Request) -> Response: - context, error = await authenticate(request) + context, error = await authenticate(request, "run.cancel") if error: return error assert context is not None try: run = server.store.get_run(UUID(request.path_params["run_id"])) - if run.tenant_id != context.tenant_id: + if not run_visible(context, run): raise KeyError + await audit_action( + request, + context, + kind=AuditKind.RUN_CONTROL, + action="run.cancel", + resource_kind="run", + resource_id=str(run.id), + ) cancelled = server.store.transition_run( run.id, expected_version=run.version, @@ -581,21 +1241,32 @@ async def cancel_run(request: Request) -> Response: return JSONResponse({"error": exc.to_dict()}, status_code=409) async def resume_run(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "run.resume") if auth_error: return auth_error assert context is not None try: run = server.store.get_run(UUID(request.path_params["run_id"])) - if run.tenant_id != context.tenant_id: + if not run_visible(context, run): raise KeyError body = await json_body(request) - if run.status is not RunStatus.NEEDS_REVIEW or body.get("confirm") is not True: + if ( + run.status is not RunStatus.NEEDS_REVIEW + or body.get("confirm") is not True + ): return error_response( "request.invalid_argument", "resume requires NEEDS_REVIEW status and confirm=true", 400, ) + await audit_action( + request, + context, + kind=AuditKind.OPERATOR_OVERRIDE, + action="run.resume", + resource_kind="run", + resource_id=str(run.id), + ) resumed = server.store.transition_run( run.id, expected_version=run.version, @@ -608,14 +1279,14 @@ async def resume_run(request: Request) -> Response: return JSONResponse({"error": exc.to_dict()}, status_code=409) async def events(request: Request) -> Response: - context, error = await authenticate(request) + context, error = await authenticate(request, "run.read") if error: return error assert context is not None try: run_id = UUID(request.path_params["run_id"]) run = server.store.get_run(run_id) - if run.tenant_id != context.tenant_id: + if not run_visible(context, run): raise KeyError except (KeyError, ValueError): return JSONResponse( @@ -623,7 +1294,12 @@ async def events(request: Request) -> Response: status_code=404, ) try: - after = int(request.query_params.get("after", "0")) + cursor_value = request.query_params.get("after") + if cursor_value is None: + cursor_value = request.headers.get("last-event-id", "0") + after = int(cursor_value) + if after < 0: + raise ValueError limit = min(max(int(request.query_params.get("limit", "200")), 1), 1000) except ValueError: return error_response( @@ -638,9 +1314,13 @@ async def events(request: Request) -> Response: "time": event.created_at.isoformat(), "data": thaw_json(event.data), } - for event in server.store.list_run_events(run_id) - if event.sequence > after - ][:limit] + for event in await asyncio.to_thread( + server.store.list_run_events, + run_id, + after_sequence=after, + limit=limit, + ) + ] return JSONResponse( { "items": items, @@ -654,20 +1334,25 @@ async def stream(request: Request) -> Response: if response.status_code != 200: return response payload = json.loads(bytes(response.body)) - if last_event and "after" not in request.query_params: - payload["items"] = [ - item - for item in payload["items"] - if int(item["sequence"]) > int(last_event) - ] follow = request.query_params.get("follow", "false").lower() == "true" run_id = UUID(request.path_params["run_id"]) async def generate() -> AsyncIterator[str]: cursor = int(last_event or request.query_params.get("after", "0")) initial = payload["items"] + idle_delay = 0.1 while True: emitted = False + durable_events = ( + () + if initial + else await asyncio.to_thread( + server.store.list_run_events, + run_id, + after_sequence=cursor, + limit=200, + ) + ) items = initial or [ { "sequence": event.sequence, @@ -676,12 +1361,12 @@ async def generate() -> AsyncIterator[str]: "time": event.created_at.isoformat(), "data": thaw_json(event.data), } - for event in server.store.list_run_events(run_id) - if event.sequence > cursor + for event in durable_events ] initial = [] for item in items: emitted = True + idle_delay = 0.1 cursor = int(item["sequence"]) yield ( f"id: {cursor}\nevent: {item['type']}\n" @@ -689,17 +1374,24 @@ async def generate() -> AsyncIterator[str]: ) if not follow: break - run = server.store.get_run(run_id) - if run.status in { - RunStatus.SUCCEEDED, - RunStatus.FAILED, - RunStatus.CANCELLED, - RunStatus.TIMED_OUT, - } and not emitted: + if await request.is_disconnected(): + break + run = await asyncio.to_thread(server.store.get_run, run_id) + if ( + run.status + in { + RunStatus.SUCCEEDED, + RunStatus.FAILED, + RunStatus.CANCELLED, + RunStatus.TIMED_OUT, + } + and not emitted + ): break if not emitted: yield ": keep-alive\n\n" - await asyncio.sleep(0.25) + idle_delay = min(idle_delay * 2, 2.0) + await asyncio.sleep(idle_delay) return StreamingResponse( generate(), @@ -711,7 +1403,7 @@ async def generate() -> AsyncIterator[str]: ) async def decide_interrupt(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "interrupt.decide") if auth_error: return auth_error assert context is not None @@ -719,7 +1411,7 @@ async def decide_interrupt(request: Request) -> Response: interrupt_id = UUID(request.path_params["interrupt_id"]) interrupt = server.store.get_interrupt(interrupt_id) run = server.store.get_run(interrupt.run_id) - if run.tenant_id != context.tenant_id: + if not run_visible(context, run): raise KeyError body = await json_body(request) payload = body.get("payload") or {} @@ -731,6 +1423,14 @@ async def decide_interrupt(request: Request) -> Response: reason=str(body["reason"]), payload=cast(Mapping[str, JSONValue], payload), ) + await audit_action( + request, + context, + kind=AuditKind.OPERATOR_OVERRIDE, + action="interrupt.decide", + resource_kind="interrupt", + resource_id=str(interrupt_id), + ) updated = server.store.decide_interrupt( interrupt_id, decision=decision, @@ -743,7 +1443,7 @@ async def decide_interrupt(request: Request) -> Response: return JSONResponse({"error": exc.to_dict()}, status_code=409) async def list_interrupts(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "interrupt.read") if auth_error: return auth_error assert context is not None @@ -763,21 +1463,19 @@ async def list_interrupts(request: Request) -> Response: return JSONResponse( { "items": [_interrupt_json(item) for item in values[:limit]], - "next": ( - str(values[limit - 1].id) if len(values) > limit else None - ), + "next": (str(values[limit - 1].id) if len(values) > limit else None), } ) async def create_feedback(request: Request) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, "feedback.create") if auth_error: return auth_error assert context is not None try: body = await json_body(request) run = server.store.get_run(UUID(str(body["run_id"]))) - if run.tenant_id != context.tenant_id: + if not run_visible(context, run): raise KeyError score = body.get("score") numeric_score = float(str(score)) if score is not None else None @@ -811,7 +1509,7 @@ async def execute_store_operation( request: Request, operation: str, ) -> Response: - context, auth_error = await authenticate(request) + context, auth_error = await authenticate(request, f"memory.{operation}") if auth_error: return auth_error assert context is not None @@ -829,15 +1527,20 @@ async def execute_store_operation( and str(requested_tenant) != context.tenant_id ): raise PermissionError("memory namespace tenant mismatch") + requested_user = ( + str(body["user_id"]) if body.get("user_id") is not None else None + ) + if ( + requested_user is not None + and requested_user != context.principal.id + and not allows(context, "memory.admin") + ): + raise PermissionError("memory namespace owner mismatch") namespace = MemoryNamespace( tenant_id=context.tenant_id, - user_id=( - str(body["user_id"]) if body.get("user_id") is not None else None - ), + user_id=requested_user, agent_id=( - str(body["agent_id"]) - if body.get("agent_id") is not None - else None + str(body["agent_id"]) if body.get("agent_id") is not None else None ), session_id=( str(body["session_id"]) @@ -863,6 +1566,23 @@ async def execute_store_operation( idempotency_key=request.headers.get("idempotency-key"), policy_constraints=PolicyConstraints(), ) + await audit_action( + request, + context, + kind=AuditKind.MEMORY_ACCESS, + action=f"memory.{operation}", + resource_kind="memory_namespace", + resource_id=":".join( + item + for item in ( + namespace.tenant_id, + namespace.user_id, + namespace.agent_id, + namespace.session_id, + ) + if item is not None + ), + ) result = await server.memory_executor.execute( server.memory_handler, cast(Any, operation), @@ -901,13 +1621,22 @@ async def delete_store_item(request: Request) -> Response: async def worker_loop() -> None: while True: await asyncio.to_thread(server.store.expire_interrupts) + await asyncio.to_thread(server.runtime.reconcile_effects) await asyncio.to_thread(server.store.requeue_expired_leases) result = await asyncio.to_thread( server.runtime.work_once, worker_id=server.worker_id, lease_seconds=server.worker_lease_seconds, ) - await asyncio.sleep(0 if result is not None else 0.1) + if result is not None: + await asyncio.sleep(0) + elif server.signals is not None: + await asyncio.to_thread( + server.signals.receive, + timeout_seconds=0.1, + ) + else: + await asyncio.sleep(0.1) @asynccontextmanager async def lifespan(app: Starlette) -> AsyncIterator[None]: diff --git a/src/rath/server/auth.py b/src/rath/server/auth.py index 0ccc042..4c5d48e 100644 --- a/src/rath/server/auth.py +++ b/src/rath/server/auth.py @@ -13,7 +13,9 @@ @runtime_checkable class AuthProvider(Protocol): - async def authenticate(self, authorization: str | None) -> SecurityContext | None: ... + async def authenticate( + self, authorization: str | None + ) -> SecurityContext | None: ... class StaticTokenAuth: diff --git a/src/rath/server/authorization.py b/src/rath/server/authorization.py new file mode 100644 index 0000000..b713a22 --- /dev/null +++ b/src/rath/server/authorization.py @@ -0,0 +1,34 @@ +"""Fail-closed action authorization for the Agent Server.""" + +from __future__ import annotations + +from rath.security import SecurityContext + +__all__ = ["allows", "project_allows"] + + +def allows(context: SecurityContext, action: str) -> bool: + """Return whether an explicit grant permits an action. + + Grants support an exact action, a namespace wildcard such as ``run.*``, + or the explicit administrative ``*`` grant. + """ + + if "*" in context.grants or action in context.grants: + return True + namespace, _, _ = action.partition(".") + return f"{namespace}.*" in context.grants + + +def project_allows( + context: SecurityContext, + resource_project_id: object | None, +) -> bool: + """Enforce project isolation when either side declares project scope.""" + + if resource_project_id is None: + return True + return ( + context.project_id is not None + and str(resource_project_id) == context.project_id + ) diff --git a/src/rath/server/cli.py b/src/rath/server/cli.py index bd20c7d..fef3dcd 100644 --- a/src/rath/server/cli.py +++ b/src/rath/server/cli.py @@ -76,21 +76,13 @@ def migrate_main() -> None: if not arguments.dsn: parser.error("--dsn or OPENRATH_POSTGRES_DSN is required") if arguments.check: - import psycopg - from psycopg import sql - - with psycopg.connect(arguments.dsn) as connection: - value = connection.execute( - sql.SQL( - "SELECT version FROM {}.schema_migrations ORDER BY version DESC LIMIT 1" - ).format(sql.Identifier(arguments.schema)) - ).fetchone() - if value is None or int(value[0]) < 1: - raise SystemExit("OpenRath schema is not current") + from rath.runtime import PostgresRunStore + + PostgresRunStore.verify_schema(arguments.dsn, schema=arguments.schema) return from rath.runtime import PostgresRunStore - PostgresRunStore(arguments.dsn, schema=arguments.schema).close() + PostgresRunStore.migrate(arguments.dsn, schema=arguments.schema) def worker_main() -> None: @@ -125,9 +117,15 @@ def stop(signum: int, frame: object) -> None: signal.signal(getattr(signal, name), stop) while not stopped.is_set(): server.store.expire_interrupts() + server.runtime.reconcile_effects() server.store.requeue_expired_leases() result = server.runtime.work_once( worker_id=arguments.worker_id, lease_seconds=arguments.lease_seconds, ) - stopped.wait(0 if result is not None else 0.1) + if result is not None: + continue + if getattr(server, "signals", None) is not None: + server.signals.receive(timeout_seconds=0.1) + else: + stopped.wait(0.1) diff --git a/src/rath/server/resources.py b/src/rath/server/resources.py index f382999..debf045 100644 --- a/src/rath/server/resources.py +++ b/src/rath/server/resources.py @@ -102,10 +102,7 @@ def create_assistant( id, tenant_id, template_id, revision_id, datetime.now(timezone.utc) ) existing = self.assistants.setdefault(key, item) - if ( - existing.template_id != template_id - or existing.revision_id != revision_id - ): + if existing.template_id != template_id or existing.revision_id != revision_id: raise ValueError("assistant id already has a different revision") return existing @@ -203,10 +200,7 @@ def create_assistant( ), ) existing = self.get_assistant(tenant_id, id) - if ( - existing.template_id != template_id - or existing.revision_id != revision_id - ): + if existing.template_id != template_id or existing.revision_id != revision_id: raise ValueError("assistant id already has a different revision") return existing @@ -361,10 +355,7 @@ def create_assistant( (tenant_id, id, template_id, revision_id, item.created_at), ) existing = self.get_assistant(tenant_id, id) - if ( - existing.template_id != template_id - or existing.revision_id != revision_id - ): + if existing.template_id != template_id or existing.revision_id != revision_id: raise ValueError("assistant id already has a different revision") return existing diff --git a/tests/artifacts/test_artifact_store.py b/tests/artifacts/test_artifact_store.py index 3f530f2..ded914e 100644 --- a/tests/artifacts/test_artifact_store.py +++ b/tests/artifacts/test_artifact_store.py @@ -5,7 +5,7 @@ import pytest -from rath.artifacts import ArtifactNotFound, LocalArtifactStore +from rath.artifacts import ArtifactNotFound, LocalArtifactStore, S3ArtifactStore def test_local_artifact_is_content_addressed_and_tenant_scoped( @@ -35,15 +35,62 @@ def test_local_artifact_enforces_size_identity_and_deletion(tmp_path: Path) -> N store.put("../tenant", b"x") artifact = store.put("tenant", b"one") - payload = ( - tmp_path - / "artifacts" - / "tenant" - / artifact.digest[:2] - / artifact.digest - ) + payload = tmp_path / "artifacts" / "tenant" / artifact.digest[:2] / artifact.digest payload.write_bytes(b"corrupt") with pytest.raises(OSError, match="verification"): store.get("tenant", artifact.digest) assert store.delete("tenant", artifact.digest) assert not store.delete("tenant", artifact.digest) + + +def test_s3_artifact_stops_reading_after_configured_limit() -> None: + class _Client: + def put_object(self, **kwargs): # type: ignore[no-untyped-def] + raise AssertionError("oversized content must not be uploaded") + + class _Stream: + def __init__(self) -> None: + self.reads = 0 + + def read(self, size: int) -> bytes: + self.reads += 1 + return b"x" * size + + stream = _Stream() + store = S3ArtifactStore( + "bucket", + client=_Client(), + max_bytes=3, + ) + + with pytest.raises(ValueError, match="size"): + store.put("tenant", stream) + assert stream.reads == 1 + + +def test_s3_artifact_streams_staged_payload_and_cleans_orphan() -> None: + class _Client: + def __init__(self) -> None: + self.calls = 0 + self.deleted: list[str] = [] + + def put_object(self, **kwargs): # type: ignore[no-untyped-def] + self.calls += 1 + if self.calls == 1: + body = kwargs["Body"] + assert not isinstance(body, bytes) + assert body.read() == b"streamed" + return {} + raise RuntimeError("manifest failed") + + def delete_objects(self, **kwargs): # type: ignore[no-untyped-def] + self.deleted.extend(item["Key"] for item in kwargs["Delete"]["Objects"]) + + client = _Client() + store = S3ArtifactStore("bucket", client=client) + + with pytest.raises(RuntimeError, match="manifest"): + store.put("tenant", io.BytesIO(b"streamed")) + + assert len(client.deleted) == 1 + assert not client.deleted[0].endswith(".json") diff --git a/tests/backends/test_local.py b/tests/backends/test_local.py index 14feb17..9702de7 100644 --- a/tests/backends/test_local.py +++ b/tests/backends/test_local.py @@ -105,7 +105,9 @@ def test_close_does_not_remove_user_supplied_working_dir(tmp_path: object) -> No assert sentinel.read_text(encoding="utf-8") == "do not delete me" -def test_filesystem_calls_reject_absolute_paths_outside_workspace(tmp_path: object) -> None: +def test_filesystem_calls_reject_absolute_paths_outside_workspace( + tmp_path: object, +) -> None: import pathlib root = pathlib.Path(str(tmp_path)) / "workspace" # type: ignore[arg-type] diff --git a/tests/backends/test_opensandbox_async.py b/tests/backends/test_opensandbox_async.py index 0992878..c70eef3 100644 --- a/tests/backends/test_opensandbox_async.py +++ b/tests/backends/test_opensandbox_async.py @@ -29,7 +29,7 @@ from rath.backend.opensandbox import OpenSandboxBackend from tests.conftest import opensandbox_real -pytestmark = opensandbox_real +pytestmark = [opensandbox_real, pytest.mark.opensandbox] @pytest.fixture @@ -70,10 +70,13 @@ def write_one(name: str) -> int: assert all(r > 0 for r in results) # If they serialised, we'd expect ~n × per_call. Parallel should beat # serial by at least 2×. Generous to avoid CI flake against a real server. - assert elapsed < per_call * n * 0.7, ( - f"distinct-path writes did not run in parallel: " - f"per-call ≈ {per_call:.2f}s, {n} parallel took {elapsed:.2f}s" - ) + # Below 10 ms the fixed thread-pool and transport setup costs dominate the + # measured operation, so the ratio is not useful evidence of serialization. + if per_call > 0.01: + assert elapsed < per_call * n * 0.7, ( + f"distinct-path writes did not run in parallel: " + f"per-call ≈ {per_call:.2f}s, {n} parallel took {elapsed:.2f}s" + ) for name, want in payloads.items(): r = sb.dispatch(BackendToolFilesRead(path=name, encoding=None)) diff --git a/tests/conformance/v2/test_adapter_contracts.py b/tests/conformance/v2/test_adapter_contracts.py index 65731f5..096f399 100644 --- a/tests/conformance/v2/test_adapter_contracts.py +++ b/tests/conformance/v2/test_adapter_contracts.py @@ -7,6 +7,7 @@ from rath.adapters import ( AdapterRequestContext, + ApprovalGrant, MemoryExecutor, MemoryNamespace, ProviderCapability, @@ -23,7 +24,14 @@ from rath.artifacts import LocalArtifactStore from rath.context import RunContext from rath.definition import EffectClass -from rath.security import LocalTrustedPolicy, PolicyConstraints, TrustLevel +from rath.runtime import arguments_digest +from rath.security import ( + LocalTrustedPolicy, + PolicyConstraints, + PolicyDecision, + PolicyEffect, + TrustLevel, +) def _contexts(): # type: ignore[no-untyped-def] @@ -121,6 +129,112 @@ def test_large_output_can_be_externalized_to_artifact_store(tmp_path) -> None: assert result["size"] > 32 +def test_tool_rejects_adapter_tenant_mismatch() -> None: + run, adapter = _contexts() + mismatched = AdapterRequestContext( + run_id=adapter.run_id, + node_id=adapter.node_id, + tenant_id="other-tenant", + deadline=adapter.deadline, + trace_context=adapter.trace_context, + idempotency_key=adapter.idempotency_key, + policy_constraints=adapter.policy_constraints, + ) + spec = ToolSpec( + name="lookup", + version="1", + input_schema={"type": "object"}, + effects=EffectClass.READ_ONLY, + risk="low", + ) + + with pytest.raises(PermissionError, match="tenant mismatch"): + asyncio.run( + ToolExecutor(LocalTrustedPolicy()).execute( + spec, + lambda arguments, context: {}, + {}, + adapter_context=mismatched, + run_context=run, + ) + ) + + +def test_tool_enforces_declared_async_timeout() -> None: + run, adapter = _contexts() + spec = ToolSpec( + name="slow", + version="1", + input_schema={"type": "object"}, + effects=EffectClass.READ_ONLY, + risk="low", + timeout_seconds=0.01, + ) + + async def slow(arguments, context): # type: ignore[no-untyped-def] + await asyncio.sleep(1) + return {} + + with pytest.raises(asyncio.TimeoutError): + asyncio.run( + ToolExecutor(LocalTrustedPolicy()).execute( + spec, + slow, + {}, + adapter_context=adapter, + run_context=run, + ) + ) + + +def test_tool_requires_a_verified_durable_approval_grant() -> None: + run, adapter = _contexts() + spec = ToolSpec( + name="charge", + version="1", + input_schema={"type": "object"}, + effects=EffectClass.NON_IDEMPOTENT, + risk="high", + ) + arguments = {"amount": 10} + grant = ApprovalGrant( + decision_id=uuid4(), + run_id=adapter.run_id, + node_id=adapter.node_id, + tenant_id=adapter.tenant_id, + tool_id="charge@1", + arguments_digest=arguments_digest(arguments), + actor_id="reviewer", + ) + + with pytest.raises(PermissionError, match="cannot be verified"): + asyncio.run( + ToolExecutor(LocalTrustedPolicy()).execute( + spec, + lambda values, context: {"charged": True}, + arguments, + adapter_context=adapter, + run_context=run, + approval=grant, + ) + ) + + result = asyncio.run( + ToolExecutor( + LocalTrustedPolicy(), + approval_validator=lambda value: value.decision_id == grant.decision_id, + ).execute( + spec, + lambda values, context: {"charged": True}, + arguments, + adapter_context=adapter, + run_context=run, + approval=grant, + ) + ) + assert result == {"charged": True} + + def test_provider_sandbox_and_memory_share_context_policy_timeout_contract() -> None: async def exercise() -> None: run, adapter = _contexts() @@ -164,3 +278,50 @@ async def exercise() -> None: assert memory_result == {"items": []} asyncio.run(exercise()) + + +def test_evaluated_policy_constraints_reach_adapter_handler() -> None: + class _ConstrainedPolicy: + async def evaluate(self, action, resource, context): # type: ignore[no-untyped-def] + return PolicyDecision( + effect=PolicyEffect.ALLOW_WITH_CONSTRAINTS, + reason="bounded", + policy_id="test", + constraints=PolicyConstraints( + timeout_seconds=1, + max_output_bytes=8, + read_only=True, + redactions=frozenset({"secret"}), + ), + ) + + seen: PolicyConstraints | None = None + + def handler(arguments, context): # type: ignore[no-untyped-def] + nonlocal seen + seen = context.policy_constraints + return {} + + run, adapter = _contexts() + asyncio.run( + ToolExecutor(_ConstrainedPolicy()).execute( + ToolSpec( + name="bounded", + version="1", + input_schema={"type": "object"}, + effects=EffectClass.READ_ONLY, + risk="low", + timeout_seconds=10, + ), + handler, + {}, + adapter_context=adapter, + run_context=run, + ) + ) + + assert seen is not None + assert seen.timeout_seconds == 1 + assert seen.max_output_bytes == 8 + assert seen.read_only is True + assert seen.redactions == frozenset({"secret"}) diff --git a/tests/core/test_events.py b/tests/core/test_events.py index 51b37d5..2780325 100644 --- a/tests/core/test_events.py +++ b/tests/core/test_events.py @@ -66,4 +66,3 @@ def test_append_assigns_next_sequence_without_mutating_original() -> None: assert log.events == () assert updated.events[0].sequence == 1 assert isinstance(updated.events[0].created_at, datetime) - diff --git a/tests/definition/test_compiler_v2.py b/tests/definition/test_compiler_v2.py index 8d6dd78..3f8cef4 100644 --- a/tests/definition/test_compiler_v2.py +++ b/tests/definition/test_compiler_v2.py @@ -124,3 +124,22 @@ def test_step_metadata_rejects_unsafe_retry_contract() -> None: def unsafe(state, context): # type: ignore[no-untyped-def] return state + +def test_production_durable_rejects_sync_step_timeout() -> None: + class _TimedSync(Workflow): + @step(entry=True, timeout_seconds=0.1) + def run(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + with pytest.raises( + DefinitionError, + match="synchronous durable steps cannot guarantee preemptive timeout", + ): + WorkflowCompiler().compile( + _TimedSync(), + revision_id=uuid4(), + production_durable=True, + ) diff --git a/tests/definition/test_definition_identity_regression.py b/tests/definition/test_definition_identity_regression.py new file mode 100644 index 0000000..b3fa106 --- /dev/null +++ b/tests/definition/test_definition_identity_regression.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from uuid import uuid4 + +from rath.definition import EffectClass, WorkflowCompiler, step +from rath.flow import Workflow +from rath.session import Session + + +def test_handler_implementation_change_changes_definition_identity() -> None: + class _ImplementationIdentity(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def execute(self, state, context): # type: ignore[no-untyped-def] + return {"value": 1} + + def forward(self, session: Session) -> Session: + return session + + revision_id = uuid4() + compiler = WorkflowCompiler() + first = compiler.compile(_ImplementationIdentity(), revision_id=revision_id) + + def replacement(self, state, context): # type: ignore[no-untyped-def] + return {"value": 999} + + replacement.__name__ = "execute" + setattr( + _ImplementationIdentity, + "execute", + step(entry=True, effects=EffectClass.READ_ONLY)(replacement), + ) + second = compiler.compile( + _ImplementationIdentity(), + revision_id=revision_id, + ) + + assert first.definition_hash != second.definition_hash + assert first.id != second.id diff --git a/tests/definition/test_plan_serialization.py b/tests/definition/test_plan_serialization.py index c85323f..20b548a 100644 --- a/tests/definition/test_plan_serialization.py +++ b/tests/definition/test_plan_serialization.py @@ -41,4 +41,3 @@ def test_existing_compiled_workflow_exposes_v2_execution_plan() -> None: assert compiled.execution_plan.definition_hash assert compiled.execution_plan.durable is True - diff --git a/tests/deployment/test_reference_manifests.py b/tests/deployment/test_reference_manifests.py new file mode 100644 index 0000000..0c4b51a --- /dev/null +++ b/tests/deployment/test_reference_manifests.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import re +from pathlib import Path + + +def test_external_reference_images_are_digest_pinned() -> None: + compose = Path("deploy/compose/compose.yaml").read_text(encoding="utf-8") + for image in re.findall(r"^\s+image:\s+(\S+)", compose, flags=re.MULTILINE): + if image.startswith("openrath:"): + continue + assert "@sha256:" in image, image + + dockerfile = Path("docker/Dockerfile").read_text(encoding="utf-8") + for image in re.findall(r"^FROM\s+(\S+)", dockerfile, flags=re.MULTILINE): + assert "@sha256:" in image, image + + +def test_kubernetes_template_covers_workloads_and_dns() -> None: + manifest = Path("deploy/kubernetes/openrath.yaml").read_text(encoding="utf-8") + assert 'values: ["openrath", "openrath-worker", "openrath-migrate"]' in manifest + assert "protocol: UDP\n port: 53" in manifest + assert "protocol: TCP\n port: 53" in manifest + assert manifest.count("Release automation must replace") == 3 + assert "imagePullPolicy: Always" in manifest + + +def test_production_workflow_pins_actions_and_service_images() -> None: + workflow = Path(".github/workflows/ci-v2-production.yml").read_text( + encoding="utf-8" + ) + assert "actions/checkout@11d5960a326750d5838078e36cf38b85af677262" in workflow + assert "postgres:17-alpine@sha256:" in workflow + assert "redis:8-alpine@sha256:" in workflow + assert "minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:" in workflow + assert "pip-audit" in workflow + assert "scanners: secret" in workflow diff --git a/tests/deployment/test_revisions.py b/tests/deployment/test_revisions.py index e9af25e..72609ac 100644 --- a/tests/deployment/test_revisions.py +++ b/tests/deployment/test_revisions.py @@ -22,5 +22,28 @@ def test_revision_identity_is_deterministic_and_persistent(tmp_path: Path) -> No store.put(first) assert first.id == second.id + assert first.content_digest == second.content_digest + assert len(first.content_digest) == 64 assert store.get(first.id) == first assert store.put(second).id == first.id + + +def test_manifest_change_changes_revision_content_identity() -> None: + first_manifest = DeploymentManifest( + image_digest="a" * 64, + plan_hash="b" * 64, + python_version="3.12", + dependencies_digest="c" * 64, + ) + second_manifest = DeploymentManifest( + image_digest="a" * 64, + plan_hash="e" * 64, + python_version="3.12", + dependencies_digest="c" * 64, + ) + + first = Revision.create(code_digest="d" * 64, manifest=first_manifest) + second = Revision.create(code_digest="d" * 64, manifest=second_manifest) + + assert first.id != second.id + assert first.content_digest != second.content_digest diff --git a/tests/eval/test_runner.py b/tests/eval/test_runner.py index cd0da6f..3df2c57 100644 --- a/tests/eval/test_runner.py +++ b/tests/eval/test_runner.py @@ -23,14 +23,9 @@ def _experiment(score: float) -> Experiment: def test_regression_gate_blocks_absolute_and_relative_regression() -> None: baseline = _experiment(0.9) - assert ( - regression_gate(_experiment(0.89), baseline=baseline) is GateDecision.PASS - ) - assert ( - regression_gate(_experiment(0.85), baseline=baseline) is GateDecision.FAIL - ) + assert regression_gate(_experiment(0.89), baseline=baseline) is GateDecision.PASS + assert regression_gate(_experiment(0.85), baseline=baseline) is GateDecision.FAIL assert ( regression_gate(_experiment(0.79), baseline=_experiment(0.7)) is GateDecision.FAIL ) - diff --git a/tests/integration/test_postgres_run_store.py b/tests/integration/test_postgres_run_store.py index dc96570..593cebc 100644 --- a/tests/integration/test_postgres_run_store.py +++ b/tests/integration/test_postgres_run_store.py @@ -52,6 +52,7 @@ def _run(*, key: str | None = None) -> Run: def store() -> PostgresRunStore: dsn = os.environ["OPENRATH_TEST_POSTGRES_DSN"] schema = f"test_{uuid4().hex}" + PostgresRunStore.migrate(dsn, schema=schema) value = PostgresRunStore(dsn, schema=schema) yield value value.close() @@ -87,9 +88,7 @@ def test_postgres_lifecycle_and_interrupt(store: PostgresRunStore) -> None: kind=InterruptKind.APPROVAL, request={"operation": "email.send"}, ) - waiting = store.create_interrupt( - interrupt, expected_run_version=running.version - ) + waiting = store.create_interrupt(interrupt, expected_run_version=running.version) assert store.list_interrupts(tenant_id="postgres-test") == (interrupt,) resumed = store.decide_interrupt( interrupt.id, @@ -125,9 +124,9 @@ def test_postgres_interrupt_deadline_is_atomic(store: PostgresRunStore) -> None: store.create_interrupt(interrupt, expected_run_version=running.version) assert interrupt.expires_at is not None - assert store.expire_interrupts( - now=interrupt.expires_at + timedelta(seconds=1) - ) == (interrupt.id,) + assert store.expire_interrupts(now=interrupt.expires_at + timedelta(seconds=1)) == ( + interrupt.id, + ) assert store.get_run(running.id).status is RunStatus.TIMED_OUT @@ -175,9 +174,7 @@ def test_postgres_checkpoint_fencing_and_orphan_recovery( ) future = datetime.now(timezone.utc) + timedelta(seconds=2) assert store.requeue_expired_leases(now=future) == (queued.id,) - second = store.claim_next( - worker_id="worker-2", lease_seconds=30, now=future - ) + second = store.claim_next(worker_id="worker-2", lease_seconds=30, now=future) assert second is not None assert second.lease.fencing_token == 2 with pytest.raises(ConflictError, match="fencing"): @@ -194,9 +191,7 @@ def test_postgres_effect_ledger_persists_ambiguous_dispatch( store: PostgresRunStore, ) -> None: run = store.create_run(_run()) - running = store.transition_run( - run.id, expected_version=0, target=RunStatus.RUNNING - ) + running = store.transition_run(run.id, expected_version=0, target=RunStatus.RUNNING) ledger = PostgresEffectLedger(store.dsn, schema=store.schema) invocation = ledger.prepare( run_id=running.id, diff --git a/tests/integration/test_v1_migration_postgres.py b/tests/integration/test_v1_migration_postgres.py index 2bd1d92..365b219 100644 --- a/tests/integration/test_v1_migration_postgres.py +++ b/tests/integration/test_v1_migration_postgres.py @@ -55,6 +55,7 @@ def test_v1_session_import_real_postgres(tmp_path: Path) -> None: assert result.returncode == 0, result.stderr payload = json.loads(report.read_text(encoding="utf-8")) assert payload["summary"]["imported"] == 1 + PostgresRunStore.migrate(dsn, schema=schema) store = PostgresRunStore(dsn, schema=schema) runs = store.list_runs(tenant_id="migration-tenant") assert len(runs) == 1 diff --git a/tests/memory/backends/conftest.py b/tests/memory/backends/conftest.py index ded12b5..78d54b6 100644 --- a/tests/memory/backends/conftest.py +++ b/tests/memory/backends/conftest.py @@ -119,7 +119,7 @@ def add_resource_with_retry( raise last_exc -@pytest.fixture(autouse=True) +@pytest.fixture(scope="session", autouse=True) def _openviking_canary() -> Iterator[None]: pytest.importorskip("openviking", reason="openviking optional extra not installed") url = os.environ.get("OPEN_VIKING_URL", _DEFAULT_URL) diff --git a/tests/migration/test_v1_to_v2.py b/tests/migration/test_v1_to_v2.py index 8be67ea..26f4683 100644 --- a/tests/migration/test_v1_to_v2.py +++ b/tests/migration/test_v1_to_v2.py @@ -47,3 +47,39 @@ def test_v1_migration_inventory_is_read_only_and_machine_readable( "imported": 0, "invalid": 0, } + + +def test_v1_migration_reports_bad_filename_and_continues(tmp_path: Path) -> None: + source = tmp_path / "sessions" + source.mkdir() + (source / "not-a-uuid.jsonl").write_text("{}\n", encoding="utf-8") + session = Session(chunk_table=ChunkTable(rows=())) + SessionWriter(session, path=source / f"{session.id}.jsonl").close() + report = tmp_path / "inventory.json" + + result = subprocess.run( + [ + sys.executable, + "scripts/migrate_v1_to_v2.py", + "--source", + str(source), + "--report", + str(report), + "--tenant", + "tenant-1", + ], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + payload = json.loads(report.read_text(encoding="utf-8")) + assert payload["summary"] == { + "total": 2, + "ready": 1, + "imported": 0, + "invalid": 1, + } + invalid = next(item for item in payload["sessions"] if item["status"] == "invalid") + assert invalid["error_code"] == "invalid_session_id" diff --git a/tests/runtime/test_effect_ledger.py b/tests/runtime/test_effect_ledger.py index 95e7c6b..8f6f368 100644 --- a/tests/runtime/test_effect_ledger.py +++ b/tests/runtime/test_effect_ledger.py @@ -25,9 +25,7 @@ def _running(store: SQLiteRunStore) -> Run: tenant_id="tenant", ) ) - return store.transition_run( - queued.id, expected_version=0, target=RunStatus.RUNNING - ) + return store.transition_run(queued.id, expected_version=0, target=RunStatus.RUNNING) def test_completed_effect_is_deduplicated(tmp_path: Path) -> None: @@ -106,3 +104,59 @@ def test_crashed_idempotent_effect_is_retryable(tmp_path: Path) -> None: assert result.retryable == (invocation.id,) assert ledger.get(invocation.id).status is InvocationStatus.PREPARED assert store.get_run(run.id).status is RunStatus.RUNNING + + +def test_crashed_idempotent_effect_without_key_requires_review( + tmp_path: Path, +) -> None: + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + run = _running(store) + ledger = SQLiteEffectLedger(str(path)) + invocation = ledger.prepare( + run_id=run.id, + tool_name="object.put@1", + effect_class=EffectClass.IDEMPOTENT, + arguments_digest=arguments_digest({"key": "a"}), + idempotency_key=None, + ) + ledger.mark_dispatched(invocation.id) + + result = reconcile_stale_effects( + ledger, + store, + grace_seconds=0, + now=datetime.now(timezone.utc) + timedelta(seconds=1), + ) + + assert result.needs_review == (invocation.id,) + assert ledger.get(invocation.id).status is InvocationStatus.AMBIGUOUS + assert store.get_run(run.id).status is RunStatus.NEEDS_REVIEW + + +def test_runtime_reconciles_effects_before_expired_run_requeue( + tmp_path: Path, +) -> None: + from rath.runtime import LocalRuntime + + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + run = _running(store) + ledger = SQLiteEffectLedger(str(path)) + invocation = ledger.prepare( + run_id=run.id, + tool_name="payment.charge@1", + effect_class=EffectClass.NON_IDEMPOTENT, + arguments_digest=arguments_digest({"amount": 10}), + idempotency_key="charge-1", + ) + ledger.mark_dispatched(invocation.id) + runtime = LocalRuntime(store, effect_ledger=ledger) + + result = runtime.reconcile_effects( + grace_seconds=0, + now=datetime.now(timezone.utc) + timedelta(seconds=1), + ) + + assert result.needs_review == (invocation.id,) + assert store.get_run(run.id).status is RunStatus.NEEDS_REVIEW diff --git a/tests/runtime/test_local_runtime.py b/tests/runtime/test_local_runtime.py index 87c338c..9042795 100644 --- a/tests/runtime/test_local_runtime.py +++ b/tests/runtime/test_local_runtime.py @@ -111,7 +111,10 @@ def forward(self, session: Session) -> Session: assert failed is not None assert failed.status is RunStatus.FAILED - assert any(event.type == "run.execution.failed" for event in store.list_run_events(failed.id)) + assert any( + event.type == "run.execution.failed" + for event in store.list_run_events(failed.id) + ) def test_worker_restores_security_context_after_process_restart( diff --git a/tests/runtime/test_review_regressions.py b/tests/runtime/test_review_regressions.py new file mode 100644 index 0000000..caa7088 --- /dev/null +++ b/tests/runtime/test_review_regressions.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import time +from pathlib import Path +from uuid import uuid4 + +from rath.context import RunContext +from rath.definition import EffectClass, step +from rath.flow import Workflow +from rath.runtime import ( + Checkpoint, + LocalRuntime, + RunStatus, + SQLiteEffectLedger, + SQLiteRunStore, + arguments_digest, +) +from rath.session import Session + + +def test_sync_timeout_does_not_report_terminal_while_handler_runs( + tmp_path: Path, +) -> None: + effects: list[str] = [] + + class _SyncTimeout(Workflow): + @step( + entry=True, + effects=EffectClass.READ_ONLY, + timeout_seconds=0.01, + ) + def slow(self, state, context): # type: ignore[no-untyped-def] + time.sleep(0.05) + effects.append("completed") + return state + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + runtime.submit( + _SyncTimeout(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + + started = time.perf_counter() + completed = runtime.work_once(worker_id="worker") + elapsed = time.perf_counter() - started + + assert completed is not None + assert completed.status is RunStatus.TIMED_OUT + assert elapsed >= 0.05 + assert effects == ["completed"] + + +def test_checkpoint_false_skips_non_terminal_checkpoint(tmp_path: Path) -> None: + class _NoCheckpoint(Workflow): + @step( + entry=True, + successors=("finish",), + effects=EffectClass.READ_ONLY, + checkpoint=False, + ) + def start(self, state, context): # type: ignore[no-untyped-def] + return {**state, "started": True} + + @step(effects=EffectClass.READ_ONLY) + def finish(self, state, context): # type: ignore[no-untyped-def] + return {**state, "finished": True} + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + run = runtime.submit( + _NoCheckpoint(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + completed = runtime.work_once(worker_id="worker") + + assert completed is not None + assert completed.status is RunStatus.SUCCEEDED + checkpoints = store.list_checkpoints(run.id) + assert len(checkpoints) == 1 + assert checkpoints[0].state["finished"] is True + + +def test_runtime_enforces_declared_input_and_state_schema(tmp_path: Path) -> None: + class _SchemaWorkflow(Workflow): + input_schema = { + "type": "object", + "required": ["count"], + "properties": {"count": {"type": "integer"}}, + } + state_schema = { + "type": "object", + "required": ["count"], + "properties": {"count": {"type": "integer"}}, + } + + @step(entry=True, effects=EffectClass.READ_ONLY) + def corrupt(self, state, context): # type: ignore[no-untyped-def] + return {"count": "not-an-integer"} + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + + try: + runtime.submit( + _SchemaWorkflow(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + state={"count": "bad"}, + ) + except Exception as exc: + assert type(exc).__name__ == "SchemaValidationError" + else: + raise AssertionError("invalid input schema was accepted") + + run = runtime.submit( + _SchemaWorkflow(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + state={"count": 1}, + ) + completed = runtime.work_once(worker_id="worker") + + assert completed is not None + assert completed.status is RunStatus.FAILED + assert store.list_checkpoints(run.id) == () + + +def test_checkpoint_records_effect_watermark(tmp_path: Path) -> None: + class _EffectWorkflow(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def finish(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + path = tmp_path / "runtime.db" + store = SQLiteRunStore(path) + ledger = SQLiteEffectLedger(str(path)) + runtime = LocalRuntime(store, effect_ledger=ledger) + run = runtime.submit( + _EffectWorkflow(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + invocation = ledger.prepare( + run_id=run.id, + tool_name="lookup@1", + effect_class=EffectClass.READ_ONLY, + arguments_digest=arguments_digest({}), + idempotency_key="lookup", + ) + ledger.complete(invocation.id, {"ok": True}) + + completed = runtime.work_once(worker_id="worker") + + assert completed is not None + assert store.list_checkpoints(run.id)[0].effect_watermark == 1 + + +def test_runtime_fails_closed_on_checkpoint_plan_mismatch( + tmp_path: Path, +) -> None: + class _ResumeWorkflow(Workflow): + @step(entry=True, effects=EffectClass.READ_ONLY) + def finish(self, state, context): # type: ignore[no-untyped-def] + return state + + def forward(self, session: Session) -> Session: + return session + + store = SQLiteRunStore(tmp_path / "runtime.db") + runtime = LocalRuntime(store) + run = runtime.submit( + _ResumeWorkflow(), + session_id=uuid4(), + context=RunContext.local(revision_id=uuid4()), + ) + store.append_checkpoint( + Checkpoint.create( + run_id=run.id, + sequence=1, + plan_hash="0" * 64, + state={}, + next_nodes=("finish",), + effect_watermark=0, + ) + ) + + completed = runtime.work_once(worker_id="worker") + + assert completed is not None + assert completed.status is RunStatus.FAILED + assert any( + event.data.get("error_type") == "PlanMismatchError" + for event in store.list_run_events(run.id) + ) diff --git a/tests/runtime/test_run_state.py b/tests/runtime/test_run_state.py index 6947d9a..0354c63 100644 --- a/tests/runtime/test_run_state.py +++ b/tests/runtime/test_run_state.py @@ -58,4 +58,3 @@ def test_run_state_is_deeply_immutable() -> None: assert run.state["nested"]["value"] == 1 # type: ignore[index] assert run.version == 0 - diff --git a/tests/runtime/test_signals.py b/tests/runtime/test_signals.py index 1369ede..8eb280a 100644 --- a/tests/runtime/test_signals.py +++ b/tests/runtime/test_signals.py @@ -42,3 +42,8 @@ def receive(self, *, timeout_seconds: float = 0) -> RunSignal | None: ) assert bus.receive() is None assert bus.failures == 2 + + +def test_in_memory_receive_without_timeout_is_non_blocking() -> None: + bus = InMemorySignalBus() + assert bus.receive(timeout_seconds=0) is None diff --git a/tests/runtime/test_sqlite_run_store.py b/tests/runtime/test_sqlite_run_store.py index 5abc759..b12cf11 100644 --- a/tests/runtime/test_sqlite_run_store.py +++ b/tests/runtime/test_sqlite_run_store.py @@ -157,9 +157,7 @@ def test_interrupt_decision_and_waiting_resume_are_atomic(tmp_path: Path) -> Non assert decided.decision is not None assert decided.decision.actor_id == "user-1" assert store.list_interrupts(tenant_id="tenant-1") == () - assert store.list_interrupts( - tenant_id="tenant-1", pending_only=False - ) == (decided,) + assert store.list_interrupts(tenant_id="tenant-1", pending_only=False) == (decided,) with pytest.raises(ConflictError, match="already decided"): store.decide_interrupt( interrupt.id, @@ -189,13 +187,34 @@ def test_interrupt_deadline_expires_run_atomically(tmp_path: Path) -> None: store.create_interrupt(interrupt, expected_run_version=running.version) assert interrupt.expires_at is not None - assert store.expire_interrupts( - now=interrupt.expires_at + timedelta(seconds=1) - ) == (interrupt.id,) + assert store.expire_interrupts(now=interrupt.expires_at + timedelta(seconds=1)) == ( + interrupt.id, + ) expired = store.get_interrupt(interrupt.id) assert expired.decision is not None assert expired.decision.kind is ApprovalDecisionKind.REJECT assert store.get_run(running.id).status is RunStatus.TIMED_OUT - assert store.expire_interrupts( - now=interrupt.expires_at + timedelta(seconds=2) - ) == () + assert ( + store.expire_interrupts(now=interrupt.expires_at + timedelta(seconds=2)) == () + ) + + +def test_store_pushes_down_run_and_event_cursors(tmp_path: Path) -> None: + store = SQLiteRunStore(tmp_path / "runtime.db") + first = store.create_run(_run()) + second = store.create_run(_run()) + third = store.create_run(_run()) + + page = store.list_runs(tenant_id="tenant-1", limit=2) + assert page == (first, second) + assert store.list_runs( + tenant_id="tenant-1", + after=second.id, + limit=2, + ) == (third,) + + store.append_run_event(first.id, "custom.one", {}) + store.append_run_event(first.id, "custom.two", {}) + events = store.list_run_events(first.id, after_sequence=1, limit=1) + assert len(events) == 1 + assert events[0].sequence == 2 diff --git a/tests/security/test_context.py b/tests/security/test_context.py index 30ee344..aca9c7e 100644 --- a/tests/security/test_context.py +++ b/tests/security/test_context.py @@ -72,4 +72,3 @@ def test_run_context_deadline_check_uses_stable_error_code() -> None: assert raised.value.code.value == "runtime.deadline_exceeded" assert isinstance(context.request_id, UUID) - diff --git a/tests/security/test_policy.py b/tests/security/test_policy.py index 04328d4..1615440 100644 --- a/tests/security/test_policy.py +++ b/tests/security/test_policy.py @@ -96,4 +96,3 @@ def test_policy_constraints_validate_resource_budgets() -> None: PolicyConstraints(max_output_bytes=0) with pytest.raises(ValueError, match="timeout_seconds"): PolicyConstraints(timeout_seconds=-1) - diff --git a/tests/security/test_secrets_audit.py b/tests/security/test_secrets_audit.py index 5a171e4..b3a6c1b 100644 --- a/tests/security/test_secrets_audit.py +++ b/tests/security/test_secrets_audit.py @@ -52,4 +52,3 @@ async def exercise() -> None: assert event.tenant_id == "local" asyncio.run(exercise()) - diff --git a/tests/server/test_agent_server.py b/tests/server/test_agent_server.py index 8479da6..ee306ba 100644 --- a/tests/server/test_agent_server.py +++ b/tests/server/test_agent_server.py @@ -10,6 +10,7 @@ from rath.flow import Workflow from rath.runtime import InterruptKind, LocalRuntime, RunStatus, SQLiteRunStore from rath.security import ( + InMemoryAuditSink, LocalTrustedPolicy, Principal, PrincipalKind, @@ -41,6 +42,98 @@ def forward(self, session: Session) -> Session: return session +def test_control_mutation_emits_redacted_audit(tmp_path: Path) -> None: + import asyncio + + async def exercise() -> None: + store = SQLiteRunStore(tmp_path / "audit.db") + runtime = LocalRuntime(store) + context = SecurityContext( + principal=Principal(id="operator", kind=PrincipalKind.USER), + tenant_id="tenant", + grants=frozenset({"*"}), + ) + audit = InMemoryAuditSink() + server = AgentServer( + store, + runtime, + auth=StaticTokenAuth({"super-secret-token": context}), + audit_sink=audit, + ) + server.register_assistant("echo", _Echo(), revision_id=uuid4()) + transport = httpx.ASGITransport(app=server.app) + headers = {"Authorization": "Bearer super-secret-token"} + async with httpx.AsyncClient( + transport=transport, + base_url="http://test", + ) as client: + session = await client.post("/v1/sessions", headers=headers) + run = await client.post( + "/v1/runs", + headers=headers, + json={ + "assistant_id": "echo", + "session_id": session.json()["id"], + }, + ) + response = await client.post( + f"/v1/runs/{run.json()['id']}/cancel", + headers=headers, + ) + assert response.status_code == 200 + + assert [event.action for event in audit.events] == ["run.cancel"] + assert "super-secret-token" not in repr(audit.events) + + asyncio.run(exercise()) + + +def test_server_requires_explicit_action_grants(tmp_path: Path) -> None: + import asyncio + + async def exercise() -> None: + store = SQLiteRunStore(tmp_path / "authorization.db") + runtime = LocalRuntime(store) + denied = SecurityContext( + principal=Principal(id="user", kind=PrincipalKind.USER), + tenant_id="tenant", + ) + reader = SecurityContext( + principal=Principal(id="reader", kind=PrincipalKind.USER), + tenant_id="tenant", + grants=frozenset({"assistant.read"}), + ) + server = AgentServer( + store, + runtime, + auth=StaticTokenAuth({"denied": denied, "reader": reader}), + ) + server.register_assistant("echo", _Echo(), revision_id=uuid4()) + transport = httpx.ASGITransport(app=server.app) + async with httpx.AsyncClient( + transport=transport, + base_url="http://test", + ) as client: + no_grant = await client.get( + "/v1/assistants", + headers={"Authorization": "Bearer denied"}, + ) + assert no_grant.status_code == 403 + allowed = await client.get( + "/v1/assistants", + headers={"Authorization": "Bearer reader"}, + ) + assert allowed.status_code == 200 + mutation = await client.post( + "/v1/assistants", + headers={"Authorization": "Bearer reader"}, + json={"id": "alias", "template_id": "echo"}, + ) + assert mutation.status_code == 403 + + asyncio.run(exercise()) + + def test_server_auth_tenant_idempotency_and_sse_sync(tmp_path: Path) -> None: import asyncio @@ -50,6 +143,7 @@ async def exercise() -> None: tenant = SecurityContext( principal=Principal(id="user-1", kind=PrincipalKind.USER), tenant_id="tenant-1", + grants=frozenset({"*"}), ) server = AgentServer( store, @@ -67,9 +161,11 @@ async def exercise() -> None: "Authorization": "Bearer token", "Idempotency-Key": "req-1", } + session = await client.post("/v1/sessions", headers=headers) + assert session.status_code == 201 body = { "assistant_id": "echo", - "session_id": str(uuid4()), + "session_id": session.json()["id"], "state": {"value": 1}, } first = await client.post("/v1/runs", headers=headers, json=body) @@ -90,8 +186,13 @@ async def exercise() -> None: assert stream.status_code == 200 assert "run.checkpoint.created" in stream.text assert (await client.get("/health/ready")).status_code == 200 - assert (await client.get("/openapi.json")).status_code == 200 - assert (await client.get("/metrics")).status_code == 200 + openapi = await client.get("/openapi.json") + assert openapi.status_code == 200 + schema = openapi.json() + assert schema["paths"]["/v1/runs"]["post"]["operationId"] == "createRun" + assert schema["components"]["securitySchemes"]["bearerAuth"] + assert all(operations for operations in schema["paths"].values()) + assert (await client.get("/metrics", headers=headers)).status_code == 200 session = await client.post("/v1/sessions", headers=headers) assert session.status_code == 201 @@ -114,19 +215,19 @@ async def exercise() -> None: ) assert alias.status_code == 201 assert alias.json()["kind"] == "alias" + alias_session = await client.post("/v1/sessions", headers=headers) + assert alias_session.status_code == 201 alias_run = await client.post( "/v1/runs", headers={"Authorization": "Bearer token"}, json={ "assistant_id": "tenant-echo", - "session_id": str(uuid4()), + "session_id": alias_session.json()["id"], "state": {"value": 3}, }, ) assert alias_run.status_code == 201 - listed_assistants = await client.get( - "/v1/assistants", headers=headers - ) + listed_assistants = await client.get("/v1/assistants", headers=headers) assert {item["id"] for item in listed_assistants.json()["items"]} == { "echo", "tenant-echo", @@ -136,6 +237,11 @@ async def exercise() -> None: headers={**headers, "Last-Event-ID": "1"}, ) assert "id: 1\n" not in reconnected.text + invalid_cursor = await client.get( + f"/v1/runs/{run_id}/stream", + headers={**headers, "Last-Event-ID": "not-an-integer"}, + ) + assert invalid_cursor.status_code == 400 feedback = await client.post( "/v1/feedback", headers=headers, @@ -159,7 +265,7 @@ def test_store_api_is_policy_governed_and_tenant_scoped(tmp_path: Path) -> None: async def exercise() -> None: store = SQLiteRunStore(tmp_path / "store-api.db") runtime = LocalRuntime(store) - local = SecurityContext.local() + local = SecurityContext.local(grants=("*", "trusted_host")) calls: list[tuple[str, str, dict[str, object]]] = [] def memory_handler(operation, namespace, payload, context): # type: ignore[no-untyped-def] @@ -206,6 +312,7 @@ async def exercise() -> None: tenant = SecurityContext( principal=Principal(id="reviewer", kind=PrincipalKind.USER), tenant_id="tenant", + grants=frozenset({"*"}), ) server = AgentServer( store, @@ -223,12 +330,14 @@ async def exercise() -> None: base_url="http://test", ) as client: headers = {"Authorization": "Bearer token"} + session = await client.post("/v1/sessions", headers=headers) + assert session.status_code == 201 created = await client.post( "/v1/runs", headers=headers, json={ "assistant_id": "approval", - "session_id": str(uuid4()), + "session_id": session.json()["id"], }, ) assert created.status_code == 201 @@ -240,10 +349,10 @@ async def exercise() -> None: decided = await client.post( f"/v1/interrupts/{interrupt['id']}/decision", headers=headers, - json={ - "kind": "approve", - "reason": "expected test operation", - }, + json={ + "kind": "approve", + "reason": "expected test operation", + }, ) assert decided.status_code == 200 completed = runtime.work_once(worker_id="worker-2") diff --git a/tests/server/test_openapi_contract.py b/tests/server/test_openapi_contract.py new file mode 100644 index 0000000..e9414ad --- /dev/null +++ b/tests/server/test_openapi_contract.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from rath.server.app import _openapi_document + + +def test_committed_openapi_matches_generator() -> None: + committed = json.loads( + Path("deploy/docs/openapi-v2.json").read_text(encoding="utf-8") + ) + assert committed == _openapi_document( + "2.0.0-unreleased", + store_enabled=True, + ) + + +def test_openapi_documents_security_actions_and_schemas() -> None: + document = _openapi_document("2.0.0-unreleased", store_enabled=True) + paths = document["paths"] + assert paths["/v1/runs"]["post"]["x-openrath-action"] == "run.create" + assert paths["/metrics"]["get"]["security"] == [{"bearerAuth": []}] + assert ( + paths["/v1/runs"]["post"]["requestBody"]["content"]["application/json"][ + "schema" + ]["$ref"] + == "#/components/schemas/CreateRunRequest" + ) + assert document["components"]["schemas"]["Run"]["required"] diff --git a/tests/unit/test_errors_v2.py b/tests/unit/test_errors_v2.py index 6f7db2e..3dc6272 100644 --- a/tests/unit/test_errors_v2.py +++ b/tests/unit/test_errors_v2.py @@ -25,4 +25,3 @@ def test_error_details_are_immutable_copies() -> None: details["nested"]["value"] = 2 assert error.details["nested"]["value"] == 1 # type: ignore[index] - diff --git a/uv.lock b/uv.lock index c3ff440..3f9dd72 100644 --- a/uv.lock +++ b/uv.lock @@ -31,7 +31,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -41,78 +41,88 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, - { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, - { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, - { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, - { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, - { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, - { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, - { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" }, + { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" }, + { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, ] [[package]] @@ -1316,11 +1326,11 @@ wheels = [ [[package]] name = "json-repair" -version = "0.59.10" +version = "0.61.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/7c/e95bb03068572146eba37e8175c760f470ea0a6097310e16bbf2bc6e6457/json_repair-0.59.10.tar.gz", hash = "sha256:2e4b85537c752d8a513ea28fdad891e5ede32c83de745366b97f648b8c34ede7", size = 49133, upload-time = "2026-05-14T06:41:51.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/a3/6001c2448ee54a80f35a2501b848f4bbd87987bd41ead8cc17367a2bfd56/json_repair-0.61.7.tar.gz", hash = "sha256:a3754543f050093efcda6c9ab00b20a236b5d082c8c622bc65b88fa74ff8d51f", size = 51573, upload-time = "2026-07-21T13:01:15.085Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/87/49b20c6b81493d55c311f711ed87319d0fbad8bd0bbfbe36e52103af36bd/json_repair-0.59.10-py3-none-any.whl", hash = "sha256:5468fa3eaadcc9b4a5646776bc4176e2fe5f374b5848a15f468cce3b60e3db0e", size = 47742, upload-time = "2026-05-14T06:41:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/76/da/7f9e2b0a1120b107a204bbab6d0ef7ff2ae37790bddc5ee21c9c1f961f3b/json_repair-0.61.7-py3-none-any.whl", hash = "sha256:45c99b8cffef404e846b60d3dc21fc6f0fd5a4595cebad169dfab083ffb8246a", size = 50146, upload-time = "2026-07-21T13:01:13.734Z" }, ] [[package]] @@ -2003,7 +2013,11 @@ opensandbox = [ { name = "opensandbox-server" }, ] openviking = [ + { name = "aiohttp" }, + { name = "json-repair" }, { name = "openviking" }, + { name = "pillow" }, + { name = "soupsieve" }, ] otel = [ { name = "opentelemetry-api" }, @@ -2046,9 +2060,11 @@ docs = [ [package.metadata] requires-dist = [ + { name = "aiohttp", marker = "extra == 'openviking'", specifier = ">=3.14.1" }, { name = "anthropic", specifier = ">=0.40.0" }, { name = "boto3", marker = "extra == 's3'", specifier = ">=1.40,<2" }, { name = "httpx", marker = "extra == 'server'", specifier = ">=0.28,<1" }, + { name = "json-repair", marker = "extra == 'openviking'", specifier = ">=0.60.1" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.80,<1.88" }, { name = "mcp", specifier = ">=1.28.1,<2" }, { name = "openai", specifier = ">=1.0.0" }, @@ -2058,9 +2074,11 @@ requires-dist = [ { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.36,<2" }, { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.36,<2" }, { name = "openviking", marker = "extra == 'openviking'", specifier = ">=0.4.11" }, + { name = "pillow", marker = "extra == 'openviking'", specifier = ">=12.3.0" }, { name = "psycopg", extras = ["binary", "pool"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=6,<7" }, + { name = "soupsieve", marker = "extra == 'openviking'", specifier = ">=2.8.4" }, { name = "starlette", marker = "extra == 'server'", specifier = ">=1.3.1,<2" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.35,<1" }, ] @@ -2413,75 +2431,54 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] @@ -3614,11 +3611,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, ] [[package]] From 0f454002b1b764e2e6be35746a89266184fca965 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Tue, 28 Jul 2026 12:21:20 +0800 Subject: [PATCH 14/22] docs: record v2 review validation evidence --- release/evidence/v2.0.0-review/README.md | 59 + .../image-scan-high-critical.json | 8309 +++++++++++++++ release/evidence/v2.0.0-review/manifest.json | 86 + .../openrath-v2-review.sbom.cdx.json | 9408 +++++++++++++++++ .../v2.0.0-review/repository-secret-scan.json | 2733 +++++ 5 files changed, 20595 insertions(+) create mode 100644 release/evidence/v2.0.0-review/README.md create mode 100644 release/evidence/v2.0.0-review/image-scan-high-critical.json create mode 100644 release/evidence/v2.0.0-review/manifest.json create mode 100644 release/evidence/v2.0.0-review/openrath-v2-review.sbom.cdx.json create mode 100644 release/evidence/v2.0.0-review/repository-secret-scan.json diff --git a/release/evidence/v2.0.0-review/README.md b/release/evidence/v2.0.0-review/README.md new file mode 100644 index 0000000..36ca875 --- /dev/null +++ b/release/evidence/v2.0.0-review/README.md @@ -0,0 +1,59 @@ +# OpenRath v2.0.0 review-remediation evidence + +This bundle records the validation performed for implementation commit +`51a26c4a23850876548c61340da2d2da3bc834ce`. It is review evidence, not a +v2.0.0 release bundle. + +## Decision + +- `release_approved`: **false** +- `package_version`: `1.3.0` +- `intended_release`: `2.0.0` (unreleased) +- Local implementation gates: passed +- Production/RC Gate D: not satisfied + +The package version was deliberately not changed to `2.0.0`. A local image ID +is not a published registry digest, and the unavailable external and +operational gates cannot be replaced by unit tests or a short local soak. + +## Completed validation + +| Area | Result | +|---|---| +| Lock | `uv lock --check` passed | +| Lint/format | Ruff check and format check passed | +| Types | mypy passed for 166 source files | +| Offline test matrix | 1043 passed, 20 skipped | +| OpenAPI contract | 2 passed; generated document matches the committed golden file | +| OpenSandbox real service | 45 passed, 3 skipped in the full run; the single environment-sensitive timing assertion was corrected and its focused rerun passed | +| PostgreSQL/Redis/S3 integration | 10 passed against real local services | +| Build | wheel and sdist built; `twine check` passed | +| Dependency audit | exact production set and all extras/groups: no known vulnerabilities | +| Container build | current source built as local image `openrath:review` | +| Image scan | 0 fixed HIGH/CRITICAL findings with Trivy 0.67.2 | +| Repository secret scan | 0 findings with Trivy 0.67.2 | +| Reference manifests | Compose validation and strict kubeconform validation passed | +| Review soak | 143 runs in 10.03 seconds, 0 failures; review profile only | + +## Outstanding release gates + +The following require external credentials, services, a registry, target +cluster, or elapsed operational time and remain blocking: + +1. Live LLM/provider lifecycle using the approved production provider. +2. Live OpenViking lifecycle against the approved service. +3. Published OpenRath OCI artifact addressed by an immutable registry digest. +4. Eight-hour soak on target-like hardware. +5. Worker scale test from one to four replicas. +6. Backup/restore, rollout/rollback, database restart, S3 restart, and Redis + loss drills on the target cluster. +7. Final CI run and release evidence regeneration on the frozen RC commit. + +Do not tag, publish, or deploy v2.0.0 from this evidence bundle. + +## Included artifacts + +- `manifest.json`: machine-readable result and blocker summary. +- `openrath-v2-review.sbom.cdx.json`: CycloneDX SBOM for the local image. +- `image-scan-high-critical.json`: Trivy HIGH/CRITICAL image scan. +- `repository-secret-scan.json`: Trivy repository secret scan. diff --git a/release/evidence/v2.0.0-review/image-scan-high-critical.json b/release/evidence/v2.0.0-review/image-scan-high-critical.json new file mode 100644 index 0000000..d95e42c --- /dev/null +++ b/release/evidence/v2.0.0-review/image-scan-high-critical.json @@ -0,0 +1,8309 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2026-07-28T04:18:30.145618288Z", + "ArtifactName": "openrath:review", + "ArtifactType": "container_image", + "Metadata": { + "Size": 239066624, + "OS": { + "Family": "debian", + "Name": "13.6" + }, + "ImageID": "sha256:181247207c0f57e438c42676579a32d98cd7f9ea66c2c6b1bf2c233526723b14", + "DiffIDs": [ + "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f", + "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167", + "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd", + "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f", + "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8", + "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1", + "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f", + "sha256:f2776a24e2368b14a6e05e79b2abd1f2b85fb5217de0b9b924fb2a926538d169" + ], + "RepoTags": [ + "openrath:review" + ], + "ImageConfig": { + "architecture": "amd64", + "created": "2026-07-28T04:16:47.739619966Z", + "history": [ + { + "created": "2026-07-13T00:00:00Z", + "created_by": "# debian.sh --arch 'amd64' out/ 'trixie' '@1783900800'", + "comment": "debuerreotype 0.17" + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV LANG=C.UTF-8", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "RUN /bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates \t\tnetbase \t\ttzdata \t; \tapt-get dist-clean # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV PYTHON_VERSION=3.12.13", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:02:10Z", + "created_by": "ENV PYTHON_SHA256=c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-14T02:11:29Z", + "created_by": "RUN /bin/sh -c set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdpkg-dev \t\tgcc \t\tgnupg \t\tlibbluetooth-dev \t\tlibbz2-dev \t\tlibc6-dev \t\tlibdb-dev \t\tlibffi-dev \t\tlibgdbm-dev \t\tliblzma-dev \t\tlibncursesw5-dev \t\tlibreadline-dev \t\tlibsqlite3-dev \t\tlibssl-dev \t\tmake \t\ttk-dev \t\tuuid-dev \t\twget \t\txz-utils \t\tzlib1g-dev \t; \t\twget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"; \techo \"$PYTHON_SHA256 *python.tar.xz\" | sha256sum -c -; \twget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"; \tGNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME; \tgpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"; \tgpg --batch --verify python.tar.xz.asc python.tar.xz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\" python.tar.xz.asc; \tmkdir -p /usr/src/python; \ttar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \trm python.tar.xz; \t\tcd /usr/src/python; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--enable-loadable-sqlite-extensions \t\t--enable-optimizations \t\t--enable-option-checking=fatal \t\t--enable-shared \t\t$(test \"${gnuArch%%-*}\" != 'riscv64' \u0026\u0026 echo '--with-lto') \t\t--with-ensurepip \t; \tnproc=\"$(nproc)\"; \tEXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"; \tLDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"; \tLDFLAGS=\"${LDFLAGS:-} -Wl,--strip-all\"; \tarch=\"$(dpkg --print-architecture)\"; arch=\"${arch##*-}\"; \tcase \"$arch\" in \t\tamd64|arm64) \t\t\tEXTRA_CFLAGS=\"${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer\"; \t\t\t;; \t\ti386) \t\t\t;; \t\t*) \t\t\tEXTRA_CFLAGS=\"${EXTRA_CFLAGS:-} -fno-omit-frame-pointer\"; \t\t\t;; \tesac; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-}\" \t; \trm python; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-} -Wl,-rpath='\\$\\$ORIGIN/../lib'\" \t\tpython \t; \tmake install; \t\tcd /; \trm -rf /usr/src/python; \t\tfind /usr/local -depth \t\t\\( \t\t\t\\( -type d -a \\( -name test -o -name tests -o -name idle_test \\) \\) \t\t\t-o \\( -type f -a \\( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \\) \\) \t\t\\) -exec rm -rf '{}' + \t; \t\tldconfig; \t\tapt-mark auto '.*' \u003e /dev/null; \tapt-mark manual $savedAptMark; \tfind /usr/local -type f -executable -not \\( -name '*tkinter*' \\) -exec ldd '{}' ';' \t\t| awk '/=\u003e/ { so = $(NF-1); if (index(so, \"/usr/local/\") == 1) { next }; gsub(\"^/(usr/)?\", \"\", so); printf \"*%s\\n\", so }' \t\t| sort -u \t\t| xargs -rt dpkg-query --search \t\t| awk 'sub(\":$\", \"\", $1) { print $1 }' \t\t| sort -u \t\t| xargs -r apt-mark manual \t; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \tapt-get dist-clean; \t\texport PYTHONDONTWRITEBYTECODE=1; \tpython3 --version; \tpip3 --version # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-14T02:11:29Z", + "created_by": "RUN /bin/sh -c set -eux; \tfor src in idle3 pip3 pydoc3 python3 python3-config; do \t\tdst=\"$(echo \"$src\" | tr -d 3)\"; \t\t[ -s \"/usr/local/bin/$src\" ]; \t\t[ ! -e \"/usr/local/bin/$dst\" ]; \t\tln -svT \"$src\" \"/usr/local/bin/$dst\"; \tdone # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-14T02:11:29Z", + "created_by": "CMD [\"python3\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-27T10:45:44Z", + "created_by": "ENV PATH=/opt/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PYTHONPATH=/app PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 OPENRATH_HOST=0.0.0.0 OPENRATH_PORT=8000", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-27T10:45:44Z", + "created_by": "RUN /bin/sh -c groupadd --system --gid 10001 openrath \u0026\u0026 useradd --system --uid 10001 --gid openrath --home /app openrath # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-27T10:45:44Z", + "created_by": "WORKDIR /app", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-28T04:16:47Z", + "created_by": "COPY /opt/venv /opt/venv # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-28T04:16:47Z", + "created_by": "COPY examples ./examples # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-07-28T04:16:47Z", + "created_by": "USER 10001:10001", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-28T04:16:47Z", + "created_by": "EXPOSE map[8000/tcp:{}]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-07-28T04:16:47Z", + "created_by": "ENTRYPOINT [\"openrath-server\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f", + "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167", + "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd", + "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f", + "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8", + "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1", + "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f", + "sha256:f2776a24e2368b14a6e05e79b2abd1f2b85fb5217de0b9b924fb2a926538d169" + ] + }, + "config": { + "Entrypoint": [ + "openrath-server" + ], + "Env": [ + "PATH=/opt/venv/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "LANG=C.UTF-8", + "GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305", + "PYTHON_VERSION=3.12.13", + "PYTHON_SHA256=c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684", + "PYTHONPATH=/app", + "PYTHONDONTWRITEBYTECODE=1", + "PYTHONUNBUFFERED=1", + "OPENRATH_HOST=0.0.0.0", + "OPENRATH_PORT=8000" + ], + "User": "10001:10001", + "WorkingDir": "/app", + "ExposedPorts": { + "8000/tcp": {} + } + } + }, + "Layers": [ + { + "Size": 81049600, + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "Size": 4127232, + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "Size": 38094848, + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "Size": 5120, + "DiffID": "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f" + }, + { + "Size": 11264, + "DiffID": "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8" + }, + { + "Size": 1536, + "DiffID": "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1" + }, + { + "Size": 115771392, + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "Size": 5632, + "DiffID": "sha256:f2776a24e2368b14a6e05e79b2abd1f2b85fb5217de0b9b924fb2a926538d169" + } + ] + }, + "Results": [ + { + "Target": "openrath:review (debian 13.6)", + "Class": "os-pkgs", + "Type": "debian", + "Packages": [ + { + "ID": "adduser@3.152", + "Name": "adduser", + "Identifier": { + "PURL": "pkg:deb/debian/adduser@3.152?arch=all\u0026distro=debian-13.6", + "UID": "a26e3466c18314ad" + }, + "Version": "3.152", + "Arch": "all", + "SrcName": "adduser", + "SrcVersion": "3.152", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian Adduser Developers \u003cadduser@packages.debian.org\u003e", + "DependsOn": [ + "passwd@1:4.17.4-2" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/sbin/adduser", + "/usr/sbin/deluser", + "/usr/share/doc/adduser/NEWS.Debian.gz", + "/usr/share/doc/adduser/README.gz", + "/usr/share/doc/adduser/TODO", + "/usr/share/doc/adduser/changelog.gz", + "/usr/share/doc/adduser/copyright", + "/usr/share/doc/adduser/examples/INSTALL", + "/usr/share/doc/adduser/examples/README", + "/usr/share/doc/adduser/examples/adduser.conf", + "/usr/share/doc/adduser/examples/adduser.local", + "/usr/share/doc/adduser/examples/adduser.local.conf", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/bash.bashrc", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/profile", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel.other/index.html", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel/dot.bash_logout", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel/dot.bash_profile", + "/usr/share/doc/adduser/examples/adduser.local.conf.examples/skel/dot.bashrc", + "/usr/share/doc/adduser/examples/deluser.conf", + "/usr/share/man/da/man5/deluser.conf.5.gz", + "/usr/share/man/de/man5/adduser.conf.5.gz", + "/usr/share/man/de/man5/deluser.conf.5.gz", + "/usr/share/man/de/man8/adduser.8.gz", + "/usr/share/man/de/man8/adduser.local.8.gz", + "/usr/share/man/de/man8/deluser.8.gz", + "/usr/share/man/es/man5/deluser.conf.5.gz", + "/usr/share/man/fr/man5/adduser.conf.5.gz", + "/usr/share/man/fr/man5/deluser.conf.5.gz", + "/usr/share/man/fr/man8/adduser.8.gz", + "/usr/share/man/fr/man8/deluser.8.gz", + "/usr/share/man/it/man5/deluser.conf.5.gz", + "/usr/share/man/man5/adduser.conf.5.gz", + "/usr/share/man/man5/deluser.conf.5.gz", + "/usr/share/man/man8/adduser.8.gz", + "/usr/share/man/man8/adduser.local.8.gz", + "/usr/share/man/man8/deluser.8.gz", + "/usr/share/man/nl/man5/adduser.conf.5.gz", + "/usr/share/man/nl/man5/deluser.conf.5.gz", + "/usr/share/man/nl/man8/adduser.8.gz", + "/usr/share/man/nl/man8/adduser.local.8.gz", + "/usr/share/man/nl/man8/deluser.8.gz", + "/usr/share/man/pl/man5/deluser.conf.5.gz", + "/usr/share/man/pt/man5/adduser.conf.5.gz", + "/usr/share/man/pt/man5/deluser.conf.5.gz", + "/usr/share/man/pt/man8/adduser.8.gz", + "/usr/share/man/pt/man8/adduser.local.8.gz", + "/usr/share/man/pt/man8/deluser.8.gz", + "/usr/share/man/ro/man5/adduser.conf.5.gz", + "/usr/share/man/ro/man5/deluser.conf.5.gz", + "/usr/share/man/ro/man8/adduser.8.gz", + "/usr/share/man/ro/man8/adduser.local.8.gz", + "/usr/share/man/ro/man8/deluser.8.gz", + "/usr/share/man/ru/man5/deluser.conf.5.gz", + "/usr/share/man/sv/man5/deluser.conf.5.gz", + "/usr/share/perl5/Debian/AdduserCommon.pm", + "/usr/share/perl5/Debian/AdduserLogging.pm", + "/usr/share/perl5/Debian/AdduserRetvalues.pm" + ] + }, + { + "ID": "apt@3.0.3", + "Name": "apt", + "Identifier": { + "PURL": "pkg:deb/debian/apt@3.0.3?arch=amd64\u0026distro=debian-13.6", + "UID": "3c1822d549195c1f" + }, + "Version": "3.0.3", + "Arch": "amd64", + "SrcName": "apt", + "SrcVersion": "3.0.3", + "Licenses": [ + "GPL-2.0-or-later", + "curl", + "BSD-3-Clause", + "MIT", + "GPL-2.0-only" + ], + "Maintainer": "APT Development Team \u003cdeity@lists.debian.org\u003e", + "DependsOn": [ + "adduser@3.152", + "base-passwd@3.6.7", + "debian-archive-keyring@2025.1", + "libapt-pkg7.0@3.0.3", + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19", + "libseccomp2@2.6.0-2", + "libssl3t64@3.5.6-1~deb13u2", + "libstdc++6@14.2.0-19", + "libsystemd0@257.13-1~deb13u1", + "sqv@1.3.0-3+b2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/apt", + "/usr/bin/apt-cache", + "/usr/bin/apt-cdrom", + "/usr/bin/apt-config", + "/usr/bin/apt-get", + "/usr/bin/apt-mark", + "/usr/lib/apt/apt-extracttemplates", + "/usr/lib/apt/apt-helper", + "/usr/lib/apt/apt.systemd.daily", + "/usr/lib/apt/methods/cdrom", + "/usr/lib/apt/methods/copy", + "/usr/lib/apt/methods/file", + "/usr/lib/apt/methods/gpgv", + "/usr/lib/apt/methods/http", + "/usr/lib/apt/methods/mirror", + "/usr/lib/apt/methods/rred", + "/usr/lib/apt/methods/sqv", + "/usr/lib/apt/methods/store", + "/usr/lib/apt/solvers/dump", + "/usr/lib/dpkg/methods/apt/desc.apt", + "/usr/lib/dpkg/methods/apt/install", + "/usr/lib/dpkg/methods/apt/names", + "/usr/lib/dpkg/methods/apt/setup", + "/usr/lib/dpkg/methods/apt/update", + "/usr/lib/systemd/system/apt-daily-upgrade.service", + "/usr/lib/systemd/system/apt-daily-upgrade.timer", + "/usr/lib/systemd/system/apt-daily.service", + "/usr/lib/systemd/system/apt-daily.timer", + "/usr/lib/x86_64-linux-gnu/libapt-private.so.0.0.0", + "/usr/share/apt/default-sequoia.config", + "/usr/share/bash-completion/completions/apt", + "/usr/share/bug/apt/script", + "/usr/share/doc/apt/NEWS.Debian.gz", + "/usr/share/doc/apt/README.md.gz", + "/usr/share/doc/apt/changelog.gz", + "/usr/share/doc/apt/copyright", + "/usr/share/doc/apt/examples/apt.conf", + "/usr/share/doc/apt/examples/configure-index", + "/usr/share/doc/apt/examples/debian.sources", + "/usr/share/doc/apt/examples/preferences", + "/usr/share/lintian/overrides/apt", + "/usr/share/locale/ar/LC_MESSAGES/apt.mo", + "/usr/share/locale/ast/LC_MESSAGES/apt.mo", + "/usr/share/locale/bg/LC_MESSAGES/apt.mo", + "/usr/share/locale/bs/LC_MESSAGES/apt.mo", + "/usr/share/locale/ca/LC_MESSAGES/apt.mo", + "/usr/share/locale/cs/LC_MESSAGES/apt.mo", + "/usr/share/locale/cy/LC_MESSAGES/apt.mo", + "/usr/share/locale/da/LC_MESSAGES/apt.mo", + "/usr/share/locale/de/LC_MESSAGES/apt.mo", + "/usr/share/locale/dz/LC_MESSAGES/apt.mo", + "/usr/share/locale/el/LC_MESSAGES/apt.mo", + "/usr/share/locale/es/LC_MESSAGES/apt.mo", + "/usr/share/locale/eu/LC_MESSAGES/apt.mo", + "/usr/share/locale/fi/LC_MESSAGES/apt.mo", + "/usr/share/locale/fr/LC_MESSAGES/apt.mo", + "/usr/share/locale/gl/LC_MESSAGES/apt.mo", + "/usr/share/locale/hu/LC_MESSAGES/apt.mo", + "/usr/share/locale/it/LC_MESSAGES/apt.mo", + "/usr/share/locale/ja/LC_MESSAGES/apt.mo", + "/usr/share/locale/km/LC_MESSAGES/apt.mo", + "/usr/share/locale/ko/LC_MESSAGES/apt.mo", + "/usr/share/locale/ku/LC_MESSAGES/apt.mo", + "/usr/share/locale/lt/LC_MESSAGES/apt.mo", + "/usr/share/locale/mr/LC_MESSAGES/apt.mo", + "/usr/share/locale/nb/LC_MESSAGES/apt.mo", + "/usr/share/locale/ne/LC_MESSAGES/apt.mo", + "/usr/share/locale/nl/LC_MESSAGES/apt.mo", + "/usr/share/locale/nn/LC_MESSAGES/apt.mo", + "/usr/share/locale/pl/LC_MESSAGES/apt.mo", + "/usr/share/locale/pt/LC_MESSAGES/apt.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/apt.mo", + "/usr/share/locale/ro/LC_MESSAGES/apt.mo", + "/usr/share/locale/ru/LC_MESSAGES/apt.mo", + "/usr/share/locale/sk/LC_MESSAGES/apt.mo", + "/usr/share/locale/sl/LC_MESSAGES/apt.mo", + "/usr/share/locale/sv/LC_MESSAGES/apt.mo", + "/usr/share/locale/th/LC_MESSAGES/apt.mo", + "/usr/share/locale/tl/LC_MESSAGES/apt.mo", + "/usr/share/locale/tr/LC_MESSAGES/apt.mo", + "/usr/share/locale/uk/LC_MESSAGES/apt.mo", + "/usr/share/locale/vi/LC_MESSAGES/apt.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/apt.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/apt.mo", + "/usr/share/man/de/man1/apt-transport-http.1.gz", + "/usr/share/man/de/man1/apt-transport-https.1.gz", + "/usr/share/man/de/man1/apt-transport-mirror.1.gz", + "/usr/share/man/de/man5/apt.conf.5.gz", + "/usr/share/man/de/man5/apt_auth.conf.5.gz", + "/usr/share/man/de/man5/apt_preferences.5.gz", + "/usr/share/man/de/man5/sources.list.5.gz", + "/usr/share/man/de/man7/apt-patterns.7.gz", + "/usr/share/man/de/man8/apt-cache.8.gz", + "/usr/share/man/de/man8/apt-cdrom.8.gz", + "/usr/share/man/de/man8/apt-config.8.gz", + "/usr/share/man/de/man8/apt-get.8.gz", + "/usr/share/man/de/man8/apt-mark.8.gz", + "/usr/share/man/de/man8/apt-secure.8.gz", + "/usr/share/man/de/man8/apt.8.gz", + "/usr/share/man/es/man5/apt_preferences.5.gz", + "/usr/share/man/es/man8/apt-cache.8.gz", + "/usr/share/man/es/man8/apt-cdrom.8.gz", + "/usr/share/man/es/man8/apt-config.8.gz", + "/usr/share/man/fr/man1/apt-transport-http.1.gz", + "/usr/share/man/fr/man1/apt-transport-https.1.gz", + "/usr/share/man/fr/man1/apt-transport-mirror.1.gz", + "/usr/share/man/fr/man5/apt.conf.5.gz", + "/usr/share/man/fr/man5/apt_auth.conf.5.gz", + "/usr/share/man/fr/man5/apt_preferences.5.gz", + "/usr/share/man/fr/man5/sources.list.5.gz", + "/usr/share/man/fr/man7/apt-patterns.7.gz", + "/usr/share/man/fr/man8/apt-cache.8.gz", + "/usr/share/man/fr/man8/apt-cdrom.8.gz", + "/usr/share/man/fr/man8/apt-config.8.gz", + "/usr/share/man/fr/man8/apt-get.8.gz", + "/usr/share/man/fr/man8/apt-mark.8.gz", + "/usr/share/man/fr/man8/apt-secure.8.gz", + "/usr/share/man/fr/man8/apt.8.gz", + "/usr/share/man/it/man5/apt.conf.5.gz", + "/usr/share/man/it/man5/apt_preferences.5.gz", + "/usr/share/man/it/man8/apt-cache.8.gz", + "/usr/share/man/it/man8/apt-cdrom.8.gz", + "/usr/share/man/it/man8/apt-config.8.gz", + "/usr/share/man/it/man8/apt-mark.8.gz", + "/usr/share/man/it/man8/apt.8.gz", + "/usr/share/man/ja/man5/apt.conf.5.gz", + "/usr/share/man/ja/man5/apt_preferences.5.gz", + "/usr/share/man/ja/man8/apt-cache.8.gz", + "/usr/share/man/ja/man8/apt-cdrom.8.gz", + "/usr/share/man/ja/man8/apt-config.8.gz", + "/usr/share/man/ja/man8/apt-mark.8.gz", + "/usr/share/man/ja/man8/apt.8.gz", + "/usr/share/man/man1/apt-transport-http.1.gz", + "/usr/share/man/man1/apt-transport-https.1.gz", + "/usr/share/man/man1/apt-transport-mirror.1.gz", + "/usr/share/man/man5/apt.conf.5.gz", + "/usr/share/man/man5/apt_auth.conf.5.gz", + "/usr/share/man/man5/apt_preferences.5.gz", + "/usr/share/man/man5/sources.list.5.gz", + "/usr/share/man/man7/apt-patterns.7.gz", + "/usr/share/man/man8/apt-cache.8.gz", + "/usr/share/man/man8/apt-cdrom.8.gz", + "/usr/share/man/man8/apt-config.8.gz", + "/usr/share/man/man8/apt-get.8.gz", + "/usr/share/man/man8/apt-mark.8.gz", + "/usr/share/man/man8/apt-secure.8.gz", + "/usr/share/man/man8/apt.8.gz", + "/usr/share/man/nl/man1/apt-transport-http.1.gz", + "/usr/share/man/nl/man1/apt-transport-https.1.gz", + "/usr/share/man/nl/man1/apt-transport-mirror.1.gz", + "/usr/share/man/nl/man5/apt.conf.5.gz", + "/usr/share/man/nl/man5/apt_auth.conf.5.gz", + "/usr/share/man/nl/man5/apt_preferences.5.gz", + "/usr/share/man/nl/man5/sources.list.5.gz", + "/usr/share/man/nl/man7/apt-patterns.7.gz", + "/usr/share/man/nl/man8/apt-cache.8.gz", + "/usr/share/man/nl/man8/apt-cdrom.8.gz", + "/usr/share/man/nl/man8/apt-config.8.gz", + "/usr/share/man/nl/man8/apt-get.8.gz", + "/usr/share/man/nl/man8/apt-mark.8.gz", + "/usr/share/man/nl/man8/apt-secure.8.gz", + "/usr/share/man/nl/man8/apt.8.gz", + "/usr/share/man/pl/man5/apt_preferences.5.gz", + "/usr/share/man/pl/man8/apt-cache.8.gz", + "/usr/share/man/pl/man8/apt-cdrom.8.gz", + "/usr/share/man/pl/man8/apt-config.8.gz", + "/usr/share/man/pt/man1/apt-transport-http.1.gz", + "/usr/share/man/pt/man1/apt-transport-https.1.gz", + "/usr/share/man/pt/man1/apt-transport-mirror.1.gz", + "/usr/share/man/pt/man5/apt.conf.5.gz", + "/usr/share/man/pt/man5/apt_auth.conf.5.gz", + "/usr/share/man/pt/man5/apt_preferences.5.gz", + "/usr/share/man/pt/man5/sources.list.5.gz", + "/usr/share/man/pt/man7/apt-patterns.7.gz", + "/usr/share/man/pt/man8/apt-cache.8.gz", + "/usr/share/man/pt/man8/apt-cdrom.8.gz", + "/usr/share/man/pt/man8/apt-config.8.gz", + "/usr/share/man/pt/man8/apt-get.8.gz", + "/usr/share/man/pt/man8/apt-mark.8.gz", + "/usr/share/man/pt/man8/apt-secure.8.gz", + "/usr/share/man/pt/man8/apt.8.gz" + ] + }, + { + "ID": "base-files@13.8+deb13u6", + "Name": "base-files", + "Identifier": { + "PURL": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64\u0026distro=debian-13.6", + "UID": "4bb4c2ef5a12c64b" + }, + "Version": "13.8+deb13u6", + "Arch": "amd64", + "SrcName": "base-files", + "SrcVersion": "13.8+deb13u6", + "Licenses": [ + "GPL-2.0-or-later", + "verbatim" + ], + "Maintainer": "Santiago Vila \u003csanvila@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/os-release", + "/usr/share/base-files/dot.bashrc", + "/usr/share/base-files/dot.profile", + "/usr/share/base-files/dot.profile.md5sums", + "/usr/share/base-files/info.dir", + "/usr/share/base-files/motd", + "/usr/share/base-files/profile", + "/usr/share/base-files/profile.md5sums", + "/usr/share/base-files/staff-group-for-usr-local", + "/usr/share/common-licenses/Apache-2.0", + "/usr/share/common-licenses/Artistic", + "/usr/share/common-licenses/BSD", + "/usr/share/common-licenses/CC0-1.0", + "/usr/share/common-licenses/GFDL-1.2", + "/usr/share/common-licenses/GFDL-1.3", + "/usr/share/common-licenses/GPL-1", + "/usr/share/common-licenses/GPL-2", + "/usr/share/common-licenses/GPL-3", + "/usr/share/common-licenses/LGPL-2", + "/usr/share/common-licenses/LGPL-2.1", + "/usr/share/common-licenses/LGPL-3", + "/usr/share/common-licenses/MPL-1.1", + "/usr/share/common-licenses/MPL-2.0", + "/usr/share/doc/base-files/NEWS.Debian.gz", + "/usr/share/doc/base-files/README", + "/usr/share/doc/base-files/README.FHS", + "/usr/share/doc/base-files/changelog.gz", + "/usr/share/doc/base-files/copyright", + "/usr/share/lintian/overrides/base-files" + ] + }, + { + "ID": "base-passwd@3.6.7", + "Name": "base-passwd", + "Identifier": { + "PURL": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64\u0026distro=debian-13.6", + "UID": "bc0cc430715927e6" + }, + "Version": "3.6.7", + "Arch": "amd64", + "SrcName": "base-passwd", + "SrcVersion": "3.6.7", + "Licenses": [ + "GPL-2.0-only", + "public-domain" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libdebconfclient0@0.280", + "libselinux1@3.8.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/sbin/update-passwd", + "/usr/share/base-passwd/group.master", + "/usr/share/base-passwd/passwd.master", + "/usr/share/doc-base/base-passwd.users-and-groups", + "/usr/share/doc/base-passwd/README", + "/usr/share/doc/base-passwd/changelog.gz", + "/usr/share/doc/base-passwd/copyright", + "/usr/share/doc/base-passwd/users-and-groups.html", + "/usr/share/doc/base-passwd/users-and-groups.txt.gz", + "/usr/share/lintian/overrides/base-passwd", + "/usr/share/man/de/man8/update-passwd.8.gz", + "/usr/share/man/es/man8/update-passwd.8.gz", + "/usr/share/man/fr/man8/update-passwd.8.gz", + "/usr/share/man/ja/man8/update-passwd.8.gz", + "/usr/share/man/man8/update-passwd.8.gz", + "/usr/share/man/pl/man8/update-passwd.8.gz", + "/usr/share/man/ro/man8/update-passwd.8.gz", + "/usr/share/man/ru/man8/update-passwd.8.gz" + ] + }, + { + "ID": "bash@5.2.37-2+b9", + "Name": "bash", + "Identifier": { + "PURL": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64\u0026distro=debian-13.6", + "UID": "2c8d7060f3972831" + }, + "Version": "5.2.37", + "Release": "2+b9", + "Arch": "amd64", + "SrcName": "bash", + "SrcVersion": "5.2.37", + "SrcRelease": "2", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "GPL-2.0-or-later", + "GPL-2.0-only", + "GFDL-1.3-no-invariants-only", + "GFDL-1.3-only", + "Latex2e", + "BSD-4-Clause-UC", + "MIT", + "permissive" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "DependsOn": [ + "base-files@13.8+deb13u6", + "debianutils@5.23.2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/bash", + "/usr/bin/bashbug", + "/usr/bin/clear_console", + "/usr/share/debianutils/shells.d/bash", + "/usr/share/doc/bash/CHANGES.gz", + "/usr/share/doc/bash/COMPAT.gz", + "/usr/share/doc/bash/INTRO.gz", + "/usr/share/doc/bash/NEWS.gz", + "/usr/share/doc/bash/POSIX.gz", + "/usr/share/doc/bash/RBASH", + "/usr/share/doc/bash/README.Debian.gz", + "/usr/share/doc/bash/README.abs-guide", + "/usr/share/doc/bash/README.commands.gz", + "/usr/share/doc/bash/README.gz", + "/usr/share/doc/bash/changelog.Debian.amd64.gz", + "/usr/share/doc/bash/changelog.Debian.gz", + "/usr/share/doc/bash/changelog.gz", + "/usr/share/doc/bash/copyright", + "/usr/share/doc/bash/inputrc.arrows", + "/usr/share/lintian/overrides/bash", + "/usr/share/locale/af/LC_MESSAGES/bash.mo", + "/usr/share/locale/bg/LC_MESSAGES/bash.mo", + "/usr/share/locale/ca/LC_MESSAGES/bash.mo", + "/usr/share/locale/cs/LC_MESSAGES/bash.mo", + "/usr/share/locale/da/LC_MESSAGES/bash.mo", + "/usr/share/locale/de/LC_MESSAGES/bash.mo", + "/usr/share/locale/el/LC_MESSAGES/bash.mo", + "/usr/share/locale/en@boldquot/LC_MESSAGES/bash.mo", + "/usr/share/locale/en@quot/LC_MESSAGES/bash.mo", + "/usr/share/locale/eo/LC_MESSAGES/bash.mo", + "/usr/share/locale/es/LC_MESSAGES/bash.mo", + "/usr/share/locale/et/LC_MESSAGES/bash.mo", + "/usr/share/locale/fi/LC_MESSAGES/bash.mo", + "/usr/share/locale/fr/LC_MESSAGES/bash.mo", + "/usr/share/locale/ga/LC_MESSAGES/bash.mo", + "/usr/share/locale/gl/LC_MESSAGES/bash.mo", + "/usr/share/locale/hr/LC_MESSAGES/bash.mo", + "/usr/share/locale/hu/LC_MESSAGES/bash.mo", + "/usr/share/locale/id/LC_MESSAGES/bash.mo", + "/usr/share/locale/it/LC_MESSAGES/bash.mo", + "/usr/share/locale/ja/LC_MESSAGES/bash.mo", + "/usr/share/locale/ko/LC_MESSAGES/bash.mo", + "/usr/share/locale/lt/LC_MESSAGES/bash.mo", + "/usr/share/locale/nb/LC_MESSAGES/bash.mo", + "/usr/share/locale/nl/LC_MESSAGES/bash.mo", + "/usr/share/locale/pl/LC_MESSAGES/bash.mo", + "/usr/share/locale/pt/LC_MESSAGES/bash.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/bash.mo", + "/usr/share/locale/ro/LC_MESSAGES/bash.mo", + "/usr/share/locale/ru/LC_MESSAGES/bash.mo", + "/usr/share/locale/sk/LC_MESSAGES/bash.mo", + "/usr/share/locale/sl/LC_MESSAGES/bash.mo", + "/usr/share/locale/sr/LC_MESSAGES/bash.mo", + "/usr/share/locale/sv/LC_MESSAGES/bash.mo", + "/usr/share/locale/tr/LC_MESSAGES/bash.mo", + "/usr/share/locale/uk/LC_MESSAGES/bash.mo", + "/usr/share/locale/vi/LC_MESSAGES/bash.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/bash.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/bash.mo", + "/usr/share/man/man1/bash.1.gz", + "/usr/share/man/man1/bashbug.1.gz", + "/usr/share/man/man1/clear_console.1.gz", + "/usr/share/man/man1/rbash.1.gz", + "/usr/share/man/man7/bash-builtins.7.gz", + "/usr/share/menu/bash" + ] + }, + { + "ID": "bsdutils@1:2.41-5", + "Name": "bsdutils", + "Identifier": { + "PURL": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "c9de60be80a96a27" + }, + "Version": "2.41", + "Release": "5", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/logger", + "/usr/bin/renice", + "/usr/bin/script", + "/usr/bin/scriptlive", + "/usr/bin/scriptreplay", + "/usr/bin/wall", + "/usr/share/bash-completion/completions/logger", + "/usr/share/bash-completion/completions/renice", + "/usr/share/bash-completion/completions/script", + "/usr/share/bash-completion/completions/scriptlive", + "/usr/share/bash-completion/completions/scriptreplay", + "/usr/share/bash-completion/completions/wall", + "/usr/share/doc/bsdutils/NEWS.Debian.gz", + "/usr/share/doc/bsdutils/changelog.Debian.gz", + "/usr/share/doc/bsdutils/changelog.gz", + "/usr/share/doc/bsdutils/copyright", + "/usr/share/lintian/overrides/bsdutils", + "/usr/share/man/man1/logger.1.gz", + "/usr/share/man/man1/renice.1.gz", + "/usr/share/man/man1/script.1.gz", + "/usr/share/man/man1/scriptlive.1.gz", + "/usr/share/man/man1/scriptreplay.1.gz", + "/usr/share/man/man1/wall.1.gz" + ] + }, + { + "ID": "ca-certificates@20250419", + "Name": "ca-certificates", + "Identifier": { + "PURL": "pkg:deb/debian/ca-certificates@20250419?arch=all\u0026distro=debian-13.6", + "UID": "6365e842529686ef" + }, + "Version": "20250419", + "Arch": "all", + "SrcName": "ca-certificates", + "SrcVersion": "20250419", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "MPL-2.0" + ], + "Maintainer": "Julien Cristau \u003cjcristau@debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91", + "openssl@3.5.6-1~deb13u2" + ], + "Layer": { + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + "InstalledFiles": [ + "/usr/sbin/update-ca-certificates", + "/usr/share/ca-certificates/mozilla/ACCVRAIZ1.crt", + "/usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM.crt", + "/usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM_SERVIDORES_SEGUROS.crt", + "/usr/share/ca-certificates/mozilla/ANF_Secure_Server_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/Actalis_Authentication_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Commercial.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Networking.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Premium.crt", + "/usr/share/ca-certificates/mozilla/AffirmTrust_Premium_ECC.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_1.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_2.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_3.crt", + "/usr/share/ca-certificates/mozilla/Amazon_Root_CA_4.crt", + "/usr/share/ca-certificates/mozilla/Atos_TrustedRoot_2011.crt", + "/usr/share/ca-certificates/mozilla/Atos_TrustedRoot_Root_CA_ECC_TLS_2021.crt", + "/usr/share/ca-certificates/mozilla/Atos_TrustedRoot_Root_CA_RSA_TLS_2021.crt", + "/usr/share/ca-certificates/mozilla/Autoridad_de_Certificacion_Firmaprofesional_CIF_A62634068.crt", + "/usr/share/ca-certificates/mozilla/BJCA_Global_Root_CA1.crt", + "/usr/share/ca-certificates/mozilla/BJCA_Global_Root_CA2.crt", + "/usr/share/ca-certificates/mozilla/Baltimore_CyberTrust_Root.crt", + "/usr/share/ca-certificates/mozilla/Buypass_Class_2_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/Buypass_Class_3_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/CA_Disig_Root_R2.crt", + "/usr/share/ca-certificates/mozilla/CFCA_EV_ROOT.crt", + "/usr/share/ca-certificates/mozilla/COMODO_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/COMODO_ECC_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/COMODO_RSA_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Certainly_Root_E1.crt", + "/usr/share/ca-certificates/mozilla/Certainly_Root_R1.crt", + "/usr/share/ca-certificates/mozilla/Certigna.crt", + "/usr/share/ca-certificates/mozilla/Certigna_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/Certum_EC-384_CA.crt", + "/usr/share/ca-certificates/mozilla/Certum_Trusted_Network_CA.crt", + "/usr/share/ca-certificates/mozilla/Certum_Trusted_Network_CA_2.crt", + "/usr/share/ca-certificates/mozilla/Certum_Trusted_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_ECC_Root-01.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_ECC_Root-02.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_RSA_Root-01.crt", + "/usr/share/ca-certificates/mozilla/CommScope_Public_Trust_RSA_Root-02.crt", + "/usr/share/ca-certificates/mozilla/Comodo_AAA_Services_root.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_BR_Root_CA_1_2020.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_BR_Root_CA_2_2023.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_EV_Root_CA_1_2020.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_EV_Root_CA_2_2023.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_Root_Class_3_CA_2_2009.crt", + "/usr/share/ca-certificates/mozilla/D-TRUST_Root_Class_3_CA_2_EV_2009.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Assured_ID_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Assured_ID_Root_G2.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Assured_ID_Root_G3.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_G2.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_G3.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_High_Assurance_EV_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_TLS_ECC_P384_Root_G5.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_TLS_RSA4096_Root_G5.crt", + "/usr/share/ca-certificates/mozilla/DigiCert_Trusted_Root_G4.crt", + "/usr/share/ca-certificates/mozilla/Entrust.net_Premium_2048_Secure_Server_CA.crt", + "/usr/share/ca-certificates/mozilla/Entrust_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Entrust_Root_Certification_Authority_-_EC1.crt", + "/usr/share/ca-certificates/mozilla/Entrust_Root_Certification_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/FIRMAPROFESIONAL_CA_ROOT-A_WEB.crt", + "/usr/share/ca-certificates/mozilla/GDCA_TrustAUTH_R5_ROOT.crt", + "/usr/share/ca-certificates/mozilla/GLOBALTRUST_2020.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R1.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R2.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R3.crt", + "/usr/share/ca-certificates/mozilla/GTS_Root_R4.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_ECC_Root_CA_-_R4.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_ECC_Root_CA_-_R5.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R3.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R6.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_E46.crt", + "/usr/share/ca-certificates/mozilla/GlobalSign_Root_R46.crt", + "/usr/share/ca-certificates/mozilla/Go_Daddy_Class_2_CA.crt", + "/usr/share/ca-certificates/mozilla/Go_Daddy_Root_Certificate_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/HARICA_TLS_ECC_Root_CA_2021.crt", + "/usr/share/ca-certificates/mozilla/HARICA_TLS_RSA_Root_CA_2021.crt", + "/usr/share/ca-certificates/mozilla/Hellenic_Academic_and_Research_Institutions_ECC_RootCA_2015.crt", + "/usr/share/ca-certificates/mozilla/Hellenic_Academic_and_Research_Institutions_RootCA_2015.crt", + "/usr/share/ca-certificates/mozilla/HiPKI_Root_CA_-_G1.crt", + "/usr/share/ca-certificates/mozilla/Hongkong_Post_Root_CA_3.crt", + "/usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt", + "/usr/share/ca-certificates/mozilla/ISRG_Root_X2.crt", + "/usr/share/ca-certificates/mozilla/IdenTrust_Commercial_Root_CA_1.crt", + "/usr/share/ca-certificates/mozilla/IdenTrust_Public_Sector_Root_CA_1.crt", + "/usr/share/ca-certificates/mozilla/Izenpe.com.crt", + "/usr/share/ca-certificates/mozilla/Microsec_e-Szigno_Root_CA_2009.crt", + "/usr/share/ca-certificates/mozilla/Microsoft_ECC_Root_Certificate_Authority_2017.crt", + "/usr/share/ca-certificates/mozilla/Microsoft_RSA_Root_Certificate_Authority_2017.crt", + "/usr/share/ca-certificates/mozilla/NAVER_Global_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/NetLock_Arany_=Class_Gold=_Főtanúsítvány.crt", + "/usr/share/ca-certificates/mozilla/OISTE_WISeKey_Global_Root_GB_CA.crt", + "/usr/share/ca-certificates/mozilla/OISTE_WISeKey_Global_Root_GC_CA.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_1_G3.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_2.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_2_G3.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_3.crt", + "/usr/share/ca-certificates/mozilla/QuoVadis_Root_CA_3_G3.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_EV_Root_Certification_Authority_ECC.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_EV_Root_Certification_Authority_RSA_R2.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_Root_Certification_Authority_ECC.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_Root_Certification_Authority_RSA.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_TLS_ECC_Root_CA_2022.crt", + "/usr/share/ca-certificates/mozilla/SSL.com_TLS_RSA_Root_CA_2022.crt", + "/usr/share/ca-certificates/mozilla/SZAFIR_ROOT_CA2.crt", + "/usr/share/ca-certificates/mozilla/Sectigo_Public_Server_Authentication_Root_E46.crt", + "/usr/share/ca-certificates/mozilla/Sectigo_Public_Server_Authentication_Root_R46.crt", + "/usr/share/ca-certificates/mozilla/SecureSign_Root_CA12.crt", + "/usr/share/ca-certificates/mozilla/SecureSign_Root_CA14.crt", + "/usr/share/ca-certificates/mozilla/SecureSign_Root_CA15.crt", + "/usr/share/ca-certificates/mozilla/SecureTrust_CA.crt", + "/usr/share/ca-certificates/mozilla/Secure_Global_CA.crt", + "/usr/share/ca-certificates/mozilla/Security_Communication_ECC_RootCA1.crt", + "/usr/share/ca-certificates/mozilla/Security_Communication_RootCA2.crt", + "/usr/share/ca-certificates/mozilla/Starfield_Class_2_CA.crt", + "/usr/share/ca-certificates/mozilla/Starfield_Root_Certificate_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/Starfield_Services_Root_Certificate_Authority_-_G2.crt", + "/usr/share/ca-certificates/mozilla/SwissSign_Gold_CA_-_G2.crt", + "/usr/share/ca-certificates/mozilla/T-TeleSec_GlobalRoot_Class_2.crt", + "/usr/share/ca-certificates/mozilla/T-TeleSec_GlobalRoot_Class_3.crt", + "/usr/share/ca-certificates/mozilla/TUBITAK_Kamu_SM_SSL_Kok_Sertifikasi_-_Surum_1.crt", + "/usr/share/ca-certificates/mozilla/TWCA_CYBER_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/TWCA_Global_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/TWCA_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Telekom_Security_TLS_ECC_Root_2020.crt", + "/usr/share/ca-certificates/mozilla/Telekom_Security_TLS_RSA_Root_2023.crt", + "/usr/share/ca-certificates/mozilla/TeliaSonera_Root_CA_v1.crt", + "/usr/share/ca-certificates/mozilla/Telia_Root_CA_v2.crt", + "/usr/share/ca-certificates/mozilla/TrustAsia_Global_Root_CA_G3.crt", + "/usr/share/ca-certificates/mozilla/TrustAsia_Global_Root_CA_G4.crt", + "/usr/share/ca-certificates/mozilla/Trustwave_Global_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Trustwave_Global_ECC_P256_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/Trustwave_Global_ECC_P384_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/TunTrust_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/UCA_Extended_Validation_Root.crt", + "/usr/share/ca-certificates/mozilla/UCA_Global_G2_Root.crt", + "/usr/share/ca-certificates/mozilla/USERTrust_ECC_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/USERTrust_RSA_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/XRamp_Global_CA_Root.crt", + "/usr/share/ca-certificates/mozilla/certSIGN_ROOT_CA.crt", + "/usr/share/ca-certificates/mozilla/certSIGN_Root_CA_G2.crt", + "/usr/share/ca-certificates/mozilla/e-Szigno_Root_CA_2017.crt", + "/usr/share/ca-certificates/mozilla/ePKI_Root_Certification_Authority.crt", + "/usr/share/ca-certificates/mozilla/emSign_ECC_Root_CA_-_C3.crt", + "/usr/share/ca-certificates/mozilla/emSign_ECC_Root_CA_-_G3.crt", + "/usr/share/ca-certificates/mozilla/emSign_Root_CA_-_C1.crt", + "/usr/share/ca-certificates/mozilla/emSign_Root_CA_-_G1.crt", + "/usr/share/ca-certificates/mozilla/vTrus_ECC_Root_CA.crt", + "/usr/share/ca-certificates/mozilla/vTrus_Root_CA.crt", + "/usr/share/doc/ca-certificates/README.Debian", + "/usr/share/doc/ca-certificates/changelog.gz", + "/usr/share/doc/ca-certificates/copyright", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/Makefile", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/README", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/ca-certificates-local.triggers", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/changelog", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/compat", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/control", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/copyright", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/postrm", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/rules", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/source/format", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/local/Local_Root_CA.crt", + "/usr/share/doc/ca-certificates/examples/ca-certificates-local/local/Makefile", + "/usr/share/man/man8/update-ca-certificates.8.gz" + ] + }, + { + "ID": "coreutils@9.7-3", + "Name": "coreutils", + "Identifier": { + "PURL": "pkg:deb/debian/coreutils@9.7-3?arch=amd64\u0026distro=debian-13.6", + "UID": "a90cbdbcbab1768e" + }, + "Version": "9.7", + "Release": "3", + "Arch": "amd64", + "SrcName": "coreutils", + "SrcVersion": "9.7", + "SrcRelease": "3", + "Licenses": [ + "GPL-3.0-or-later", + "BSD-4-Clause-UC", + "GPL-3.0-only", + "ISC", + "FSFULLR", + "GFDL-1.3-no-invariants-only", + "GFDL-1.3-only" + ], + "Maintainer": "Michael Stone \u003cmstone@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/[", + "/usr/bin/arch", + "/usr/bin/b2sum", + "/usr/bin/base32", + "/usr/bin/base64", + "/usr/bin/basename", + "/usr/bin/basenc", + "/usr/bin/cat", + "/usr/bin/chcon", + "/usr/bin/chgrp", + "/usr/bin/chmod", + "/usr/bin/chown", + "/usr/bin/cksum", + "/usr/bin/comm", + "/usr/bin/cp", + "/usr/bin/csplit", + "/usr/bin/cut", + "/usr/bin/date", + "/usr/bin/dd", + "/usr/bin/df", + "/usr/bin/dir", + "/usr/bin/dircolors", + "/usr/bin/dirname", + "/usr/bin/du", + "/usr/bin/echo", + "/usr/bin/env", + "/usr/bin/expand", + "/usr/bin/expr", + "/usr/bin/factor", + "/usr/bin/false", + "/usr/bin/fmt", + "/usr/bin/fold", + "/usr/bin/groups", + "/usr/bin/head", + "/usr/bin/hostid", + "/usr/bin/id", + "/usr/bin/install", + "/usr/bin/join", + "/usr/bin/link", + "/usr/bin/ln", + "/usr/bin/logname", + "/usr/bin/ls", + "/usr/bin/md5sum", + "/usr/bin/mkdir", + "/usr/bin/mkfifo", + "/usr/bin/mknod", + "/usr/bin/mktemp", + "/usr/bin/mv", + "/usr/bin/nice", + "/usr/bin/nl", + "/usr/bin/nohup", + "/usr/bin/nproc", + "/usr/bin/numfmt", + "/usr/bin/od", + "/usr/bin/paste", + "/usr/bin/pathchk", + "/usr/bin/pinky", + "/usr/bin/pr", + "/usr/bin/printenv", + "/usr/bin/printf", + "/usr/bin/ptx", + "/usr/bin/pwd", + "/usr/bin/readlink", + "/usr/bin/realpath", + "/usr/bin/rm", + "/usr/bin/rmdir", + "/usr/bin/runcon", + "/usr/bin/seq", + "/usr/bin/sha1sum", + "/usr/bin/sha224sum", + "/usr/bin/sha256sum", + "/usr/bin/sha384sum", + "/usr/bin/sha512sum", + "/usr/bin/shred", + "/usr/bin/shuf", + "/usr/bin/sleep", + "/usr/bin/sort", + "/usr/bin/split", + "/usr/bin/stat", + "/usr/bin/stdbuf", + "/usr/bin/stty", + "/usr/bin/sum", + "/usr/bin/sync", + "/usr/bin/tac", + "/usr/bin/tail", + "/usr/bin/tee", + "/usr/bin/test", + "/usr/bin/timeout", + "/usr/bin/touch", + "/usr/bin/tr", + "/usr/bin/true", + "/usr/bin/truncate", + "/usr/bin/tsort", + "/usr/bin/tty", + "/usr/bin/uname", + "/usr/bin/unexpand", + "/usr/bin/uniq", + "/usr/bin/unlink", + "/usr/bin/users", + "/usr/bin/vdir", + "/usr/bin/wc", + "/usr/bin/who", + "/usr/bin/whoami", + "/usr/bin/yes", + "/usr/libexec/coreutils/libstdbuf.so", + "/usr/sbin/chroot", + "/usr/share/doc/coreutils/AUTHORS", + "/usr/share/doc/coreutils/NEWS.gz", + "/usr/share/doc/coreutils/README.Debian", + "/usr/share/doc/coreutils/README.gz", + "/usr/share/doc/coreutils/THANKS.gz", + "/usr/share/doc/coreutils/TODO.gz", + "/usr/share/doc/coreutils/changelog.Debian.gz", + "/usr/share/doc/coreutils/changelog.gz", + "/usr/share/doc/coreutils/copyright", + "/usr/share/info/coreutils.info.gz", + "/usr/share/lintian/overrides/coreutils", + "/usr/share/locale/af/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/be/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/bg/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ca/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/cs/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/da/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/de/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/el/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/eo/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/es/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/et/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/eu/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/fi/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/fr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ga/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/gl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/hr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/hu/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ia/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/id/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/it/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ja/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ka/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/kk/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ko/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/lg/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/lt/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ms/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/nb/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/nl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/pl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/pt/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ro/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ru/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sk/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sl/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/sv/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/ta/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/tr/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/uk/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/vi/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/coreutils.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/coreutils.mo", + "/usr/share/man/man1/arch.1.gz", + "/usr/share/man/man1/b2sum.1.gz", + "/usr/share/man/man1/base32.1.gz", + "/usr/share/man/man1/base64.1.gz", + "/usr/share/man/man1/basename.1.gz", + "/usr/share/man/man1/basenc.1.gz", + "/usr/share/man/man1/cat.1.gz", + "/usr/share/man/man1/chcon.1.gz", + "/usr/share/man/man1/chgrp.1.gz", + "/usr/share/man/man1/chmod.1.gz", + "/usr/share/man/man1/chown.1.gz", + "/usr/share/man/man1/cksum.1.gz", + "/usr/share/man/man1/comm.1.gz", + "/usr/share/man/man1/cp.1.gz", + "/usr/share/man/man1/csplit.1.gz", + "/usr/share/man/man1/cut.1.gz", + "/usr/share/man/man1/date.1.gz", + "/usr/share/man/man1/dd.1.gz", + "/usr/share/man/man1/df.1.gz", + "/usr/share/man/man1/dir.1.gz", + "/usr/share/man/man1/dircolors.1.gz", + "/usr/share/man/man1/dirname.1.gz", + "/usr/share/man/man1/du.1.gz", + "/usr/share/man/man1/echo.1.gz", + "/usr/share/man/man1/env.1.gz", + "/usr/share/man/man1/expand.1.gz", + "/usr/share/man/man1/expr.1.gz", + "/usr/share/man/man1/factor.1.gz", + "/usr/share/man/man1/false.1.gz", + "/usr/share/man/man1/fmt.1.gz", + "/usr/share/man/man1/fold.1.gz", + "/usr/share/man/man1/groups.1.gz", + "/usr/share/man/man1/head.1.gz", + "/usr/share/man/man1/hostid.1.gz", + "/usr/share/man/man1/id.1.gz", + "/usr/share/man/man1/install.1.gz", + "/usr/share/man/man1/join.1.gz", + "/usr/share/man/man1/link.1.gz", + "/usr/share/man/man1/ln.1.gz", + "/usr/share/man/man1/logname.1.gz", + "/usr/share/man/man1/ls.1.gz", + "/usr/share/man/man1/md5sum.1.gz", + "/usr/share/man/man1/mkdir.1.gz", + "/usr/share/man/man1/mkfifo.1.gz", + "/usr/share/man/man1/mknod.1.gz", + "/usr/share/man/man1/mktemp.1.gz", + "/usr/share/man/man1/mv.1.gz", + "/usr/share/man/man1/nice.1.gz", + "/usr/share/man/man1/nl.1.gz", + "/usr/share/man/man1/nohup.1.gz", + "/usr/share/man/man1/nproc.1.gz", + "/usr/share/man/man1/numfmt.1.gz", + "/usr/share/man/man1/od.1.gz", + "/usr/share/man/man1/paste.1.gz", + "/usr/share/man/man1/pathchk.1.gz", + "/usr/share/man/man1/pinky.1.gz", + "/usr/share/man/man1/pr.1.gz", + "/usr/share/man/man1/printenv.1.gz", + "/usr/share/man/man1/printf.1.gz", + "/usr/share/man/man1/ptx.1.gz", + "/usr/share/man/man1/pwd.1.gz", + "/usr/share/man/man1/readlink.1.gz", + "/usr/share/man/man1/realpath.1.gz", + "/usr/share/man/man1/rm.1.gz", + "/usr/share/man/man1/rmdir.1.gz", + "/usr/share/man/man1/runcon.1.gz", + "/usr/share/man/man1/seq.1.gz", + "/usr/share/man/man1/sha1sum.1.gz", + "/usr/share/man/man1/sha224sum.1.gz", + "/usr/share/man/man1/sha256sum.1.gz", + "/usr/share/man/man1/sha384sum.1.gz", + "/usr/share/man/man1/sha512sum.1.gz", + "/usr/share/man/man1/shred.1.gz", + "/usr/share/man/man1/shuf.1.gz", + "/usr/share/man/man1/sleep.1.gz", + "/usr/share/man/man1/sort.1.gz", + "/usr/share/man/man1/split.1.gz", + "/usr/share/man/man1/stat.1.gz", + "/usr/share/man/man1/stdbuf.1.gz", + "/usr/share/man/man1/stty.1.gz", + "/usr/share/man/man1/sum.1.gz", + "/usr/share/man/man1/sync.1.gz", + "/usr/share/man/man1/tac.1.gz", + "/usr/share/man/man1/tail.1.gz", + "/usr/share/man/man1/tee.1.gz", + "/usr/share/man/man1/test.1.gz", + "/usr/share/man/man1/timeout.1.gz", + "/usr/share/man/man1/touch.1.gz", + "/usr/share/man/man1/tr.1.gz", + "/usr/share/man/man1/true.1.gz", + "/usr/share/man/man1/truncate.1.gz", + "/usr/share/man/man1/tsort.1.gz", + "/usr/share/man/man1/tty.1.gz", + "/usr/share/man/man1/uname.1.gz", + "/usr/share/man/man1/unexpand.1.gz", + "/usr/share/man/man1/uniq.1.gz", + "/usr/share/man/man1/unlink.1.gz", + "/usr/share/man/man1/users.1.gz", + "/usr/share/man/man1/vdir.1.gz", + "/usr/share/man/man1/wc.1.gz", + "/usr/share/man/man1/who.1.gz", + "/usr/share/man/man1/whoami.1.gz", + "/usr/share/man/man1/yes.1.gz", + "/usr/share/man/man8/chroot.8.gz" + ] + }, + { + "ID": "cyrus-sasl-lib@2.1.26-24.el7_9", + "Name": "cyrus-sasl-lib", + "Identifier": { + "PURL": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9", + "UID": "b814395bf7062155", + "BOMRef": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2" + }, + "Version": "2.1.26", + "Release": "24.el7_9", + "SrcName": "cyrus-sasl-lib", + "SrcVersion": "2.1.26", + "SrcRelease": "24.el7_9", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "dash@0.5.12-12", + "Name": "dash", + "Identifier": { + "PURL": "pkg:deb/debian/dash@0.5.12-12?arch=amd64\u0026distro=debian-13.6", + "UID": "89c835b0985cdc5c" + }, + "Version": "0.5.12", + "Release": "12", + "Arch": "amd64", + "SrcName": "dash", + "SrcVersion": "0.5.12", + "SrcRelease": "12", + "Licenses": [ + "BSD-3-Clause", + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Andrej Shadura \u003candrewsh@debian.org\u003e", + "DependsOn": [ + "debianutils@5.23.2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/dash", + "/usr/share/debianutils/shells.d/dash", + "/usr/share/doc/dash/README.Debian.diet", + "/usr/share/doc/dash/README.source", + "/usr/share/doc/dash/changelog.Debian.gz", + "/usr/share/doc/dash/changelog.gz", + "/usr/share/doc/dash/copyright", + "/usr/share/lintian/overrides/dash", + "/usr/share/man/man1/dash.1.gz", + "/usr/share/menu/dash" + ] + }, + { + "ID": "debconf@1.5.91", + "Name": "debconf", + "Identifier": { + "PURL": "pkg:deb/debian/debconf@1.5.91?arch=all\u0026distro=debian-13.6", + "UID": "dbd74d1c32616a65" + }, + "Version": "1.5.91", + "Arch": "all", + "SrcName": "debconf", + "SrcVersion": "1.5.91", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "Debconf Developers \u003cdebconf-devel@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/debconf", + "/usr/bin/debconf-apt-progress", + "/usr/bin/debconf-communicate", + "/usr/bin/debconf-copydb", + "/usr/bin/debconf-escape", + "/usr/bin/debconf-set-selections", + "/usr/bin/debconf-show", + "/usr/sbin/dpkg-preconfigure", + "/usr/sbin/dpkg-reconfigure", + "/usr/share/bash-completion/completions/debconf", + "/usr/share/debconf/confmodule", + "/usr/share/debconf/confmodule.sh", + "/usr/share/debconf/debconf.conf", + "/usr/share/debconf/fix_db.pl", + "/usr/share/debconf/frontend", + "/usr/share/doc/debconf/README.Debian", + "/usr/share/doc/debconf/changelog.gz", + "/usr/share/doc/debconf/copyright", + "/usr/share/lintian/overrides/debconf", + "/usr/share/man/man1/debconf-apt-progress.1.gz", + "/usr/share/man/man1/debconf-communicate.1.gz", + "/usr/share/man/man1/debconf-copydb.1.gz", + "/usr/share/man/man1/debconf-escape.1.gz", + "/usr/share/man/man1/debconf-set-selections.1.gz", + "/usr/share/man/man1/debconf-show.1.gz", + "/usr/share/man/man1/debconf.1.gz", + "/usr/share/man/man8/dpkg-preconfigure.8.gz", + "/usr/share/man/man8/dpkg-reconfigure.8.gz", + "/usr/share/perl5/Debconf/AutoSelect.pm", + "/usr/share/perl5/Debconf/Base.pm", + "/usr/share/perl5/Debconf/Client/ConfModule.pm", + "/usr/share/perl5/Debconf/ConfModule.pm", + "/usr/share/perl5/Debconf/Config.pm", + "/usr/share/perl5/Debconf/Db.pm", + "/usr/share/perl5/Debconf/DbDriver.pm", + "/usr/share/perl5/Debconf/DbDriver/Backup.pm", + "/usr/share/perl5/Debconf/DbDriver/Cache.pm", + "/usr/share/perl5/Debconf/DbDriver/Copy.pm", + "/usr/share/perl5/Debconf/DbDriver/Debug.pm", + "/usr/share/perl5/Debconf/DbDriver/DirTree.pm", + "/usr/share/perl5/Debconf/DbDriver/Directory.pm", + "/usr/share/perl5/Debconf/DbDriver/File.pm", + "/usr/share/perl5/Debconf/DbDriver/LDAP.pm", + "/usr/share/perl5/Debconf/DbDriver/PackageDir.pm", + "/usr/share/perl5/Debconf/DbDriver/Pipe.pm", + "/usr/share/perl5/Debconf/DbDriver/Stack.pm", + "/usr/share/perl5/Debconf/Element.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Error.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Note.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Password.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Progress.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Select.pm", + "/usr/share/perl5/Debconf/Element/Dialog/String.pm", + "/usr/share/perl5/Debconf/Element/Dialog/Text.pm", + "/usr/share/perl5/Debconf/Element/Editor/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Editor/Error.pm", + "/usr/share/perl5/Debconf/Element/Editor/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Editor/Note.pm", + "/usr/share/perl5/Debconf/Element/Editor/Password.pm", + "/usr/share/perl5/Debconf/Element/Editor/Progress.pm", + "/usr/share/perl5/Debconf/Element/Editor/Select.pm", + "/usr/share/perl5/Debconf/Element/Editor/String.pm", + "/usr/share/perl5/Debconf/Element/Editor/Text.pm", + "/usr/share/perl5/Debconf/Element/Gnome.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Error.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Note.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Password.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Progress.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Select.pm", + "/usr/share/perl5/Debconf/Element/Gnome/String.pm", + "/usr/share/perl5/Debconf/Element/Gnome/Text.pm", + "/usr/share/perl5/Debconf/Element/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Error.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Note.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Password.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Progress.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Select.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/String.pm", + "/usr/share/perl5/Debconf/Element/Noninteractive/Text.pm", + "/usr/share/perl5/Debconf/Element/Select.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Error.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Note.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Password.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Progress.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Select.pm", + "/usr/share/perl5/Debconf/Element/Teletype/String.pm", + "/usr/share/perl5/Debconf/Element/Teletype/Text.pm", + "/usr/share/perl5/Debconf/Element/Web/Boolean.pm", + "/usr/share/perl5/Debconf/Element/Web/Error.pm", + "/usr/share/perl5/Debconf/Element/Web/Multiselect.pm", + "/usr/share/perl5/Debconf/Element/Web/Note.pm", + "/usr/share/perl5/Debconf/Element/Web/Password.pm", + "/usr/share/perl5/Debconf/Element/Web/Progress.pm", + "/usr/share/perl5/Debconf/Element/Web/Select.pm", + "/usr/share/perl5/Debconf/Element/Web/String.pm", + "/usr/share/perl5/Debconf/Element/Web/Text.pm", + "/usr/share/perl5/Debconf/Encoding.pm", + "/usr/share/perl5/Debconf/Format.pm", + "/usr/share/perl5/Debconf/Format/822.pm", + "/usr/share/perl5/Debconf/FrontEnd.pm", + "/usr/share/perl5/Debconf/FrontEnd/Dialog.pm", + "/usr/share/perl5/Debconf/FrontEnd/Editor.pm", + "/usr/share/perl5/Debconf/FrontEnd/Gnome.pm", + "/usr/share/perl5/Debconf/FrontEnd/Kde.pm", + "/usr/share/perl5/Debconf/FrontEnd/Noninteractive.pm", + "/usr/share/perl5/Debconf/FrontEnd/Passthrough.pm", + "/usr/share/perl5/Debconf/FrontEnd/Readline.pm", + "/usr/share/perl5/Debconf/FrontEnd/ScreenSize.pm", + "/usr/share/perl5/Debconf/FrontEnd/Teletype.pm", + "/usr/share/perl5/Debconf/FrontEnd/Text.pm", + "/usr/share/perl5/Debconf/FrontEnd/Web.pm", + "/usr/share/perl5/Debconf/Gettext.pm", + "/usr/share/perl5/Debconf/Iterator.pm", + "/usr/share/perl5/Debconf/Log.pm", + "/usr/share/perl5/Debconf/Path.pm", + "/usr/share/perl5/Debconf/Priority.pm", + "/usr/share/perl5/Debconf/Question.pm", + "/usr/share/perl5/Debconf/Template.pm", + "/usr/share/perl5/Debconf/Template/Transient.pm", + "/usr/share/perl5/Debconf/TmpFile.pm", + "/usr/share/perl5/Debian/DebConf/Client/ConfModule.pm", + "/usr/share/pixmaps/debian-logo.png" + ] + }, + { + "ID": "debian-archive-keyring@2025.1", + "Name": "debian-archive-keyring", + "Identifier": { + "PURL": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all\u0026distro=debian-13.6", + "UID": "3d23f3bc34b84a13" + }, + "Version": "2025.1", + "Arch": "all", + "SrcName": "debian-archive-keyring", + "SrcVersion": "2025.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Debian Release Team \u003cpackages@release.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/debian-archive-keyring/NEWS.Debian.gz", + "/usr/share/doc/debian-archive-keyring/README", + "/usr/share/doc/debian-archive-keyring/changelog.gz", + "/usr/share/doc/debian-archive-keyring/copyright", + "/usr/share/keyrings/debian-archive-bookworm-automatic.pgp", + "/usr/share/keyrings/debian-archive-bookworm-security-automatic.pgp", + "/usr/share/keyrings/debian-archive-bookworm-stable.pgp", + "/usr/share/keyrings/debian-archive-bullseye-automatic.pgp", + "/usr/share/keyrings/debian-archive-bullseye-security-automatic.pgp", + "/usr/share/keyrings/debian-archive-bullseye-stable.pgp", + "/usr/share/keyrings/debian-archive-keyring.pgp", + "/usr/share/keyrings/debian-archive-removed-keys.pgp", + "/usr/share/keyrings/debian-archive-trixie-automatic.pgp", + "/usr/share/keyrings/debian-archive-trixie-security-automatic.pgp", + "/usr/share/keyrings/debian-archive-trixie-stable.pgp" + ] + }, + { + "ID": "debianutils@5.23.2", + "Name": "debianutils", + "Identifier": { + "PURL": "pkg:deb/debian/debianutils@5.23.2?arch=amd64\u0026distro=debian-13.6", + "UID": "1faaee83f4beb2af" + }, + "Version": "5.23.2", + "Arch": "amd64", + "SrcName": "debianutils", + "SrcVersion": "5.23.2", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "SMAIL-GPL" + ], + "Maintainer": "Ileana Dumitrescu \u003cileanadumitrescu95@gmail.com\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/ischroot", + "/usr/bin/run-parts", + "/usr/bin/savelog", + "/usr/bin/tempfile", + "/usr/bin/which.debianutils", + "/usr/sbin/add-shell", + "/usr/sbin/installkernel", + "/usr/sbin/remove-shell", + "/usr/sbin/update-shells", + "/usr/share/debianutils/shells", + "/usr/share/doc/debianutils/README.shells", + "/usr/share/doc/debianutils/changelog.gz", + "/usr/share/doc/debianutils/copyright", + "/usr/share/man/de/man1/which.debianutils.1.gz", + "/usr/share/man/de/man8/add-shell.8.gz", + "/usr/share/man/de/man8/installkernel.8.gz", + "/usr/share/man/de/man8/remove-shell.8.gz", + "/usr/share/man/de/man8/run-parts.8.gz", + "/usr/share/man/de/man8/savelog.8.gz", + "/usr/share/man/es/man1/which.debianutils.1.gz", + "/usr/share/man/es/man8/add-shell.8.gz", + "/usr/share/man/es/man8/installkernel.8.gz", + "/usr/share/man/es/man8/remove-shell.8.gz", + "/usr/share/man/es/man8/run-parts.8.gz", + "/usr/share/man/es/man8/savelog.8.gz", + "/usr/share/man/fr/man1/which.debianutils.1.gz", + "/usr/share/man/fr/man8/add-shell.8.gz", + "/usr/share/man/fr/man8/installkernel.8.gz", + "/usr/share/man/fr/man8/remove-shell.8.gz", + "/usr/share/man/fr/man8/run-parts.8.gz", + "/usr/share/man/fr/man8/savelog.8.gz", + "/usr/share/man/it/man1/which.debianutils.1.gz", + "/usr/share/man/it/man8/add-shell.8.gz", + "/usr/share/man/it/man8/installkernel.8.gz", + "/usr/share/man/it/man8/remove-shell.8.gz", + "/usr/share/man/it/man8/run-parts.8.gz", + "/usr/share/man/it/man8/savelog.8.gz", + "/usr/share/man/ja/man1/which.debianutils.1.gz", + "/usr/share/man/ja/man8/add-shell.8.gz", + "/usr/share/man/ja/man8/installkernel.8.gz", + "/usr/share/man/ja/man8/remove-shell.8.gz", + "/usr/share/man/ja/man8/run-parts.8.gz", + "/usr/share/man/ja/man8/savelog.8.gz", + "/usr/share/man/man1/ischroot.1.gz", + "/usr/share/man/man1/tempfile.1.gz", + "/usr/share/man/man1/which.debianutils.1.gz", + "/usr/share/man/man8/add-shell.8.gz", + "/usr/share/man/man8/installkernel.8.gz", + "/usr/share/man/man8/remove-shell.8.gz", + "/usr/share/man/man8/run-parts.8.gz", + "/usr/share/man/man8/savelog.8.gz", + "/usr/share/man/man8/update-shells.8.gz", + "/usr/share/man/pl/man1/which.debianutils.1.gz", + "/usr/share/man/pl/man8/add-shell.8.gz", + "/usr/share/man/pl/man8/installkernel.8.gz", + "/usr/share/man/pl/man8/remove-shell.8.gz", + "/usr/share/man/pl/man8/run-parts.8.gz", + "/usr/share/man/pl/man8/savelog.8.gz", + "/usr/share/man/pt/man1/which.debianutils.1.gz", + "/usr/share/man/pt/man8/add-shell.8.gz", + "/usr/share/man/pt/man8/installkernel.8.gz", + "/usr/share/man/pt/man8/remove-shell.8.gz", + "/usr/share/man/pt/man8/run-parts.8.gz", + "/usr/share/man/pt/man8/savelog.8.gz", + "/usr/share/man/sl/man1/which.debianutils.1.gz", + "/usr/share/man/sl/man8/add-shell.8.gz", + "/usr/share/man/sl/man8/installkernel.8.gz", + "/usr/share/man/sl/man8/remove-shell.8.gz", + "/usr/share/man/sl/man8/run-parts.8.gz", + "/usr/share/man/sl/man8/savelog.8.gz" + ] + }, + { + "ID": "diffutils@1:3.10-4", + "Name": "diffutils", + "Identifier": { + "PURL": "pkg:deb/debian/diffutils@3.10-4?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "6ae1b70a720e3ebb" + }, + "Version": "3.10", + "Release": "4", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "diffutils", + "SrcVersion": "3.10", + "SrcRelease": "4", + "SrcEpoch": 1, + "Licenses": [ + "GPL-3.0-or-later", + "FSFULLR", + "LGPL-2.1-or-later", + "GPL-3.0-with-autoconf-exception+", + "GPL-3.0-only", + "GPL-3+ with texinfo exception", + "LGPL-2.0-or-later", + "GPL-2.0-or-later", + "X11", + "FSFAP", + "GFDL-1.3-no-invariants-only", + "LGPL-3.0-or-later", + "LGPL-3.0-only", + "public-domain", + "LGPL-2.0-only", + "LGPL-2.1-only", + "GPL-2.0-only", + "GFDL-1.3-only" + ], + "Maintainer": "Santiago Vila \u003csanvila@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/cmp", + "/usr/bin/diff", + "/usr/bin/diff3", + "/usr/bin/sdiff", + "/usr/share/doc/diffutils/NEWS.gz", + "/usr/share/doc/diffutils/changelog.Debian.gz", + "/usr/share/doc/diffutils/changelog.gz", + "/usr/share/doc/diffutils/copyright", + "/usr/share/info/diffutils.info.gz", + "/usr/share/locale/bg/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ca/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/cs/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/da/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/de/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/el/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/eo/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/es/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/fi/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/fr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ga/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/gl/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/he/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/hr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/hu/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/id/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/it/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ja/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ka/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ko/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/lv/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ms/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/nb/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/nl/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/pl/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/pt/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ro/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/ru/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/sr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/sv/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/tr/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/uk/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/vi/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/diffutils.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/diffutils.mo", + "/usr/share/man/man1/cmp.1.gz", + "/usr/share/man/man1/diff.1.gz", + "/usr/share/man/man1/diff3.1.gz", + "/usr/share/man/man1/sdiff.1.gz" + ] + }, + { + "ID": "dpkg@1.22.22", + "Name": "dpkg", + "Identifier": { + "PURL": "pkg:deb/debian/dpkg@1.22.22?arch=amd64\u0026distro=debian-13.6", + "UID": "d88bc872d04d38e8" + }, + "Version": "1.22.22", + "Arch": "amd64", + "SrcName": "dpkg", + "SrcVersion": "1.22.22", + "Licenses": [ + "GPL-2.0-or-later", + "public-domain-s-s-d", + "GPL-2.0-only" + ], + "Maintainer": "Dpkg Developers \u003cdebian-dpkg@lists.debian.org\u003e", + "DependsOn": [ + "tar@1.35+dfsg-3.1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/dpkg", + "/usr/bin/dpkg-deb", + "/usr/bin/dpkg-divert", + "/usr/bin/dpkg-maintscript-helper", + "/usr/bin/dpkg-query", + "/usr/bin/dpkg-realpath", + "/usr/bin/dpkg-split", + "/usr/bin/dpkg-statoverride", + "/usr/bin/dpkg-trigger", + "/usr/bin/update-alternatives", + "/usr/lib/systemd/system/dpkg-db-backup.service", + "/usr/lib/systemd/system/dpkg-db-backup.timer", + "/usr/libexec/dpkg/dpkg-db-backup", + "/usr/libexec/dpkg/dpkg-db-keeper", + "/usr/sbin/start-stop-daemon", + "/usr/share/doc/dpkg/AUTHORS", + "/usr/share/doc/dpkg/README.api", + "/usr/share/doc/dpkg/README.bug-usertags.gz", + "/usr/share/doc/dpkg/README.feature-removal-schedule.gz", + "/usr/share/doc/dpkg/THANKS.gz", + "/usr/share/doc/dpkg/changelog.gz", + "/usr/share/doc/dpkg/copyright", + "/usr/share/dpkg/abitable", + "/usr/share/dpkg/cputable", + "/usr/share/dpkg/ostable", + "/usr/share/dpkg/sh/dpkg-error.sh", + "/usr/share/dpkg/tupletable", + "/usr/share/lintian/overrides/dpkg", + "/usr/share/lintian/profiles/dpkg/main.profile", + "/usr/share/locale/ast/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/bs/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ca/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/cs/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/da/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/de/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/dz/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/el/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/eo/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/es/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/et/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/eu/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/fr/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/gl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/hu/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/id/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/it/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ja/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/km/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ko/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ku/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/lt/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/mr/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/nb/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ne/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/nl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/nn/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/oc/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pa/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pt/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ro/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/ru/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/sk/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/sv/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/th/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/tl/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/tr/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/vi/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/dpkg.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/dpkg.mo", + "/usr/share/man/de/man1/dpkg-deb.1.gz", + "/usr/share/man/de/man1/dpkg-divert.1.gz", + "/usr/share/man/de/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/de/man1/dpkg-query.1.gz", + "/usr/share/man/de/man1/dpkg-realpath.1.gz", + "/usr/share/man/de/man1/dpkg-split.1.gz", + "/usr/share/man/de/man1/dpkg-statoverride.1.gz", + "/usr/share/man/de/man1/dpkg-trigger.1.gz", + "/usr/share/man/de/man1/dpkg.1.gz", + "/usr/share/man/de/man1/update-alternatives.1.gz", + "/usr/share/man/de/man5/dpkg.cfg.5.gz", + "/usr/share/man/de/man8/start-stop-daemon.8.gz", + "/usr/share/man/es/man5/dpkg.cfg.5.gz", + "/usr/share/man/fr/man1/dpkg-divert.1.gz", + "/usr/share/man/fr/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/fr/man1/dpkg-query.1.gz", + "/usr/share/man/fr/man1/dpkg-realpath.1.gz", + "/usr/share/man/fr/man1/dpkg-split.1.gz", + "/usr/share/man/fr/man1/dpkg-trigger.1.gz", + "/usr/share/man/fr/man1/update-alternatives.1.gz", + "/usr/share/man/fr/man5/dpkg.cfg.5.gz", + "/usr/share/man/fr/man8/start-stop-daemon.8.gz", + "/usr/share/man/it/man5/dpkg.cfg.5.gz", + "/usr/share/man/ja/man5/dpkg.cfg.5.gz", + "/usr/share/man/man1/dpkg-deb.1.gz", + "/usr/share/man/man1/dpkg-divert.1.gz", + "/usr/share/man/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/man1/dpkg-query.1.gz", + "/usr/share/man/man1/dpkg-realpath.1.gz", + "/usr/share/man/man1/dpkg-split.1.gz", + "/usr/share/man/man1/dpkg-statoverride.1.gz", + "/usr/share/man/man1/dpkg-trigger.1.gz", + "/usr/share/man/man1/dpkg.1.gz", + "/usr/share/man/man1/update-alternatives.1.gz", + "/usr/share/man/man5/dpkg.cfg.5.gz", + "/usr/share/man/man8/start-stop-daemon.8.gz", + "/usr/share/man/nl/man1/dpkg-deb.1.gz", + "/usr/share/man/nl/man1/dpkg-divert.1.gz", + "/usr/share/man/nl/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/nl/man1/dpkg-query.1.gz", + "/usr/share/man/nl/man1/dpkg-realpath.1.gz", + "/usr/share/man/nl/man1/dpkg-split.1.gz", + "/usr/share/man/nl/man1/dpkg-statoverride.1.gz", + "/usr/share/man/nl/man1/dpkg-trigger.1.gz", + "/usr/share/man/nl/man1/dpkg.1.gz", + "/usr/share/man/nl/man1/update-alternatives.1.gz", + "/usr/share/man/nl/man5/dpkg.cfg.5.gz", + "/usr/share/man/nl/man8/start-stop-daemon.8.gz", + "/usr/share/man/pl/man5/dpkg.cfg.5.gz", + "/usr/share/man/pt/man1/dpkg-deb.1.gz", + "/usr/share/man/pt/man1/dpkg-divert.1.gz", + "/usr/share/man/pt/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/pt/man1/dpkg-query.1.gz", + "/usr/share/man/pt/man1/dpkg-realpath.1.gz", + "/usr/share/man/pt/man1/dpkg-split.1.gz", + "/usr/share/man/pt/man1/dpkg-statoverride.1.gz", + "/usr/share/man/pt/man1/dpkg-trigger.1.gz", + "/usr/share/man/pt/man1/dpkg.1.gz", + "/usr/share/man/pt/man1/update-alternatives.1.gz", + "/usr/share/man/pt/man5/dpkg.cfg.5.gz", + "/usr/share/man/pt/man8/start-stop-daemon.8.gz", + "/usr/share/man/sv/man1/dpkg-deb.1.gz", + "/usr/share/man/sv/man1/dpkg-divert.1.gz", + "/usr/share/man/sv/man1/dpkg-maintscript-helper.1.gz", + "/usr/share/man/sv/man1/dpkg-query.1.gz", + "/usr/share/man/sv/man1/dpkg-realpath.1.gz", + "/usr/share/man/sv/man1/dpkg-split.1.gz", + "/usr/share/man/sv/man1/dpkg-statoverride.1.gz", + "/usr/share/man/sv/man1/dpkg-trigger.1.gz", + "/usr/share/man/sv/man1/dpkg.1.gz", + "/usr/share/man/sv/man1/update-alternatives.1.gz", + "/usr/share/man/sv/man5/dpkg.cfg.5.gz", + "/usr/share/man/sv/man8/start-stop-daemon.8.gz", + "/usr/share/polkit-1/actions/org.dpkg.pkexec.update-alternatives.policy" + ] + }, + { + "ID": "findutils@4.10.0-3", + "Name": "findutils", + "Identifier": { + "PURL": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64\u0026distro=debian-13.6", + "UID": "111949a4800741f1" + }, + "Version": "4.10.0", + "Release": "3", + "Arch": "amd64", + "SrcName": "findutils", + "SrcVersion": "4.10.0", + "SrcRelease": "3", + "Licenses": [ + "GFDL-1.3-no-invariants-or-later", + "GPL-3.0-or-later", + "FSFAP", + "GPL-2+ with Autoconf-data exception", + "GPL-3+ with Autoconf-data exception", + "FSFULLR", + "GPL-2.0-or-later", + "X11", + "public-domain", + "LGPL-2.1-or-later", + "GPL with automake exception", + "LGPL-2.0-or-later", + "LGPL-3.0-or-later", + "BSD-3-Clause", + "GPL-3+ with Bison-2.2 exception", + "LGPL-3.0-only", + "ISC", + "GFDL-1.3-only", + "GPL-2.0-only", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Andreas Metzler \u003cametzler@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/find", + "/usr/bin/xargs", + "/usr/share/doc-base/findutils.findutils", + "/usr/share/doc/findutils/NEWS.gz", + "/usr/share/doc/findutils/README.gz", + "/usr/share/doc/findutils/TODO", + "/usr/share/doc/findutils/changelog.Debian.gz", + "/usr/share/doc/findutils/changelog.gz", + "/usr/share/doc/findutils/copyright", + "/usr/share/info/find-maint.info.gz", + "/usr/share/info/find.info.gz", + "/usr/share/locale/be/LC_MESSAGES/findutils.mo", + "/usr/share/locale/bg/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ca/LC_MESSAGES/findutils.mo", + "/usr/share/locale/cs/LC_MESSAGES/findutils.mo", + "/usr/share/locale/da/LC_MESSAGES/findutils.mo", + "/usr/share/locale/de/LC_MESSAGES/findutils.mo", + "/usr/share/locale/el/LC_MESSAGES/findutils.mo", + "/usr/share/locale/eo/LC_MESSAGES/findutils.mo", + "/usr/share/locale/es/LC_MESSAGES/findutils.mo", + "/usr/share/locale/et/LC_MESSAGES/findutils.mo", + "/usr/share/locale/fi/LC_MESSAGES/findutils.mo", + "/usr/share/locale/fr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ga/LC_MESSAGES/findutils.mo", + "/usr/share/locale/gl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/hr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/hu/LC_MESSAGES/findutils.mo", + "/usr/share/locale/id/LC_MESSAGES/findutils.mo", + "/usr/share/locale/it/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ja/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ka/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ko/LC_MESSAGES/findutils.mo", + "/usr/share/locale/lg/LC_MESSAGES/findutils.mo", + "/usr/share/locale/lt/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ms/LC_MESSAGES/findutils.mo", + "/usr/share/locale/nb/LC_MESSAGES/findutils.mo", + "/usr/share/locale/nl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/pl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/pt/LC_MESSAGES/findutils.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ro/LC_MESSAGES/findutils.mo", + "/usr/share/locale/ru/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sk/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sl/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/sv/LC_MESSAGES/findutils.mo", + "/usr/share/locale/tr/LC_MESSAGES/findutils.mo", + "/usr/share/locale/uk/LC_MESSAGES/findutils.mo", + "/usr/share/locale/vi/LC_MESSAGES/findutils.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/findutils.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/findutils.mo", + "/usr/share/man/man1/find.1.gz", + "/usr/share/man/man1/xargs.1.gz" + ] + }, + { + "ID": "gcc-14-base@14.2.0-19", + "Name": "gcc-14-base", + "Identifier": { + "PURL": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64\u0026distro=debian-13.6", + "UID": "a2d64c6b5f038075" + }, + "Version": "14.2.0", + "Release": "19", + "Arch": "amd64", + "SrcName": "gcc-14", + "SrcVersion": "14.2.0", + "SrcRelease": "19", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-3.0-only", + "GFDL-1.2-only", + "Artistic-2.0", + "LGPL-2.0-or-later" + ], + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/gcc-14-base/README.Debian.amd64.gz", + "/usr/share/doc/gcc-14-base/TODO.Debian", + "/usr/share/doc/gcc-14-base/changelog.Debian.gz", + "/usr/share/doc/gcc-14-base/copyright" + ] + }, + { + "ID": "grep@3.11-4", + "Name": "grep", + "Identifier": { + "PURL": "pkg:deb/debian/grep@3.11-4?arch=amd64\u0026distro=debian-13.6", + "UID": "d450e0ea7fae458f" + }, + "Version": "3.11", + "Release": "4", + "Arch": "amd64", + "SrcName": "grep", + "SrcVersion": "3.11", + "SrcRelease": "4", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/egrep", + "/usr/bin/fgrep", + "/usr/bin/grep", + "/usr/bin/rgrep", + "/usr/share/doc/grep/AUTHORS", + "/usr/share/doc/grep/NEWS.Debian.gz", + "/usr/share/doc/grep/NEWS.gz", + "/usr/share/doc/grep/README", + "/usr/share/doc/grep/THANKS.gz", + "/usr/share/doc/grep/TODO.gz", + "/usr/share/doc/grep/changelog.Debian.gz", + "/usr/share/doc/grep/changelog.gz", + "/usr/share/doc/grep/copyright", + "/usr/share/info/grep.info.gz", + "/usr/share/locale/af/LC_MESSAGES/grep.mo", + "/usr/share/locale/be/LC_MESSAGES/grep.mo", + "/usr/share/locale/bg/LC_MESSAGES/grep.mo", + "/usr/share/locale/ca/LC_MESSAGES/grep.mo", + "/usr/share/locale/cs/LC_MESSAGES/grep.mo", + "/usr/share/locale/da/LC_MESSAGES/grep.mo", + "/usr/share/locale/de/LC_MESSAGES/grep.mo", + "/usr/share/locale/el/LC_MESSAGES/grep.mo", + "/usr/share/locale/eo/LC_MESSAGES/grep.mo", + "/usr/share/locale/es/LC_MESSAGES/grep.mo", + "/usr/share/locale/et/LC_MESSAGES/grep.mo", + "/usr/share/locale/eu/LC_MESSAGES/grep.mo", + "/usr/share/locale/fi/LC_MESSAGES/grep.mo", + "/usr/share/locale/fr/LC_MESSAGES/grep.mo", + "/usr/share/locale/ga/LC_MESSAGES/grep.mo", + "/usr/share/locale/gl/LC_MESSAGES/grep.mo", + "/usr/share/locale/he/LC_MESSAGES/grep.mo", + "/usr/share/locale/hr/LC_MESSAGES/grep.mo", + "/usr/share/locale/hu/LC_MESSAGES/grep.mo", + "/usr/share/locale/id/LC_MESSAGES/grep.mo", + "/usr/share/locale/it/LC_MESSAGES/grep.mo", + "/usr/share/locale/ja/LC_MESSAGES/grep.mo", + "/usr/share/locale/ka/LC_MESSAGES/grep.mo", + "/usr/share/locale/ko/LC_MESSAGES/grep.mo", + "/usr/share/locale/ky/LC_MESSAGES/grep.mo", + "/usr/share/locale/lt/LC_MESSAGES/grep.mo", + "/usr/share/locale/nb/LC_MESSAGES/grep.mo", + "/usr/share/locale/nl/LC_MESSAGES/grep.mo", + "/usr/share/locale/pa/LC_MESSAGES/grep.mo", + "/usr/share/locale/pl/LC_MESSAGES/grep.mo", + "/usr/share/locale/pt/LC_MESSAGES/grep.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/grep.mo", + "/usr/share/locale/ro/LC_MESSAGES/grep.mo", + "/usr/share/locale/ru/LC_MESSAGES/grep.mo", + "/usr/share/locale/sk/LC_MESSAGES/grep.mo", + "/usr/share/locale/sl/LC_MESSAGES/grep.mo", + "/usr/share/locale/sr/LC_MESSAGES/grep.mo", + "/usr/share/locale/sv/LC_MESSAGES/grep.mo", + "/usr/share/locale/ta/LC_MESSAGES/grep.mo", + "/usr/share/locale/th/LC_MESSAGES/grep.mo", + "/usr/share/locale/tr/LC_MESSAGES/grep.mo", + "/usr/share/locale/uk/LC_MESSAGES/grep.mo", + "/usr/share/locale/vi/LC_MESSAGES/grep.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/grep.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/grep.mo", + "/usr/share/man/man1/grep.1.gz" + ] + }, + { + "ID": "gzip@1.13-1", + "Name": "gzip", + "Identifier": { + "PURL": "pkg:deb/debian/gzip@1.13-1?arch=amd64\u0026distro=debian-13.6", + "UID": "60254b2bea6a1f09" + }, + "Version": "1.13", + "Release": "1", + "Arch": "amd64", + "SrcName": "gzip", + "SrcVersion": "1.13", + "SrcRelease": "1", + "Licenses": [ + "GPL-3.0-or-later", + "GFDL-1.3+-no-invariant", + "FSF-manpages", + "GPL-3.0-only", + "GFDL-3" + ], + "Maintainer": "Milan Kupcevic \u003cmilan@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/gunzip", + "/usr/bin/gzexe", + "/usr/bin/gzip", + "/usr/bin/zcat", + "/usr/bin/zcmp", + "/usr/bin/zdiff", + "/usr/bin/zegrep", + "/usr/bin/zfgrep", + "/usr/bin/zforce", + "/usr/bin/zgrep", + "/usr/bin/zless", + "/usr/bin/zmore", + "/usr/bin/znew", + "/usr/share/doc/gzip/NEWS.gz", + "/usr/share/doc/gzip/README.gz", + "/usr/share/doc/gzip/TODO", + "/usr/share/doc/gzip/changelog.Debian.gz", + "/usr/share/doc/gzip/changelog.gz", + "/usr/share/doc/gzip/copyright", + "/usr/share/info/gzip.info.gz", + "/usr/share/man/man1/gzexe.1.gz", + "/usr/share/man/man1/gzip.1.gz", + "/usr/share/man/man1/zdiff.1.gz", + "/usr/share/man/man1/zforce.1.gz", + "/usr/share/man/man1/zgrep.1.gz", + "/usr/share/man/man1/zless.1.gz", + "/usr/share/man/man1/zmore.1.gz", + "/usr/share/man/man1/znew.1.gz" + ] + }, + { + "ID": "hostname@3.25", + "Name": "hostname", + "Identifier": { + "PURL": "pkg:deb/debian/hostname@3.25?arch=amd64\u0026distro=debian-13.6", + "UID": "87ef62957d44b2c" + }, + "Version": "3.25", + "Arch": "amd64", + "SrcName": "hostname", + "SrcVersion": "3.25", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Michael Meskes \u003cmeskes@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/hostname", + "/usr/share/doc/hostname/changelog.gz", + "/usr/share/doc/hostname/copyright", + "/usr/share/man/man1/hostname.1.gz" + ] + }, + { + "ID": "init-system-helpers@1.69~deb13u1", + "Name": "init-system-helpers", + "Identifier": { + "PURL": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all\u0026distro=debian-13.6", + "UID": "317be9d9c6744acd" + }, + "Version": "1.69~deb13u1", + "Arch": "all", + "SrcName": "init-system-helpers", + "SrcVersion": "1.69~deb13u1", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/deb-systemd-helper", + "/usr/bin/deb-systemd-invoke", + "/usr/sbin/invoke-rc.d", + "/usr/sbin/service", + "/usr/sbin/update-rc.d", + "/usr/share/bug/init-system-helpers/control", + "/usr/share/doc/init-system-helpers/README.invoke-rc.d.gz", + "/usr/share/doc/init-system-helpers/README.policy-rc.d.gz", + "/usr/share/doc/init-system-helpers/changelog.gz", + "/usr/share/doc/init-system-helpers/copyright", + "/usr/share/lintian/overrides/init-system-helpers", + "/usr/share/man/man1/deb-systemd-helper.1p.gz", + "/usr/share/man/man1/deb-systemd-invoke.1p.gz", + "/usr/share/man/man8/invoke-rc.d.8.gz", + "/usr/share/man/man8/service.8.gz", + "/usr/share/man/man8/update-rc.d.8.gz" + ] + }, + { + "ID": "keyutils-libs@1.5.8-3.el7", + "Name": "keyutils-libs", + "Identifier": { + "PURL": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7", + "UID": "93c3119deb6a93bf", + "BOMRef": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9" + }, + "Version": "1.5.8", + "Release": "3.el7", + "SrcName": "keyutils-libs", + "SrcVersion": "1.5.8", + "SrcRelease": "3.el7", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "krb5-libs@1.15.1-55.el7_9", + "Name": "krb5-libs", + "Identifier": { + "PURL": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9", + "UID": "4f23f9cf8fc8b67c", + "BOMRef": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#8cee64e9f2ddb592df8075ecb6489ed5727979b245606ae73dae382890f54ed5" + }, + "Version": "1.15.1", + "Release": "55.el7_9", + "SrcName": "krb5-libs", + "SrcVersion": "1.15.1", + "SrcRelease": "55.el7_9", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "libacl1@2.3.2-2+b1", + "Name": "libacl1", + "Identifier": { + "PURL": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64\u0026distro=debian-13.6", + "UID": "82548f6f7baf25f3" + }, + "Version": "2.3.2", + "Release": "2+b1", + "Arch": "amd64", + "SrcName": "acl", + "SrcVersion": "2.3.2", + "SrcRelease": "2", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2302", + "/usr/share/doc/libacl1/changelog.Debian.amd64.gz", + "/usr/share/doc/libacl1/changelog.Debian.gz", + "/usr/share/doc/libacl1/changelog.gz", + "/usr/share/doc/libacl1/copyright", + "/usr/share/lintian/overrides/libacl1" + ] + }, + { + "ID": "libapt-pkg7.0@3.0.3", + "Name": "libapt-pkg7.0", + "Identifier": { + "PURL": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64\u0026distro=debian-13.6", + "UID": "df08f16788e2e8a6" + }, + "Version": "3.0.3", + "Arch": "amd64", + "SrcName": "apt", + "SrcVersion": "3.0.3", + "Licenses": [ + "GPL-2.0-or-later", + "curl", + "BSD-3-Clause", + "MIT", + "GPL-2.0-only" + ], + "Maintainer": "APT Development Team \u003cdeity@lists.debian.org\u003e", + "DependsOn": [ + "libbz2-1.0@1.0.8-6", + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19", + "liblz4-1@1.10.0-4", + "liblzma5@5.8.1-1+deb13u1", + "libssl3t64@3.5.6-1~deb13u2", + "libstdc++6@14.2.0-19", + "libsystemd0@257.13-1~deb13u1", + "libudev1@257.13-1~deb13u1", + "libxxhash0@0.8.3-2", + "libzstd1@1.5.7+dfsg-1", + "zlib1g@1:1.3.dfsg+really1.3.1-1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libapt-pkg.so.7.0.0", + "/usr/share/doc/libapt-pkg7.0/NEWS.Debian.gz", + "/usr/share/doc/libapt-pkg7.0/changelog.gz", + "/usr/share/doc/libapt-pkg7.0/copyright", + "/usr/share/locale/ar/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ast/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/bg/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/bs/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ca/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/cs/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/cy/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/da/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/de/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/dz/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/el/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/es/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/eu/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/fi/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/fr/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/gl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/hu/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/it/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ja/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/km/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ko/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ku/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/lt/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/mr/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/nb/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ne/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/nl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/nn/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/pl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/pt/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ro/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/ru/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/sk/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/sl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/sv/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/th/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/tl/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/tr/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/uk/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/vi/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/libapt-pkg7.0.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/libapt-pkg7.0.mo" + ] + }, + { + "ID": "libattr1@1:2.5.2-3", + "Name": "libattr1", + "Identifier": { + "PURL": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "953f1d1395118b4b" + }, + "Version": "2.5.2", + "Release": "3", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "attr", + "SrcVersion": "2.5.2", + "SrcRelease": "3", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libattr.so.1.1.2502", + "/usr/share/doc/libattr1/changelog.Debian.gz", + "/usr/share/doc/libattr1/changelog.gz", + "/usr/share/doc/libattr1/copyright", + "/usr/share/lintian/overrides/libattr1" + ] + }, + { + "ID": "libaudit-common@1:4.0.2-2", + "Name": "libaudit-common", + "Identifier": { + "PURL": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all\u0026distro=debian-13.6\u0026epoch=1", + "UID": "4845289e49197cbd" + }, + "Version": "4.0.2", + "Release": "2", + "Epoch": 1, + "Arch": "all", + "SrcName": "audit", + "SrcVersion": "4.0.2", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.1-only", + "GPL-1.0-only" + ], + "Maintainer": "Laurent Bigonville \u003cbigon@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/libaudit-common/changelog.Debian.gz", + "/usr/share/doc/libaudit-common/changelog.gz", + "/usr/share/doc/libaudit-common/copyright", + "/usr/share/man/man5/libaudit.conf.5.gz" + ] + }, + { + "ID": "libaudit1@1:4.0.2-2+b2", + "Name": "libaudit1", + "Identifier": { + "PURL": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "c9ebadb6608e2305" + }, + "Version": "4.0.2", + "Release": "2+b2", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "audit", + "SrcVersion": "4.0.2", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.1-only", + "GPL-1.0-only" + ], + "Maintainer": "Laurent Bigonville \u003cbigon@debian.org\u003e", + "DependsOn": [ + "libaudit-common@1:4.0.2-2", + "libc6@2.41-12+deb13u3", + "libcap-ng0@0.8.5-4+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libaudit.so.1.0.0", + "/usr/share/doc/libaudit1/changelog.Debian.amd64.gz", + "/usr/share/doc/libaudit1/changelog.Debian.gz", + "/usr/share/doc/libaudit1/changelog.gz", + "/usr/share/doc/libaudit1/copyright" + ] + }, + { + "ID": "libblkid1@2.41-5", + "Name": "libblkid1", + "Identifier": { + "PURL": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "c427952a98b1e3ee" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libblkid.so.1.1.0", + "/usr/share/doc/libblkid1/NEWS.Debian.gz", + "/usr/share/doc/libblkid1/changelog.Debian.gz", + "/usr/share/doc/libblkid1/changelog.gz", + "/usr/share/doc/libblkid1/copyright", + "/usr/share/lintian/overrides/libblkid1" + ] + }, + { + "ID": "libbsd0@0.12.2-2", + "Name": "libbsd0", + "Identifier": { + "PURL": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64\u0026distro=debian-13.6", + "UID": "6a35c140077b4eb4" + }, + "Version": "0.12.2", + "Release": "2", + "Arch": "amd64", + "SrcName": "libbsd", + "SrcVersion": "0.12.2", + "SrcRelease": "2", + "Licenses": [ + "BSD-3-Clause", + "BSD-3-clause-Regents", + "BSD-2-Clause-NetBSD", + "BSD-3-clause-author", + "BSD-3-clause-John-Birrell", + "BSD-5-clause-Peter-Wemm", + "BSD-2-Clause", + "BSD-2-clause-verbatim", + "BSD-2-clause-author", + "ISC", + "ISC-Original", + "MIT", + "public-domain", + "Beerware" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libmd0@1.1.0-2+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libbsd.so.0.12.2", + "/usr/share/doc/libbsd0/changelog.Debian.gz", + "/usr/share/doc/libbsd0/changelog.gz", + "/usr/share/doc/libbsd0/copyright", + "/usr/share/lintian/overrides/libbsd0" + ] + }, + { + "ID": "libbz2-1.0@1.0.8-6", + "Name": "libbz2-1.0", + "Identifier": { + "PURL": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64\u0026distro=debian-13.6", + "UID": "395e7ab13e254394" + }, + "Version": "1.0.8", + "Release": "6", + "Arch": "amd64", + "SrcName": "bzip2", + "SrcVersion": "1.0.8", + "SrcRelease": "6", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libbz2.so.1.0.4", + "/usr/share/doc/libbz2-1.0/changelog.Debian.gz", + "/usr/share/doc/libbz2-1.0/changelog.gz", + "/usr/share/doc/libbz2-1.0/copyright" + ] + }, + { + "ID": "libc-bin@2.41-12+deb13u3", + "Name": "libc-bin", + "Identifier": { + "PURL": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64\u0026distro=debian-13.6", + "UID": "c17717cca0e61621" + }, + "Version": "2.41", + "Release": "12+deb13u3", + "Arch": "amd64", + "SrcName": "glibc", + "SrcVersion": "2.41", + "SrcRelease": "12+deb13u3", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.0-or-later", + "LGPL-2.1+-with-link-exception", + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "GPL-2+-with-link-exception", + "GPL-2.0-only", + "GPL-3.0-or-later", + "FSFAP", + "Carnegie", + "Inner-Net", + "MIT-like-Lord", + "BSD-like-Spencer", + "PCRE", + "BSD-3-clause-Carnegie", + "Unicode-DFS-2016", + "BSL-1.0", + "SunPro", + "CORE-MATH", + "BSD-3-clause-Berkeley", + "BSD-3-clause-WIDE", + "BSD-2-Clause", + "BSD-3-clause-Oracle", + "DEC", + "IBM", + "ISC", + "Univ-Coimbra", + "public-domain", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/getconf", + "/usr/bin/getent", + "/usr/bin/iconv", + "/usr/bin/ldd", + "/usr/bin/locale", + "/usr/bin/localedef", + "/usr/bin/pldd", + "/usr/bin/tzselect", + "/usr/bin/zdump", + "/usr/lib/locale/C.utf8/LC_ADDRESS", + "/usr/lib/locale/C.utf8/LC_COLLATE", + "/usr/lib/locale/C.utf8/LC_CTYPE", + "/usr/lib/locale/C.utf8/LC_IDENTIFICATION", + "/usr/lib/locale/C.utf8/LC_MEASUREMENT", + "/usr/lib/locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES", + "/usr/lib/locale/C.utf8/LC_MONETARY", + "/usr/lib/locale/C.utf8/LC_NAME", + "/usr/lib/locale/C.utf8/LC_NUMERIC", + "/usr/lib/locale/C.utf8/LC_PAPER", + "/usr/lib/locale/C.utf8/LC_TELEPHONE", + "/usr/lib/locale/C.utf8/LC_TIME", + "/usr/sbin/iconvconfig", + "/usr/sbin/ldconfig", + "/usr/sbin/zic", + "/usr/share/doc/libc-bin/changelog.Debian.gz", + "/usr/share/doc/libc-bin/changelog.gz", + "/usr/share/doc/libc-bin/copyright", + "/usr/share/libc-bin/nsswitch.conf", + "/usr/share/lintian/overrides/libc-bin", + "/usr/share/man/man1/getconf.1.gz", + "/usr/share/man/man1/tzselect.1.gz" + ] + }, + { + "ID": "libc6@2.41-12+deb13u3", + "Name": "libc6", + "Identifier": { + "PURL": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64\u0026distro=debian-13.6", + "UID": "19d151c4d1229080" + }, + "Version": "2.41", + "Release": "12+deb13u3", + "Arch": "amd64", + "SrcName": "glibc", + "SrcVersion": "2.41", + "SrcRelease": "12+deb13u3", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.0-or-later", + "LGPL-2.1+-with-link-exception", + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "GPL-2+-with-link-exception", + "GPL-2.0-only", + "GPL-3.0-or-later", + "FSFAP", + "Carnegie", + "Inner-Net", + "MIT-like-Lord", + "BSD-like-Spencer", + "PCRE", + "BSD-3-clause-Carnegie", + "Unicode-DFS-2016", + "BSL-1.0", + "SunPro", + "CORE-MATH", + "BSD-3-clause-Berkeley", + "BSD-3-clause-WIDE", + "BSD-2-Clause", + "BSD-3-clause-Oracle", + "DEC", + "IBM", + "ISC", + "Univ-Coimbra", + "public-domain", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "DependsOn": [ + "libgcc-s1@14.2.0-19" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/gconv/ANSI_X3.110.so", + "/usr/lib/x86_64-linux-gnu/gconv/ARMSCII-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/ASMO_449.so", + "/usr/lib/x86_64-linux-gnu/gconv/BIG5.so", + "/usr/lib/x86_64-linux-gnu/gconv/BIG5HKSCS.so", + "/usr/lib/x86_64-linux-gnu/gconv/BRF.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP10007.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1125.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1250.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1251.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1252.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1253.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1254.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1255.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1256.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1257.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP1258.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP737.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP770.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP771.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP772.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP773.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP774.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP775.so", + "/usr/lib/x86_64-linux-gnu/gconv/CP932.so", + "/usr/lib/x86_64-linux-gnu/gconv/CSN_369103.so", + "/usr/lib/x86_64-linux-gnu/gconv/CWI.so", + "/usr/lib/x86_64-linux-gnu/gconv/DEC-MCS.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-AT-DE-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-AT-DE.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-CA-FR.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-DK-NO-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-DK-NO.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-ES-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-ES-S.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-ES.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-FI-SE-A.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-FI-SE.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-FR.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-IS-FRISS.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-IT.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-PT.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-UK.so", + "/usr/lib/x86_64-linux-gnu/gconv/EBCDIC-US.so", + "/usr/lib/x86_64-linux-gnu/gconv/ECMA-CYRILLIC.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-CN.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-JISX0213.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-JP-MS.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-JP.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-KR.so", + "/usr/lib/x86_64-linux-gnu/gconv/EUC-TW.so", + "/usr/lib/x86_64-linux-gnu/gconv/GB18030.so", + "/usr/lib/x86_64-linux-gnu/gconv/GBBIG5.so", + "/usr/lib/x86_64-linux-gnu/gconv/GBGBK.so", + "/usr/lib/x86_64-linux-gnu/gconv/GBK.so", + "/usr/lib/x86_64-linux-gnu/gconv/GEORGIAN-ACADEMY.so", + "/usr/lib/x86_64-linux-gnu/gconv/GEORGIAN-PS.so", + "/usr/lib/x86_64-linux-gnu/gconv/GOST_19768-74.so", + "/usr/lib/x86_64-linux-gnu/gconv/GREEK-CCITT.so", + "/usr/lib/x86_64-linux-gnu/gconv/GREEK7-OLD.so", + "/usr/lib/x86_64-linux-gnu/gconv/GREEK7.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-GREEK8.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-ROMAN8.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-ROMAN9.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-THAI8.so", + "/usr/lib/x86_64-linux-gnu/gconv/HP-TURKISH8.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM037.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM038.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1004.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1008.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1008_420.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1025.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1026.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1046.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1047.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1097.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1112.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1122.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1123.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1124.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1129.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1130.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1132.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1133.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1137.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1140.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1141.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1142.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1143.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1144.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1145.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1146.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1147.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1148.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1149.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1153.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1154.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1155.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1156.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1157.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1158.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1160.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1161.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1162.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1163.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1164.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1166.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1167.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM12712.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1364.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1371.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1388.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1390.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM1399.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM16804.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM256.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM273.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM274.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM275.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM277.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM278.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM280.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM281.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM284.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM285.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM290.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM297.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM420.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM423.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM424.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM437.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4517.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4899.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4909.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM4971.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM500.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM5347.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM803.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM850.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM851.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM852.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM855.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM856.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM857.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM858.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM860.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM861.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM862.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM863.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM864.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM865.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM866.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM866NAV.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM868.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM869.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM870.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM871.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM874.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM875.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM880.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM891.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM901.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM902.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM903.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM9030.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM904.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM905.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM9066.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM918.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM921.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM922.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM930.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM932.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM933.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM935.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM937.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM939.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM943.so", + "/usr/lib/x86_64-linux-gnu/gconv/IBM9448.so", + "/usr/lib/x86_64-linux-gnu/gconv/IEC_P27-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/INIS-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/INIS-CYRILLIC.so", + "/usr/lib/x86_64-linux-gnu/gconv/INIS.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISIRI-3342.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-CN-EXT.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-CN.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-JP-3.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-JP.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-2022-KR.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-IR-197.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO-IR-209.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO646.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-10.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-11.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-13.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-14.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-15.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-16.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-2.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-3.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-4.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-5.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-6.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-7.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-9.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO8859-9E.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_10367-BOX.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_11548-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_2033.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_5427-EXT.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_5427.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_5428.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_6937-2.so", + "/usr/lib/x86_64-linux-gnu/gconv/ISO_6937.so", + "/usr/lib/x86_64-linux-gnu/gconv/JOHAB.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI-8.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-R.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-RU.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-T.so", + "/usr/lib/x86_64-linux-gnu/gconv/KOI8-U.so", + "/usr/lib/x86_64-linux-gnu/gconv/LATIN-GREEK-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/LATIN-GREEK.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-CENTRALEUROPE.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-IS.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-SAMI.so", + "/usr/lib/x86_64-linux-gnu/gconv/MAC-UK.so", + "/usr/lib/x86_64-linux-gnu/gconv/MACINTOSH.so", + "/usr/lib/x86_64-linux-gnu/gconv/MIK.so", + "/usr/lib/x86_64-linux-gnu/gconv/NATS-DANO.so", + "/usr/lib/x86_64-linux-gnu/gconv/NATS-SEFI.so", + "/usr/lib/x86_64-linux-gnu/gconv/PT154.so", + "/usr/lib/x86_64-linux-gnu/gconv/RK1048.so", + "/usr/lib/x86_64-linux-gnu/gconv/SAMI-WS2.so", + "/usr/lib/x86_64-linux-gnu/gconv/SHIFT_JISX0213.so", + "/usr/lib/x86_64-linux-gnu/gconv/SJIS.so", + "/usr/lib/x86_64-linux-gnu/gconv/T.61.so", + "/usr/lib/x86_64-linux-gnu/gconv/TCVN5712-1.so", + "/usr/lib/x86_64-linux-gnu/gconv/TIS-620.so", + "/usr/lib/x86_64-linux-gnu/gconv/TSCII.so", + "/usr/lib/x86_64-linux-gnu/gconv/UHC.so", + "/usr/lib/x86_64-linux-gnu/gconv/UNICODE.so", + "/usr/lib/x86_64-linux-gnu/gconv/UTF-16.so", + "/usr/lib/x86_64-linux-gnu/gconv/UTF-32.so", + "/usr/lib/x86_64-linux-gnu/gconv/UTF-7.so", + "/usr/lib/x86_64-linux-gnu/gconv/VISCII.so", + "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules", + "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", + "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.d/gconv-modules-extra.conf", + "/usr/lib/x86_64-linux-gnu/gconv/libCNS.so", + "/usr/lib/x86_64-linux-gnu/gconv/libGB.so", + "/usr/lib/x86_64-linux-gnu/gconv/libISOIR165.so", + "/usr/lib/x86_64-linux-gnu/gconv/libJIS.so", + "/usr/lib/x86_64-linux-gnu/gconv/libJISX0213.so", + "/usr/lib/x86_64-linux-gnu/gconv/libKSC.so", + "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + "/usr/lib/x86_64-linux-gnu/libBrokenLocale.so.1", + "/usr/lib/x86_64-linux-gnu/libanl.so.1", + "/usr/lib/x86_64-linux-gnu/libc.so.6", + "/usr/lib/x86_64-linux-gnu/libc_malloc_debug.so.0", + "/usr/lib/x86_64-linux-gnu/libdl.so.2", + "/usr/lib/x86_64-linux-gnu/libm.so.6", + "/usr/lib/x86_64-linux-gnu/libmemusage.so", + "/usr/lib/x86_64-linux-gnu/libmvec.so.1", + "/usr/lib/x86_64-linux-gnu/libnsl.so.1", + "/usr/lib/x86_64-linux-gnu/libnss_compat.so.2", + "/usr/lib/x86_64-linux-gnu/libnss_dns.so.2", + "/usr/lib/x86_64-linux-gnu/libnss_files.so.2", + "/usr/lib/x86_64-linux-gnu/libnss_hesiod.so.2", + "/usr/lib/x86_64-linux-gnu/libpcprofile.so", + "/usr/lib/x86_64-linux-gnu/libpthread.so.0", + "/usr/lib/x86_64-linux-gnu/libresolv.so.2", + "/usr/lib/x86_64-linux-gnu/librt.so.1", + "/usr/lib/x86_64-linux-gnu/libthread_db.so.1", + "/usr/lib/x86_64-linux-gnu/libutil.so.1", + "/usr/share/doc/libc6/NEWS.Debian.gz", + "/usr/share/doc/libc6/NEWS.gz", + "/usr/share/doc/libc6/README.Debian.gz", + "/usr/share/doc/libc6/README.hesiod.gz", + "/usr/share/doc/libc6/changelog.Debian.gz", + "/usr/share/doc/libc6/changelog.gz", + "/usr/share/doc/libc6/copyright", + "/usr/share/lintian/overrides/libc6" + ] + }, + { + "ID": "libcap-ng0@0.8.5-4+b1", + "Name": "libcap-ng0", + "Identifier": { + "PURL": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64\u0026distro=debian-13.6", + "UID": "853c3a4f587b1e09" + }, + "Version": "0.8.5", + "Release": "4+b1", + "Arch": "amd64", + "SrcName": "libcap-ng", + "SrcVersion": "0.8.5", + "SrcRelease": "4", + "Licenses": [ + "LGPL-2.1-or-later", + "GPL-2.0-or-later", + "GPL-3.0-only", + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Håvard F. Aasen \u003chavard.f.aasen@pfft.no\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libcap-ng.so.0.0.0", + "/usr/lib/x86_64-linux-gnu/libdrop_ambient.so.0.0.0", + "/usr/share/doc/libcap-ng0/changelog.Debian.amd64.gz", + "/usr/share/doc/libcap-ng0/changelog.Debian.gz", + "/usr/share/doc/libcap-ng0/changelog.gz", + "/usr/share/doc/libcap-ng0/copyright" + ] + }, + { + "ID": "libcap2@1:2.75-10+deb13u1+b1", + "Name": "libcap2", + "Identifier": { + "PURL": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "451a510e42b50c7c" + }, + "Version": "2.75", + "Release": "10+deb13u1+b1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "libcap2", + "SrcVersion": "2.75", + "SrcRelease": "10+deb13u1", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Christian Kastner \u003cckk@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libcap.so.2.75", + "/usr/lib/x86_64-linux-gnu/libpsx.so.2.75", + "/usr/share/doc/libcap2/changelog.Debian.amd64.gz", + "/usr/share/doc/libcap2/changelog.Debian.gz", + "/usr/share/doc/libcap2/changelog.gz", + "/usr/share/doc/libcap2/copyright" + ] + }, + { + "ID": "libcom_err@1.42.9-19.el7", + "Name": "libcom_err", + "Identifier": { + "PURL": "pkg:rpm/centos/libcom_err@1.42.9-19.el7", + "UID": "dd13ac5974bf1c3f", + "BOMRef": "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce" + }, + "Version": "1.42.9", + "Release": "19.el7", + "SrcName": "libcom_err", + "SrcVersion": "1.42.9", + "SrcRelease": "19.el7", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "libcrypt1@1:4.4.38-1", + "Name": "libcrypt1", + "Identifier": { + "PURL": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "185d1b457ef399b8" + }, + "Version": "4.4.38", + "Release": "1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "libxcrypt", + "SrcVersion": "4.4.38", + "SrcRelease": "1", + "SrcEpoch": 1, + "Maintainer": "Marco d'Itri \u003cmd@linux.it\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libcrypt.so.1.1.0", + "/usr/share/doc/libcrypt1/changelog.Debian.gz", + "/usr/share/doc/libcrypt1/changelog.gz", + "/usr/share/doc/libcrypt1/copyright" + ] + }, + { + "ID": "libdb5.3t64@5.3.28+dfsg2-9", + "Name": "libdb5.3t64", + "Identifier": { + "PURL": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64\u0026distro=debian-13.6", + "UID": "1d93101a053d025d" + }, + "Version": "5.3.28+dfsg2", + "Release": "9", + "Arch": "amd64", + "SrcName": "db5.3", + "SrcVersion": "5.3.28+dfsg2", + "SrcRelease": "9", + "Licenses": [ + "Sleepycat", + "BSD-3-Clause", + "MS-PL", + "GPL-2.0-or-later", + "Artistic-2.0", + "X11", + "MIT-old", + "TCL-like", + "BSD-3-clause-fjord", + "GPL-3.0-only", + "Zlib" + ], + "Maintainer": "Debian QA Group \u003cpackages@qa.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libdb-5.3.so", + "/usr/share/doc/libdb5.3t64/build_signature_amd64.txt", + "/usr/share/doc/libdb5.3t64/changelog.Debian.gz", + "/usr/share/doc/libdb5.3t64/copyright", + "/usr/share/lintian/overrides/libdb5.3t64" + ] + }, + { + "ID": "libdebconfclient0@0.280", + "Name": "libdebconfclient0", + "Identifier": { + "PURL": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64\u0026distro=debian-13.6", + "UID": "f4d35c54ea8ebcdc" + }, + "Version": "0.280", + "Arch": "amd64", + "SrcName": "cdebconf", + "SrcVersion": "0.280", + "Licenses": [ + "BSD-2-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian Install System Team \u003cdebian-boot@lists.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libdebconfclient.so.0.0.0", + "/usr/share/doc/libdebconfclient0/changelog.gz", + "/usr/share/doc/libdebconfclient0/copyright" + ] + }, + { + "ID": "libffi8@3.4.8-2", + "Name": "libffi8", + "Identifier": { + "PURL": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64\u0026distro=debian-13.6", + "UID": "d78c144a996938aa" + }, + "Version": "3.4.8", + "Release": "2", + "Arch": "amd64", + "SrcName": "libffi", + "SrcVersion": "3.4.8", + "SrcRelease": "2", + "Licenses": [ + "MIT", + "X11", + "GPL-2.0-or-later", + "GPL-3.0-or-later", + "MPL-1.1", + "LGPL-2.1-or-later", + "public-domain" + ], + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libffi.so.8.1.4", + "/usr/share/doc/libffi8/changelog.Debian.gz", + "/usr/share/doc/libffi8/copyright" + ] + }, + { + "ID": "libgcc-s1@14.2.0-19", + "Name": "libgcc-s1", + "Identifier": { + "PURL": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64\u0026distro=debian-13.6", + "UID": "a939ab1a9133b3fa" + }, + "Version": "14.2.0", + "Release": "19", + "Arch": "amd64", + "SrcName": "gcc-14", + "SrcVersion": "14.2.0", + "SrcRelease": "19", + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "DependsOn": [ + "gcc-14-base@14.2.0-19", + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libgcc_s.so.1", + "/usr/share/lintian/overrides/libgcc-s1" + ] + }, + { + "ID": "libgdbm6t64@1.24-2", + "Name": "libgdbm6t64", + "Identifier": { + "PURL": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64\u0026distro=debian-13.6", + "UID": "79fdad8a6bd05b2d" + }, + "Version": "1.24", + "Release": "2", + "Arch": "amd64", + "SrcName": "gdbm", + "SrcVersion": "1.24", + "SrcRelease": "2", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-2.0-or-later", + "GFDL-1.3-no-invariants-or-later", + "GPL-3.0-only", + "GPL-2.0-only" + ], + "Maintainer": "Nicolas Mora \u003cbabelouest@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libgdbm.so.6.0.0", + "/usr/share/doc/libgdbm6t64/changelog.Debian.gz", + "/usr/share/doc/libgdbm6t64/changelog.gz", + "/usr/share/doc/libgdbm6t64/copyright", + "/usr/share/lintian/overrides/libgdbm6t64" + ] + }, + { + "ID": "libgmp10@2:6.3.0+dfsg-3", + "Name": "libgmp10", + "Identifier": { + "PURL": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64\u0026distro=debian-13.6\u0026epoch=2", + "UID": "8d1eefd06321d7f5" + }, + "Version": "6.3.0+dfsg", + "Release": "3", + "Epoch": 2, + "Arch": "amd64", + "SrcName": "gmp", + "SrcVersion": "6.3.0+dfsg", + "SrcRelease": "3", + "SrcEpoch": 2, + "Licenses": [ + "GPL-2.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-or-later", + "GPL-3+ with Bison exception", + "GPL-2.0-only", + "GPL-3.0-only", + "LGPL-3.0-only" + ], + "Maintainer": "Debian Science Maintainers \u003cdebian-science-maintainers@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libgmp.so.10.5.0", + "/usr/share/doc/libgmp10/README.Debian", + "/usr/share/doc/libgmp10/changelog.Debian.gz", + "/usr/share/doc/libgmp10/changelog.gz", + "/usr/share/doc/libgmp10/copyright" + ] + }, + { + "ID": "libhogweed6t64@3.10.1-1", + "Name": "libhogweed6t64", + "Identifier": { + "PURL": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "8c7c2e41ab9a40c8" + }, + "Version": "3.10.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "nettle", + "SrcVersion": "3.10.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "MIT", + "GPL-3.0-with-autoconf-exception+", + "public-domain", + "GPL-2.0-only", + "GAP" + ], + "Maintainer": "Magnus Holmgren \u003cholmgren@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libgmp10@2:6.3.0+dfsg-3", + "libnettle8t64@3.10.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libhogweed.so.6.10", + "/usr/share/doc/libhogweed6t64/changelog.Debian.gz", + "/usr/share/doc/libhogweed6t64/changelog.gz", + "/usr/share/doc/libhogweed6t64/copyright", + "/usr/share/lintian/overrides/libhogweed6t64" + ] + }, + { + "ID": "liblastlog2-2@2.41-5", + "Name": "liblastlog2-2", + "Identifier": { + "PURL": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "d5ec9bb1797e1476" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libsqlite3-0@3.46.1-7+deb13u1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/liblastlog2.so.2.0.0", + "/usr/share/doc/liblastlog2-2/NEWS.Debian.gz", + "/usr/share/doc/liblastlog2-2/changelog.Debian.gz", + "/usr/share/doc/liblastlog2-2/changelog.gz", + "/usr/share/doc/liblastlog2-2/copyright" + ] + }, + { + "ID": "liblz4-1@1.10.0-4", + "Name": "liblz4-1", + "Identifier": { + "PURL": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64\u0026distro=debian-13.6", + "UID": "d06629bc3067545a" + }, + "Version": "1.10.0", + "Release": "4", + "Arch": "amd64", + "SrcName": "lz4", + "SrcVersion": "1.10.0", + "SrcRelease": "4", + "Licenses": [ + "GPL-2.0-or-later", + "BSD-2-Clause", + "GPL-2.0-only" + ], + "Maintainer": "Nobuhiro Iwamatsu \u003ciwamatsu@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libxxhash0@0.8.3-2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/liblz4.so.1.10.0", + "/usr/share/doc/liblz4-1/changelog.Debian.gz", + "/usr/share/doc/liblz4-1/copyright" + ] + }, + { + "ID": "liblzma5@5.8.1-1+deb13u1", + "Name": "liblzma5", + "Identifier": { + "PURL": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "6bb07f060c067c08" + }, + "Version": "5.8.1", + "Release": "1+deb13u1", + "Arch": "amd64", + "SrcName": "xz-utils", + "SrcVersion": "5.8.1", + "SrcRelease": "1+deb13u1", + "Licenses": [ + "0BSD", + "GPL-2.0-or-later", + "LGPL-2.1-or-later", + "FSFULLR", + "GPL-3.0-or-later-WITH-Autoconf-exception-macro", + "none", + "PD", + "permissive-nowarranty", + "FSFUL", + "noderivs", + "PD-debian", + "LGPL-2.1-only", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "Maintainer": "Sebastian Andrzej Siewior \u003csebastian@breakpoint.cc\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/liblzma.so.5.8.1", + "/usr/share/doc/liblzma5/AUTHORS", + "/usr/share/doc/liblzma5/NEWS.gz", + "/usr/share/doc/liblzma5/THANKS.gz", + "/usr/share/doc/liblzma5/changelog.Debian.gz", + "/usr/share/doc/liblzma5/changelog.gz", + "/usr/share/doc/liblzma5/copyright" + ] + }, + { + "ID": "libmd0@1.1.0-2+b1", + "Name": "libmd0", + "Identifier": { + "PURL": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64\u0026distro=debian-13.6", + "UID": "8f7242077c74e850" + }, + "Version": "1.1.0", + "Release": "2+b1", + "Arch": "amd64", + "SrcName": "libmd", + "SrcVersion": "1.1.0", + "SrcRelease": "2", + "Licenses": [ + "BSD-3-Clause", + "BSD-3-clause-Aaron-D-Gifford", + "BSD-2-Clause", + "BSD-2-Clause-NetBSD", + "ISC", + "Beerware", + "public-domain-md4", + "public-domain-md5", + "public-domain-sha1" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libmd.so.0.1.0", + "/usr/share/doc/libmd0/changelog.Debian.amd64.gz", + "/usr/share/doc/libmd0/changelog.Debian.gz", + "/usr/share/doc/libmd0/changelog.gz", + "/usr/share/doc/libmd0/copyright" + ] + }, + { + "ID": "libmount1@2.41-5", + "Name": "libmount1", + "Identifier": { + "PURL": "pkg:deb/debian/libmount1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "e4ddbeb1b284f3ca" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libblkid1@2.41-5", + "libc6@2.41-12+deb13u3", + "libselinux1@3.8.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libmount.so.1.1.0", + "/usr/share/doc/libmount1/NEWS.Debian.gz", + "/usr/share/doc/libmount1/changelog.Debian.gz", + "/usr/share/doc/libmount1/changelog.gz", + "/usr/share/doc/libmount1/copyright", + "/usr/share/lintian/overrides/libmount1" + ] + }, + { + "ID": "libncursesw6@6.5+20250216-2", + "Name": "libncursesw6", + "Identifier": { + "PURL": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64\u0026distro=debian-13.6", + "UID": "fac6fddb91f7c21c" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libtinfo6@6.5+20250216-2" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libformw.so.6.5", + "/usr/lib/x86_64-linux-gnu/libmenuw.so.6.5", + "/usr/lib/x86_64-linux-gnu/libncursesw.so.6.5", + "/usr/lib/x86_64-linux-gnu/libpanelw.so.6.5" + ] + }, + { + "ID": "libnettle8t64@3.10.1-1", + "Name": "libnettle8t64", + "Identifier": { + "PURL": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "3cd10b6383088c9" + }, + "Version": "3.10.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "nettle", + "SrcVersion": "3.10.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "MIT", + "GPL-3.0-with-autoconf-exception+", + "public-domain", + "GPL-2.0-only", + "GAP" + ], + "Maintainer": "Magnus Holmgren \u003cholmgren@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libnettle.so.8.10", + "/usr/share/doc/libnettle8t64/NEWS.gz", + "/usr/share/doc/libnettle8t64/README", + "/usr/share/doc/libnettle8t64/changelog.Debian.gz", + "/usr/share/doc/libnettle8t64/changelog.gz", + "/usr/share/doc/libnettle8t64/copyright", + "/usr/share/lintian/overrides/libnettle8t64" + ] + }, + { + "ID": "libpam-modules@1.7.0-5", + "Name": "libpam-modules", + "Identifier": { + "PURL": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64\u0026distro=debian-13.6", + "UID": "b9d6f9c66558c40d" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/security/pam_access.so", + "/usr/lib/x86_64-linux-gnu/security/pam_canonicalize_user.so", + "/usr/lib/x86_64-linux-gnu/security/pam_debug.so", + "/usr/lib/x86_64-linux-gnu/security/pam_deny.so", + "/usr/lib/x86_64-linux-gnu/security/pam_echo.so", + "/usr/lib/x86_64-linux-gnu/security/pam_env.so", + "/usr/lib/x86_64-linux-gnu/security/pam_exec.so", + "/usr/lib/x86_64-linux-gnu/security/pam_faildelay.so", + "/usr/lib/x86_64-linux-gnu/security/pam_faillock.so", + "/usr/lib/x86_64-linux-gnu/security/pam_filter.so", + "/usr/lib/x86_64-linux-gnu/security/pam_ftp.so", + "/usr/lib/x86_64-linux-gnu/security/pam_group.so", + "/usr/lib/x86_64-linux-gnu/security/pam_issue.so", + "/usr/lib/x86_64-linux-gnu/security/pam_keyinit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_limits.so", + "/usr/lib/x86_64-linux-gnu/security/pam_listfile.so", + "/usr/lib/x86_64-linux-gnu/security/pam_localuser.so", + "/usr/lib/x86_64-linux-gnu/security/pam_loginuid.so", + "/usr/lib/x86_64-linux-gnu/security/pam_mail.so", + "/usr/lib/x86_64-linux-gnu/security/pam_mkhomedir.so", + "/usr/lib/x86_64-linux-gnu/security/pam_motd.so", + "/usr/lib/x86_64-linux-gnu/security/pam_namespace.so", + "/usr/lib/x86_64-linux-gnu/security/pam_nologin.so", + "/usr/lib/x86_64-linux-gnu/security/pam_permit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_pwhistory.so", + "/usr/lib/x86_64-linux-gnu/security/pam_rhosts.so", + "/usr/lib/x86_64-linux-gnu/security/pam_rootok.so", + "/usr/lib/x86_64-linux-gnu/security/pam_securetty.so", + "/usr/lib/x86_64-linux-gnu/security/pam_selinux.so", + "/usr/lib/x86_64-linux-gnu/security/pam_sepermit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_setquota.so", + "/usr/lib/x86_64-linux-gnu/security/pam_shells.so", + "/usr/lib/x86_64-linux-gnu/security/pam_stress.so", + "/usr/lib/x86_64-linux-gnu/security/pam_succeed_if.so", + "/usr/lib/x86_64-linux-gnu/security/pam_time.so", + "/usr/lib/x86_64-linux-gnu/security/pam_timestamp.so", + "/usr/lib/x86_64-linux-gnu/security/pam_tty_audit.so", + "/usr/lib/x86_64-linux-gnu/security/pam_umask.so", + "/usr/lib/x86_64-linux-gnu/security/pam_unix.so", + "/usr/lib/x86_64-linux-gnu/security/pam_userdb.so", + "/usr/lib/x86_64-linux-gnu/security/pam_usertype.so", + "/usr/lib/x86_64-linux-gnu/security/pam_warn.so", + "/usr/lib/x86_64-linux-gnu/security/pam_wheel.so", + "/usr/lib/x86_64-linux-gnu/security/pam_xauth.so", + "/usr/share/doc/libpam-modules/NEWS.Debian.gz", + "/usr/share/doc/libpam-modules/changelog.Debian.gz", + "/usr/share/doc/libpam-modules/changelog.gz", + "/usr/share/doc/libpam-modules/copyright", + "/usr/share/doc/libpam-modules/examples/upperLOWER.c", + "/usr/share/lintian/overrides/libpam-modules", + "/usr/share/pam-configs/mkhomedir" + ] + }, + { + "ID": "libpam-modules-bin@1.7.0-5", + "Name": "libpam-modules-bin", + "Identifier": { + "PURL": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64\u0026distro=debian-13.6", + "UID": "f047dc4396624183" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "DependsOn": [ + "libaudit1@1:4.0.2-2+b2", + "libc6@2.41-12+deb13u3", + "libcrypt1@1:4.4.38-1", + "libpam0g@1.7.0-5", + "libselinux1@3.8.1-1", + "libsystemd0@257.13-1~deb13u1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/systemd/system/pam_namespace.service", + "/usr/sbin/faillock", + "/usr/sbin/mkhomedir_helper", + "/usr/sbin/pam_namespace_helper", + "/usr/sbin/pam_timestamp_check", + "/usr/sbin/pwhistory_helper", + "/usr/sbin/unix_chkpwd", + "/usr/sbin/unix_update", + "/usr/share/doc/libpam-modules-bin/changelog.Debian.gz", + "/usr/share/doc/libpam-modules-bin/changelog.gz", + "/usr/share/doc/libpam-modules-bin/copyright", + "/usr/share/lintian/overrides/libpam-modules-bin" + ] + }, + { + "ID": "libpam-runtime@1.7.0-5", + "Name": "libpam-runtime", + "Identifier": { + "PURL": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all\u0026distro=debian-13.6", + "UID": "2e8bd19930283d52" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "all", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91", + "libpam-modules@1.7.0-5" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/sbin/pam-auth-update", + "/usr/sbin/pam_getenv", + "/usr/share/doc/libpam-runtime/changelog.Debian.gz", + "/usr/share/doc/libpam-runtime/changelog.gz", + "/usr/share/doc/libpam-runtime/copyright", + "/usr/share/lintian/overrides/libpam-runtime", + "/usr/share/locale/af/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/am/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ar/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/as/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/az/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/be/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bg/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bn_IN/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/bs/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ca/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/cs/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/cy/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/da/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/de/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/de_CH/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/el/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/eo/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/es/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/et/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/eu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/fa/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/fi/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/fr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ga/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/gl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/gu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/he/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/hi/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/hr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/hu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ia/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/id/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/is/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/it/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ja/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ka/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/kk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/km/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/kn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ko/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/kw_GB/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ky/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/lt/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/lv/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/mk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ml/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/mn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/mr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ms/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/my/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/nb/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ne/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/nl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/nn/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/or/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pa/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pt/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ro/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ru/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/si/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sl/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sq/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sr@latin/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/sv/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ta/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/te/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/tg/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/th/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/tr/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/uk/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/ur/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/vi/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/yo/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zh_HK/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/locale/zu/LC_MESSAGES/Linux-PAM.mo", + "/usr/share/man/man5/access.conf.5.gz", + "/usr/share/man/man5/faillock.conf.5.gz", + "/usr/share/man/man5/group.conf.5.gz", + "/usr/share/man/man5/limits.conf.5.gz", + "/usr/share/man/man5/namespace.conf.5.gz", + "/usr/share/man/man5/pam.conf.5.gz", + "/usr/share/man/man5/pam_env.conf.5.gz", + "/usr/share/man/man5/pwhistory.conf.5.gz", + "/usr/share/man/man5/sepermit.conf.5.gz", + "/usr/share/man/man5/time.conf.5.gz", + "/usr/share/man/man7/PAM.7.gz", + "/usr/share/man/man8/faillock.8.gz", + "/usr/share/man/man8/mkhomedir_helper.8.gz", + "/usr/share/man/man8/pam-auth-update.8.gz", + "/usr/share/man/man8/pam_access.8.gz", + "/usr/share/man/man8/pam_canonicalize_user.8.gz", + "/usr/share/man/man8/pam_debug.8.gz", + "/usr/share/man/man8/pam_deny.8.gz", + "/usr/share/man/man8/pam_echo.8.gz", + "/usr/share/man/man8/pam_env.8.gz", + "/usr/share/man/man8/pam_exec.8.gz", + "/usr/share/man/man8/pam_faildelay.8.gz", + "/usr/share/man/man8/pam_faillock.8.gz", + "/usr/share/man/man8/pam_filter.8.gz", + "/usr/share/man/man8/pam_ftp.8.gz", + "/usr/share/man/man8/pam_getenv.8.gz", + "/usr/share/man/man8/pam_group.8.gz", + "/usr/share/man/man8/pam_issue.8.gz", + "/usr/share/man/man8/pam_keyinit.8.gz", + "/usr/share/man/man8/pam_limits.8.gz", + "/usr/share/man/man8/pam_listfile.8.gz", + "/usr/share/man/man8/pam_localuser.8.gz", + "/usr/share/man/man8/pam_loginuid.8.gz", + "/usr/share/man/man8/pam_mail.8.gz", + "/usr/share/man/man8/pam_mkhomedir.8.gz", + "/usr/share/man/man8/pam_motd.8.gz", + "/usr/share/man/man8/pam_namespace.8.gz", + "/usr/share/man/man8/pam_namespace_helper.8.gz", + "/usr/share/man/man8/pam_nologin.8.gz", + "/usr/share/man/man8/pam_permit.8.gz", + "/usr/share/man/man8/pam_pwhistory.8.gz", + "/usr/share/man/man8/pam_rhosts.8.gz", + "/usr/share/man/man8/pam_rootok.8.gz", + "/usr/share/man/man8/pam_securetty.8.gz", + "/usr/share/man/man8/pam_selinux.8.gz", + "/usr/share/man/man8/pam_sepermit.8.gz", + "/usr/share/man/man8/pam_setquota.8.gz", + "/usr/share/man/man8/pam_shells.8.gz", + "/usr/share/man/man8/pam_stress.8.gz", + "/usr/share/man/man8/pam_succeed_if.8.gz", + "/usr/share/man/man8/pam_time.8.gz", + "/usr/share/man/man8/pam_timestamp.8.gz", + "/usr/share/man/man8/pam_timestamp_check.8.gz", + "/usr/share/man/man8/pam_tty_audit.8.gz", + "/usr/share/man/man8/pam_umask.8.gz", + "/usr/share/man/man8/pam_unix.8.gz", + "/usr/share/man/man8/pam_userdb.8.gz", + "/usr/share/man/man8/pam_usertype.8.gz", + "/usr/share/man/man8/pam_warn.8.gz", + "/usr/share/man/man8/pam_wheel.8.gz", + "/usr/share/man/man8/pam_xauth.8.gz", + "/usr/share/man/man8/pwhistory_helper.8.gz", + "/usr/share/man/man8/unix_chkpwd.8.gz", + "/usr/share/man/man8/unix_update.8.gz", + "/usr/share/pam-configs/unix", + "/usr/share/pam/common-account", + "/usr/share/pam/common-account.md5sums", + "/usr/share/pam/common-auth", + "/usr/share/pam/common-auth.md5sums", + "/usr/share/pam/common-password", + "/usr/share/pam/common-password.md5sums", + "/usr/share/pam/common-session", + "/usr/share/pam/common-session-noninteractive", + "/usr/share/pam/common-session-noninteractive.md5sums", + "/usr/share/pam/common-session.md5sums" + ] + }, + { + "ID": "libpam0g@1.7.0-5", + "Name": "libpam0g", + "Identifier": { + "PURL": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64\u0026distro=debian-13.6", + "UID": "5dbda12bb939f426" + }, + "Version": "1.7.0", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.7.0", + "SrcRelease": "5", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-1.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "BSD-tcp_wrappers", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "public-domain", + "Beerware" + ], + "Maintainer": "Sam Hartman \u003chartmans@debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91", + "libaudit1@1:4.0.2-2+b2", + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libpam.so.0.85.1", + "/usr/lib/x86_64-linux-gnu/libpam_misc.so.0.82.1", + "/usr/lib/x86_64-linux-gnu/libpamc.so.0.82.1", + "/usr/share/doc/libpam0g/Debian-PAM-MiniPolicy.gz", + "/usr/share/doc/libpam0g/README", + "/usr/share/doc/libpam0g/README.Debian", + "/usr/share/doc/libpam0g/TODO.Debian", + "/usr/share/doc/libpam0g/changelog.Debian.gz", + "/usr/share/doc/libpam0g/changelog.gz", + "/usr/share/doc/libpam0g/copyright", + "/usr/share/lintian/overrides/libpam0g" + ] + }, + { + "ID": "libpcre2-8-0@10.46-1~deb13u1", + "Name": "libpcre2-8-0", + "Identifier": { + "PURL": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "cb52bbc65534e04d" + }, + "Version": "10.46", + "Release": "1~deb13u1", + "Arch": "amd64", + "SrcName": "pcre2", + "SrcVersion": "10.46", + "SrcRelease": "1~deb13u1", + "Licenses": [ + "BSD-3-clause-Cambridge with BINARY LIBRARY-LIKE PACKAGES exception", + "BSD-3-Clause", + "X11", + "BSD-2-Clause", + "public-domain" + ], + "Maintainer": "Matthew Vernon \u003cmatthew@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0.14.0", + "/usr/share/doc/libpcre2-8-0/README.Debian", + "/usr/share/doc/libpcre2-8-0/changelog.Debian.gz", + "/usr/share/doc/libpcre2-8-0/changelog.gz", + "/usr/share/doc/libpcre2-8-0/copyright" + ] + }, + { + "ID": "libreadline8t64@8.2-6", + "Name": "libreadline8t64", + "Identifier": { + "PURL": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64\u0026distro=debian-13.6", + "UID": "3182860c74d6d08d" + }, + "Version": "8.2", + "Release": "6", + "Arch": "amd64", + "SrcName": "readline", + "SrcVersion": "8.2", + "SrcRelease": "6", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only", + "GFDL-1.3-no-invariants-or-later", + "GFDL-1.3-or-later", + "ISC-no-attribution" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libtinfo6@6.5+20250216-2", + "readline-common@8.2-6" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libhistory.so.8.2", + "/usr/lib/x86_64-linux-gnu/libreadline.so.8.2", + "/usr/share/doc/libreadline8t64/README.Debian", + "/usr/share/doc/libreadline8t64/USAGE", + "/usr/share/doc/libreadline8t64/changelog.Debian.gz", + "/usr/share/doc/libreadline8t64/changelog.gz", + "/usr/share/doc/libreadline8t64/copyright", + "/usr/share/doc/libreadline8t64/examples/Inputrc", + "/usr/share/doc/libreadline8t64/inputrc.arrows" + ] + }, + { + "ID": "libseccomp2@2.6.0-2", + "Name": "libseccomp2", + "Identifier": { + "PURL": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64\u0026distro=debian-13.6", + "UID": "872fd916e9134574" + }, + "Version": "2.6.0", + "Release": "2", + "Arch": "amd64", + "SrcName": "libseccomp", + "SrcVersion": "2.6.0", + "SrcRelease": "2", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "Kees Cook \u003ckees@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libseccomp.so.2.6.0", + "/usr/share/doc/libseccomp2/changelog.Debian.gz", + "/usr/share/doc/libseccomp2/changelog.gz", + "/usr/share/doc/libseccomp2/copyright" + ] + }, + { + "ID": "libselinux@2.5-15.el7", + "Name": "libselinux", + "Identifier": { + "PURL": "pkg:rpm/centos/libselinux@2.5-15.el7", + "UID": "6e37f06bf3057b37", + "BOMRef": "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed" + }, + "Version": "2.5", + "Release": "15.el7", + "SrcName": "libselinux", + "SrcVersion": "2.5", + "SrcRelease": "15.el7", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "libselinux1@3.8.1-1", + "Name": "libselinux1", + "Identifier": { + "PURL": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "dad36e826c972129" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "libselinux", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "public-domain", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libpcre2-8-0@10.46-1~deb13u1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/tmpfiles.d/libselinux1.conf", + "/usr/lib/x86_64-linux-gnu/libselinux.so.1", + "/usr/share/doc/libselinux1/changelog.Debian.gz", + "/usr/share/doc/libselinux1/copyright" + ] + }, + { + "ID": "libsemanage-common@3.8.1-1", + "Name": "libsemanage-common", + "Identifier": { + "PURL": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all\u0026distro=debian-13.6", + "UID": "82e27fcff653c8e2" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "all", + "SrcName": "libsemanage", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/libsemanage-common/changelog.Debian.gz", + "/usr/share/doc/libsemanage-common/copyright", + "/usr/share/man/man5/semanage.conf.5.gz" + ] + }, + { + "ID": "libsemanage2@3.8.1-1", + "Name": "libsemanage2", + "Identifier": { + "PURL": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "dc362d6ee87a25c7" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "libsemanage", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libaudit1@1:4.0.2-2+b2", + "libbz2-1.0@1.0.8-6", + "libc6@2.41-12+deb13u3", + "libselinux1@3.8.1-1", + "libsemanage-common@3.8.1-1", + "libsepol2@3.8.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsemanage.so.2", + "/usr/share/doc/libsemanage2/changelog.Debian.gz", + "/usr/share/doc/libsemanage2/copyright" + ] + }, + { + "ID": "libsepol2@3.8.1-1", + "Name": "libsepol2", + "Identifier": { + "PURL": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64\u0026distro=debian-13.6", + "UID": "8a467f0e7ce023a5" + }, + "Version": "3.8.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "libsepol", + "SrcVersion": "3.8.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "Zlib", + "GPL-2.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsepol.so.2", + "/usr/share/doc/libsepol2/changelog.Debian.gz", + "/usr/share/doc/libsepol2/copyright" + ] + }, + { + "ID": "libsmartcols1@2.41-5", + "Name": "libsmartcols1", + "Identifier": { + "PURL": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "4d10e734941c1d1d" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsmartcols.so.1.1.0", + "/usr/share/doc/libsmartcols1/NEWS.Debian.gz", + "/usr/share/doc/libsmartcols1/changelog.Debian.gz", + "/usr/share/doc/libsmartcols1/changelog.gz", + "/usr/share/doc/libsmartcols1/copyright", + "/usr/share/lintian/overrides/libsmartcols1" + ] + }, + { + "ID": "libsqlite3-0@3.46.1-7+deb13u1", + "Name": "libsqlite3-0", + "Identifier": { + "PURL": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "49853bfc5d923fa8" + }, + "Version": "3.46.1", + "Release": "7+deb13u1", + "Arch": "amd64", + "SrcName": "sqlite3", + "SrcVersion": "3.46.1", + "SrcRelease": "7+deb13u1", + "Licenses": [ + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Laszlo Boszormenyi (GCS) \u003cgcs@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6", + "/usr/share/doc/libsqlite3-0/README.Debian", + "/usr/share/doc/libsqlite3-0/changelog.Debian.gz", + "/usr/share/doc/libsqlite3-0/changelog.gz", + "/usr/share/doc/libsqlite3-0/changelog.html.gz", + "/usr/share/doc/libsqlite3-0/copyright" + ] + }, + { + "ID": "libssl3t64@3.5.6-1~deb13u2", + "Name": "libssl3t64", + "Identifier": { + "PURL": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64\u0026distro=debian-13.6", + "UID": "1ca83fc0831fc12a" + }, + "Version": "3.5.6", + "Release": "1~deb13u2", + "Arch": "amd64", + "SrcName": "openssl", + "SrcVersion": "3.5.6", + "SrcRelease": "1~deb13u2", + "Licenses": [ + "Apache-2.0", + "Artistic-2.0", + "GPL-1.0-or-later", + "GPL-1.0-only" + ], + "Maintainer": "Debian OpenSSL Team \u003cpkg-openssl-devel@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libzstd1@1.5.7+dfsg-1", + "openssl-provider-legacy@3.5.6-1~deb13u2", + "zlib1g@1:1.3.dfsg+really1.3.1-1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/engines-3/afalg.so", + "/usr/lib/x86_64-linux-gnu/engines-3/loader_attic.so", + "/usr/lib/x86_64-linux-gnu/engines-3/padlock.so", + "/usr/lib/x86_64-linux-gnu/libcrypto.so.3", + "/usr/lib/x86_64-linux-gnu/libssl.so.3", + "/usr/share/doc/libssl3t64/NEWS.Debian.gz", + "/usr/share/doc/libssl3t64/changelog.Debian.gz", + "/usr/share/doc/libssl3t64/changelog.gz", + "/usr/share/doc/libssl3t64/copyright", + "/usr/share/lintian/overrides/libssl3t64" + ] + }, + { + "ID": "libstdc++6@14.2.0-19", + "Name": "libstdc++6", + "Identifier": { + "PURL": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64\u0026distro=debian-13.6", + "UID": "44612415c1730efa" + }, + "Version": "14.2.0", + "Release": "19", + "Arch": "amd64", + "SrcName": "gcc-14", + "SrcVersion": "14.2.0", + "SrcRelease": "19", + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "DependsOn": [ + "gcc-14-base@14.2.0-19", + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33", + "/usr/share/gcc/python/libstdcxx/__init__.py", + "/usr/share/gcc/python/libstdcxx/v6/__init__.py", + "/usr/share/gcc/python/libstdcxx/v6/printers.py", + "/usr/share/gcc/python/libstdcxx/v6/xmethods.py", + "/usr/share/gdb/auto-load/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33-gdb.py" + ] + }, + { + "ID": "libsystemd0@257.13-1~deb13u1", + "Name": "libsystemd0", + "Identifier": { + "PURL": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "d7f6e0bcc5fd6683" + }, + "Version": "257.13", + "Release": "1~deb13u1", + "Arch": "amd64", + "SrcName": "systemd", + "SrcVersion": "257.13", + "SrcRelease": "1~deb13u1", + "Licenses": [ + "LGPL-2.1-or-later", + "CC0-1.0", + "GPL-2 with Linux-syscall-note exception", + "MIT", + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libcap2@1:2.75-10+deb13u1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libsystemd.so.0.40.0", + "/usr/share/doc/libsystemd0/NEWS.Debian.gz", + "/usr/share/doc/libsystemd0/changelog.Debian.gz", + "/usr/share/doc/libsystemd0/copyright" + ] + }, + { + "ID": "libtinfo6@6.5+20250216-2", + "Name": "libtinfo6", + "Identifier": { + "PURL": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64\u0026distro=debian-13.6", + "UID": "12095a35cca3541c" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Licenses": [ + "MIT/X11", + "X11", + "BSD-3-Clause" + ], + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libtic.so.6.5", + "/usr/lib/x86_64-linux-gnu/libtinfo.so.6.5", + "/usr/share/doc/libtinfo6/changelog.Debian.gz", + "/usr/share/doc/libtinfo6/changelog.gz", + "/usr/share/doc/libtinfo6/copyright" + ] + }, + { + "ID": "libudev1@257.13-1~deb13u1", + "Name": "libudev1", + "Identifier": { + "PURL": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "151b8b6a80baf5ce" + }, + "Version": "257.13", + "Release": "1~deb13u1", + "Arch": "amd64", + "SrcName": "systemd", + "SrcVersion": "257.13", + "SrcRelease": "1~deb13u1", + "Licenses": [ + "LGPL-2.1-or-later", + "CC0-1.0", + "GPL-2 with Linux-syscall-note exception", + "MIT", + "public-domain", + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libcap2@1:2.75-10+deb13u1+b1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libudev.so.1.7.10", + "/usr/share/doc/libudev1/NEWS.Debian.gz", + "/usr/share/doc/libudev1/changelog.Debian.gz", + "/usr/share/doc/libudev1/copyright" + ] + }, + { + "ID": "libuuid1@2.41-5", + "Name": "libuuid1", + "Identifier": { + "PURL": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "ad45ef419bdcd798" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libuuid.so.1.3.0", + "/usr/share/doc/libuuid1/NEWS.Debian.gz", + "/usr/share/doc/libuuid1/changelog.Debian.gz", + "/usr/share/doc/libuuid1/changelog.gz", + "/usr/share/doc/libuuid1/copyright" + ] + }, + { + "ID": "libxxhash0@0.8.3-2", + "Name": "libxxhash0", + "Identifier": { + "PURL": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64\u0026distro=debian-13.6", + "UID": "413b9b44940ce169" + }, + "Version": "0.8.3", + "Release": "2", + "Arch": "amd64", + "SrcName": "xxhash", + "SrcVersion": "0.8.3", + "SrcRelease": "2", + "Licenses": [ + "BSD-2-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Josue Ortega \u003cjosue@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libxxhash.so.0.8.3", + "/usr/share/doc/libxxhash0/changelog.Debian.gz", + "/usr/share/doc/libxxhash0/changelog.gz", + "/usr/share/doc/libxxhash0/copyright" + ] + }, + { + "ID": "libzstd1@1.5.7+dfsg-1", + "Name": "libzstd1", + "Identifier": { + "PURL": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64\u0026distro=debian-13.6", + "UID": "7262601866572971" + }, + "Version": "1.5.7+dfsg", + "Release": "1", + "Arch": "amd64", + "SrcName": "libzstd", + "SrcVersion": "1.5.7+dfsg", + "SrcRelease": "1", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "Zlib", + "MIT" + ], + "Maintainer": "RPM packaging team \u003cteam+pkg-rpm@tracker.debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7", + "/usr/share/doc/libzstd1/changelog.Debian.gz", + "/usr/share/doc/libzstd1/changelog.gz", + "/usr/share/doc/libzstd1/copyright" + ] + }, + { + "ID": "login@1:4.16.0-2+really2.41-5", + "Name": "login", + "Identifier": { + "PURL": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "468f3d4a374ef1" + }, + "Version": "4.16.0-2+really2.41", + "Release": "5", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "DependsOn": [ + "libaudit1@1:4.0.2-2+b2", + "libc6@2.41-12+deb13u3", + "libcrypt1@1:4.4.38-1", + "libpam-modules@1.7.0-5", + "libpam-runtime@1.7.0-5", + "libpam0g@1.7.0-5" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/login", + "/usr/bin/newgrp", + "/usr/sbin/nologin", + "/usr/share/bash-completion/completions/newgrp", + "/usr/share/doc/login/NEWS.Debian.gz", + "/usr/share/doc/login/changelog.Debian.gz", + "/usr/share/doc/login/changelog.gz", + "/usr/share/doc/login/copyright", + "/usr/share/lintian/overrides/login", + "/usr/share/man/de/man1/login.1.gz", + "/usr/share/man/de/man8/nologin.8.gz", + "/usr/share/man/fr/man1/login.1.gz", + "/usr/share/man/man1/login.1.gz", + "/usr/share/man/man1/newgrp.1.gz", + "/usr/share/man/man8/nologin.8.gz", + "/usr/share/man/pl/man1/login.1.gz", + "/usr/share/man/pl/man1/newgrp.1.gz", + "/usr/share/man/pl/man8/nologin.8.gz", + "/usr/share/man/ro/man1/login.1.gz", + "/usr/share/man/ro/man1/newgrp.1.gz", + "/usr/share/man/ro/man8/nologin.8.gz", + "/usr/share/man/sr/man1/login.1.gz", + "/usr/share/man/sr/man8/nologin.8.gz", + "/usr/share/man/uk/man1/login.1.gz", + "/usr/share/man/uk/man1/newgrp.1.gz", + "/usr/share/man/uk/man8/nologin.8.gz" + ] + }, + { + "ID": "login.defs@1:4.17.4-2", + "Name": "login.defs", + "Identifier": { + "PURL": "pkg:deb/debian/login.defs@4.17.4-2?arch=all\u0026distro=debian-13.6\u0026epoch=1", + "UID": "b2ebc9108569350a" + }, + "Version": "4.17.4", + "Release": "2", + "Epoch": 1, + "Arch": "all", + "SrcName": "shadow", + "SrcVersion": "4.17.4", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-1.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/login.defs/NEWS.Debian.gz", + "/usr/share/doc/login.defs/changelog.Debian.gz", + "/usr/share/doc/login.defs/changelog.gz", + "/usr/share/doc/login.defs/copyright", + "/usr/share/man/de/man5/login.defs.5.gz", + "/usr/share/man/fr/man5/login.defs.5.gz", + "/usr/share/man/it/man5/login.defs.5.gz", + "/usr/share/man/ja/man5/login.defs.5.gz", + "/usr/share/man/man5/login.defs.5.gz", + "/usr/share/man/ru/man5/login.defs.5.gz", + "/usr/share/man/uk/man5/login.defs.5.gz", + "/usr/share/man/zh_CN/man5/login.defs.5.gz" + ] + }, + { + "ID": "mawk@1.3.4.20250131-1", + "Name": "mawk", + "Identifier": { + "PURL": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64\u0026distro=debian-13.6", + "UID": "9048a1b0d5acbb7e" + }, + "Version": "1.3.4.20250131", + "Release": "1", + "Arch": "amd64", + "SrcName": "mawk", + "SrcVersion": "1.3.4.20250131", + "SrcRelease": "1", + "Licenses": [ + "GPL-2.0-only", + "X11", + "CC-BY-3.0" + ], + "Maintainer": "Boyuan Yang \u003cbyang@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/mawk", + "/usr/share/doc/mawk/ACKNOWLEDGMENT", + "/usr/share/doc/mawk/README", + "/usr/share/doc/mawk/changelog.Debian.gz", + "/usr/share/doc/mawk/changelog.gz", + "/usr/share/doc/mawk/copyright", + "/usr/share/doc/mawk/examples/ct_length.awk", + "/usr/share/doc/mawk/examples/decl.awk", + "/usr/share/doc/mawk/examples/deps.awk", + "/usr/share/doc/mawk/examples/eatc.awk", + "/usr/share/doc/mawk/examples/gdecl.awk", + "/usr/share/doc/mawk/examples/hcal", + "/usr/share/doc/mawk/examples/hical", + "/usr/share/doc/mawk/examples/nocomment.awk", + "/usr/share/doc/mawk/examples/primes.awk", + "/usr/share/doc/mawk/examples/qsort.awk", + "/usr/share/man/man1/mawk.1.gz", + "/usr/share/man/man7/mawk-arrays.7.gz", + "/usr/share/man/man7/mawk-code.7.gz" + ] + }, + { + "ID": "mount@2.41-5", + "Name": "mount", + "Identifier": { + "PURL": "pkg:deb/debian/mount@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "c6fdc5cf989db569" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/mount", + "/usr/bin/umount", + "/usr/sbin/losetup", + "/usr/sbin/swapoff", + "/usr/sbin/swapon", + "/usr/share/bash-completion/completions/losetup", + "/usr/share/bash-completion/completions/mount", + "/usr/share/bash-completion/completions/swapoff", + "/usr/share/bash-completion/completions/swapon", + "/usr/share/bash-completion/completions/umount", + "/usr/share/doc/mount/NEWS.Debian.gz", + "/usr/share/doc/mount/changelog.Debian.gz", + "/usr/share/doc/mount/changelog.gz", + "/usr/share/doc/mount/copyright", + "/usr/share/doc/mount/examples/filesystems", + "/usr/share/doc/mount/examples/fstab", + "/usr/share/doc/mount/examples/mount.fstab", + "/usr/share/doc/mount/mount.txt", + "/usr/share/lintian/overrides/mount", + "/usr/share/man/man5/fstab.5.gz", + "/usr/share/man/man8/losetup.8.gz", + "/usr/share/man/man8/mount.8.gz", + "/usr/share/man/man8/swapon.8.gz", + "/usr/share/man/man8/umount.8.gz" + ] + }, + { + "ID": "ncurses-base@6.5+20250216-2", + "Name": "ncurses-base", + "Identifier": { + "PURL": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all\u0026distro=debian-13.6", + "UID": "76a1fb5936f344dc" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "all", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Licenses": [ + "MIT/X11", + "X11", + "BSD-3-Clause" + ], + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/ncurses-base/FAQ", + "/usr/share/doc/ncurses-base/TODO.Debian", + "/usr/share/doc/ncurses-base/changelog.Debian.gz", + "/usr/share/doc/ncurses-base/changelog.gz", + "/usr/share/doc/ncurses-base/copyright", + "/usr/share/lintian/overrides/ncurses-base", + "/usr/share/tabset/std", + "/usr/share/tabset/stdcrt", + "/usr/share/tabset/vt100", + "/usr/share/tabset/vt300", + "/usr/share/terminfo/E/Eterm", + "/usr/share/terminfo/a/ansi", + "/usr/share/terminfo/c/cons25", + "/usr/share/terminfo/c/cygwin", + "/usr/share/terminfo/d/dumb", + "/usr/share/terminfo/h/hurd", + "/usr/share/terminfo/l/linux", + "/usr/share/terminfo/m/mach", + "/usr/share/terminfo/m/mach-bold", + "/usr/share/terminfo/m/mach-color", + "/usr/share/terminfo/m/mach-gnu", + "/usr/share/terminfo/m/mach-gnu-color", + "/usr/share/terminfo/p/pcansi", + "/usr/share/terminfo/r/rxvt", + "/usr/share/terminfo/r/rxvt-basic", + "/usr/share/terminfo/r/rxvt-unicode", + "/usr/share/terminfo/r/rxvt-unicode-256color", + "/usr/share/terminfo/s/screen", + "/usr/share/terminfo/s/screen-256color", + "/usr/share/terminfo/s/screen-256color-bce", + "/usr/share/terminfo/s/screen-bce", + "/usr/share/terminfo/s/screen-s", + "/usr/share/terminfo/s/screen-w", + "/usr/share/terminfo/s/screen.xterm-256color", + "/usr/share/terminfo/s/sun", + "/usr/share/terminfo/t/tmux", + "/usr/share/terminfo/t/tmux-256color", + "/usr/share/terminfo/v/vt100", + "/usr/share/terminfo/v/vt102", + "/usr/share/terminfo/v/vt220", + "/usr/share/terminfo/v/vt52", + "/usr/share/terminfo/w/wsvt25", + "/usr/share/terminfo/w/wsvt25m", + "/usr/share/terminfo/x/xterm", + "/usr/share/terminfo/x/xterm-256color", + "/usr/share/terminfo/x/xterm-color", + "/usr/share/terminfo/x/xterm-mono", + "/usr/share/terminfo/x/xterm-r5", + "/usr/share/terminfo/x/xterm-r6", + "/usr/share/terminfo/x/xterm-vt220", + "/usr/share/terminfo/x/xterm-xfree86" + ] + }, + { + "ID": "ncurses-bin@6.5+20250216-2", + "Name": "ncurses-bin", + "Identifier": { + "PURL": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64\u0026distro=debian-13.6", + "UID": "d03e89ad6a7a5243" + }, + "Version": "6.5+20250216", + "Release": "2", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.5+20250216", + "SrcRelease": "2", + "Licenses": [ + "MIT/X11", + "X11", + "BSD-3-Clause" + ], + "Maintainer": "Ncurses Maintainers \u003cncurses@packages.debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/clear", + "/usr/bin/infocmp", + "/usr/bin/tabs", + "/usr/bin/tic", + "/usr/bin/toe", + "/usr/bin/tput", + "/usr/bin/tset", + "/usr/share/doc/ncurses-bin/changelog.Debian.gz", + "/usr/share/doc/ncurses-bin/changelog.gz", + "/usr/share/doc/ncurses-bin/copyright", + "/usr/share/man/man1/captoinfo.1.gz", + "/usr/share/man/man1/clear.1.gz", + "/usr/share/man/man1/infocmp.1.gz", + "/usr/share/man/man1/infotocap.1.gz", + "/usr/share/man/man1/tabs.1.gz", + "/usr/share/man/man1/tic.1.gz", + "/usr/share/man/man1/toe.1.gz", + "/usr/share/man/man1/tput.1.gz", + "/usr/share/man/man1/tset.1.gz", + "/usr/share/man/man5/scr_dump.5.gz", + "/usr/share/man/man5/term.5.gz", + "/usr/share/man/man5/terminfo.5.gz", + "/usr/share/man/man5/user_caps.5.gz", + "/usr/share/man/man7/term.7.gz" + ] + }, + { + "ID": "netbase@6.5", + "Name": "netbase", + "Identifier": { + "PURL": "pkg:deb/debian/netbase@6.5?arch=all\u0026distro=debian-13.6", + "UID": "b9a2c240e75fe15e" + }, + "Version": "6.5", + "Arch": "all", + "SrcName": "netbase", + "SrcVersion": "6.5", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Marco d'Itri \u003cmd@linux.it\u003e", + "Layer": { + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + "InstalledFiles": [ + "/usr/share/doc/netbase/changelog.gz", + "/usr/share/doc/netbase/copyright" + ] + }, + { + "ID": "openssl@3.5.6-1~deb13u2", + "Name": "openssl", + "Identifier": { + "PURL": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64\u0026distro=debian-13.6", + "UID": "8f9e5d7117307079" + }, + "Version": "3.5.6", + "Release": "1~deb13u2", + "Arch": "amd64", + "SrcName": "openssl", + "SrcVersion": "3.5.6", + "SrcRelease": "1~deb13u2", + "Licenses": [ + "Apache-2.0", + "Artistic-2.0", + "GPL-1.0-or-later", + "GPL-1.0-only" + ], + "Maintainer": "Debian OpenSSL Team \u003cpkg-openssl-devel@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libssl3t64@3.5.6-1~deb13u2" + ], + "Layer": { + "DiffID": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + "InstalledFiles": [ + "/usr/bin/c_rehash", + "/usr/bin/openssl", + "/usr/lib/ssl/misc/CA.pl", + "/usr/lib/ssl/misc/tsget.pl", + "/usr/share/doc/openssl/HOWTO/certificates.txt.gz", + "/usr/share/doc/openssl/HOWTO/documenting-functions-and-macros.md.gz", + "/usr/share/doc/openssl/HOWTO/keys.txt.gz", + "/usr/share/doc/openssl/NEWS.md.gz", + "/usr/share/doc/openssl/README-ENGINES.md.gz", + "/usr/share/doc/openssl/README-PROVIDERS.md.gz", + "/usr/share/doc/openssl/README-QUIC.md.gz", + "/usr/share/doc/openssl/README.Debian", + "/usr/share/doc/openssl/README.md.gz", + "/usr/share/doc/openssl/changelog.Debian.gz", + "/usr/share/doc/openssl/changelog.gz", + "/usr/share/doc/openssl/copyright", + "/usr/share/doc/openssl/fingerprints.txt", + "/usr/share/lintian/overrides/openssl", + "/usr/share/man/man1/CA.pl.1ssl.gz", + "/usr/share/man/man1/openssl-asn1parse.1ssl.gz", + "/usr/share/man/man1/openssl-ca.1ssl.gz", + "/usr/share/man/man1/openssl-ciphers.1ssl.gz", + "/usr/share/man/man1/openssl-cmds.1ssl.gz", + "/usr/share/man/man1/openssl-cmp.1ssl.gz", + "/usr/share/man/man1/openssl-cms.1ssl.gz", + "/usr/share/man/man1/openssl-crl.1ssl.gz", + "/usr/share/man/man1/openssl-crl2pkcs7.1ssl.gz", + "/usr/share/man/man1/openssl-dgst.1ssl.gz", + "/usr/share/man/man1/openssl-dhparam.1ssl.gz", + "/usr/share/man/man1/openssl-dsa.1ssl.gz", + "/usr/share/man/man1/openssl-dsaparam.1ssl.gz", + "/usr/share/man/man1/openssl-ec.1ssl.gz", + "/usr/share/man/man1/openssl-ecparam.1ssl.gz", + "/usr/share/man/man1/openssl-enc.1ssl.gz", + "/usr/share/man/man1/openssl-engine.1ssl.gz", + "/usr/share/man/man1/openssl-errstr.1ssl.gz", + "/usr/share/man/man1/openssl-fipsinstall.1ssl.gz", + "/usr/share/man/man1/openssl-format-options.1ssl.gz", + "/usr/share/man/man1/openssl-gendsa.1ssl.gz", + "/usr/share/man/man1/openssl-genpkey.1ssl.gz", + "/usr/share/man/man1/openssl-genrsa.1ssl.gz", + "/usr/share/man/man1/openssl-info.1ssl.gz", + "/usr/share/man/man1/openssl-kdf.1ssl.gz", + "/usr/share/man/man1/openssl-list.1ssl.gz", + "/usr/share/man/man1/openssl-mac.1ssl.gz", + "/usr/share/man/man1/openssl-namedisplay-options.1ssl.gz", + "/usr/share/man/man1/openssl-nseq.1ssl.gz", + "/usr/share/man/man1/openssl-ocsp.1ssl.gz", + "/usr/share/man/man1/openssl-passphrase-options.1ssl.gz", + "/usr/share/man/man1/openssl-passwd.1ssl.gz", + "/usr/share/man/man1/openssl-pkcs12.1ssl.gz", + "/usr/share/man/man1/openssl-pkcs7.1ssl.gz", + "/usr/share/man/man1/openssl-pkcs8.1ssl.gz", + "/usr/share/man/man1/openssl-pkey.1ssl.gz", + "/usr/share/man/man1/openssl-pkeyparam.1ssl.gz", + "/usr/share/man/man1/openssl-pkeyutl.1ssl.gz", + "/usr/share/man/man1/openssl-prime.1ssl.gz", + "/usr/share/man/man1/openssl-rand.1ssl.gz", + "/usr/share/man/man1/openssl-rehash.1ssl.gz", + "/usr/share/man/man1/openssl-req.1ssl.gz", + "/usr/share/man/man1/openssl-rsa.1ssl.gz", + "/usr/share/man/man1/openssl-rsautl.1ssl.gz", + "/usr/share/man/man1/openssl-s_client.1ssl.gz", + "/usr/share/man/man1/openssl-s_server.1ssl.gz", + "/usr/share/man/man1/openssl-s_time.1ssl.gz", + "/usr/share/man/man1/openssl-sess_id.1ssl.gz", + "/usr/share/man/man1/openssl-skeyutl.1ssl.gz", + "/usr/share/man/man1/openssl-smime.1ssl.gz", + "/usr/share/man/man1/openssl-speed.1ssl.gz", + "/usr/share/man/man1/openssl-spkac.1ssl.gz", + "/usr/share/man/man1/openssl-srp.1ssl.gz", + "/usr/share/man/man1/openssl-storeutl.1ssl.gz", + "/usr/share/man/man1/openssl-ts.1ssl.gz", + "/usr/share/man/man1/openssl-verification-options.1ssl.gz", + "/usr/share/man/man1/openssl-verify.1ssl.gz", + "/usr/share/man/man1/openssl-version.1ssl.gz", + "/usr/share/man/man1/openssl-x509.1ssl.gz", + "/usr/share/man/man1/openssl.1ssl.gz", + "/usr/share/man/man1/tsget.1ssl.gz", + "/usr/share/man/man5/config.5ssl.gz", + "/usr/share/man/man5/fips_config.5ssl.gz", + "/usr/share/man/man5/x509v3_config.5ssl.gz", + "/usr/share/man/man7/EVP_ASYM_CIPHER-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_ASYM_CIPHER-SM2.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-AES.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-ARIA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-BLOWFISH.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-CAMELLIA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-CAST.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-CHACHA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-DES.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-IDEA.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-NULL.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-RC2.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-RC4.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-RC5.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-SEED.7ssl.gz", + "/usr/share/man/man7/EVP_CIPHER-SM4.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-ARGON2.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-HKDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-HMAC-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-KB.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-KRB5KDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PBKDF1.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PBKDF2.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PKCS12KDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-PVKKDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-SCRYPT.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-SS.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-SSHKDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-TLS13_KDF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-TLS1_PRF.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-X942-ASN1.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-X942-CONCAT.7ssl.gz", + "/usr/share/man/man7/EVP_KDF-X963.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-EC.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-ML-KEM.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_KEM-X25519.7ssl.gz", + "/usr/share/man/man7/EVP_KEYEXCH-DH.7ssl.gz", + "/usr/share/man/man7/EVP_KEYEXCH-ECDH.7ssl.gz", + "/usr/share/man/man7/EVP_KEYEXCH-X25519.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-BLAKE2.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-CMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-GMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-HMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-KMAC.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-Poly1305.7ssl.gz", + "/usr/share/man/man7/EVP_MAC-Siphash.7ssl.gz", + "/usr/share/man/man7/EVP_MD-BLAKE2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-KECCAK.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD4.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD5-SHA1.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MD5.7ssl.gz", + "/usr/share/man/man7/EVP_MD-MDC2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-NULL.7ssl.gz", + "/usr/share/man/man7/EVP_MD-RIPEMD160.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHA1.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHA2.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHA3.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SHAKE.7ssl.gz", + "/usr/share/man/man7/EVP_MD-SM3.7ssl.gz", + "/usr/share/man/man7/EVP_MD-WHIRLPOOL.7ssl.gz", + "/usr/share/man/man7/EVP_MD-common.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-DH.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-EC.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-FFC.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-HMAC.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-ML-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-ML-KEM.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-SLH-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-SM2.7ssl.gz", + "/usr/share/man/man7/EVP_PKEY-X25519.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-CRNG-TEST.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-CTR-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-HASH-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-HMAC-DRBG.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-JITTER.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-SEED-SRC.7ssl.gz", + "/usr/share/man/man7/EVP_RAND-TEST-RAND.7ssl.gz", + "/usr/share/man/man7/EVP_RAND.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-ECDSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-ED25519.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-HMAC.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-ML-DSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-RSA.7ssl.gz", + "/usr/share/man/man7/EVP_SIGNATURE-SLH-DSA.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-FIPS.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-base.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-default.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-legacy.7ssl.gz", + "/usr/share/man/man7/OSSL_PROVIDER-null.7ssl.gz", + "/usr/share/man/man7/OSSL_STORE-winstore.7ssl.gz", + "/usr/share/man/man7/RAND.7ssl.gz", + "/usr/share/man/man7/RSA-PSS.7ssl.gz", + "/usr/share/man/man7/X25519.7ssl.gz", + "/usr/share/man/man7/bio.7ssl.gz", + "/usr/share/man/man7/ct.7ssl.gz", + "/usr/share/man/man7/des_modes.7ssl.gz", + "/usr/share/man/man7/evp.7ssl.gz", + "/usr/share/man/man7/fips_module.7ssl.gz", + "/usr/share/man/man7/life_cycle-cipher.7ssl.gz", + "/usr/share/man/man7/life_cycle-digest.7ssl.gz", + "/usr/share/man/man7/life_cycle-kdf.7ssl.gz", + "/usr/share/man/man7/life_cycle-mac.7ssl.gz", + "/usr/share/man/man7/life_cycle-pkey.7ssl.gz", + "/usr/share/man/man7/life_cycle-rand.7ssl.gz", + "/usr/share/man/man7/openssl-core.h.7ssl.gz", + "/usr/share/man/man7/openssl-core_dispatch.h.7ssl.gz", + "/usr/share/man/man7/openssl-core_names.h.7ssl.gz", + "/usr/share/man/man7/openssl-env.7ssl.gz", + "/usr/share/man/man7/openssl-glossary.7ssl.gz", + "/usr/share/man/man7/openssl-qlog.7ssl.gz", + "/usr/share/man/man7/openssl-quic-concurrency.7ssl.gz", + "/usr/share/man/man7/openssl-quic.7ssl.gz", + "/usr/share/man/man7/openssl-threads.7ssl.gz", + "/usr/share/man/man7/openssl_user_macros.7ssl.gz", + "/usr/share/man/man7/ossl-guide-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-libcrypto-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-libraries-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-libssl-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-migration.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-client-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-client-non-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-multi-stream.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-server-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-quic-server-non-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-client-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-client-non-block.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-introduction.7ssl.gz", + "/usr/share/man/man7/ossl-guide-tls-server-block.7ssl.gz", + "/usr/share/man/man7/ossl_store-file.7ssl.gz", + "/usr/share/man/man7/ossl_store.7ssl.gz", + "/usr/share/man/man7/passphrase-encoding.7ssl.gz", + "/usr/share/man/man7/property.7ssl.gz", + "/usr/share/man/man7/provider-asym_cipher.7ssl.gz", + "/usr/share/man/man7/provider-base.7ssl.gz", + "/usr/share/man/man7/provider-cipher.7ssl.gz", + "/usr/share/man/man7/provider-decoder.7ssl.gz", + "/usr/share/man/man7/provider-digest.7ssl.gz", + "/usr/share/man/man7/provider-encoder.7ssl.gz", + "/usr/share/man/man7/provider-kdf.7ssl.gz", + "/usr/share/man/man7/provider-kem.7ssl.gz", + "/usr/share/man/man7/provider-keyexch.7ssl.gz", + "/usr/share/man/man7/provider-keymgmt.7ssl.gz", + "/usr/share/man/man7/provider-mac.7ssl.gz", + "/usr/share/man/man7/provider-object.7ssl.gz", + "/usr/share/man/man7/provider-rand.7ssl.gz", + "/usr/share/man/man7/provider-signature.7ssl.gz", + "/usr/share/man/man7/provider-skeymgmt.7ssl.gz", + "/usr/share/man/man7/provider-storemgmt.7ssl.gz", + "/usr/share/man/man7/provider.7ssl.gz", + "/usr/share/man/man7/proxy-certificates.7ssl.gz", + "/usr/share/man/man7/x509.7ssl.gz" + ] + }, + { + "ID": "openssl-provider-legacy@3.5.6-1~deb13u2", + "Name": "openssl-provider-legacy", + "Identifier": { + "PURL": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64\u0026distro=debian-13.6", + "UID": "f40d953a73d33d41" + }, + "Version": "3.5.6", + "Release": "1~deb13u2", + "Arch": "amd64", + "SrcName": "openssl", + "SrcVersion": "3.5.6", + "SrcRelease": "1~deb13u2", + "Licenses": [ + "Apache-2.0", + "Artistic-2.0", + "GPL-1.0-or-later", + "GPL-1.0-only" + ], + "Maintainer": "Debian OpenSSL Team \u003cpkg-openssl-devel@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libssl3t64@3.5.6-1~deb13u2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/ossl-modules/legacy.so", + "/usr/share/doc/openssl-provider-legacy/changelog.Debian.gz", + "/usr/share/doc/openssl-provider-legacy/changelog.gz", + "/usr/share/doc/openssl-provider-legacy/copyright" + ] + }, + { + "ID": "passwd@1:4.17.4-2", + "Name": "passwd", + "Identifier": { + "PURL": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "5c48c1fc5bd92522" + }, + "Version": "4.17.4", + "Release": "2", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "shadow", + "SrcVersion": "4.17.4", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-1.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "DependsOn": [ + "base-passwd@3.6.7", + "libacl1@2.3.2-2+b1", + "libattr1@1:2.5.2-3", + "libaudit1@1:4.0.2-2+b2", + "libbsd0@0.12.2-2", + "libc6@2.41-12+deb13u3", + "libcrypt1@1:4.4.38-1", + "libpam-modules@1.7.0-5", + "libpam0g@1.7.0-5", + "libselinux1@3.8.1-1", + "libsemanage2@3.8.1-1", + "login.defs@1:4.17.4-2" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/chage", + "/usr/bin/chfn", + "/usr/bin/chsh", + "/usr/bin/expiry", + "/usr/bin/gpasswd", + "/usr/bin/passwd", + "/usr/lib/tmpfiles.d/passwd.conf", + "/usr/sbin/chgpasswd", + "/usr/sbin/chpasswd", + "/usr/sbin/groupadd", + "/usr/sbin/groupdel", + "/usr/sbin/groupmod", + "/usr/sbin/grpck", + "/usr/sbin/grpconv", + "/usr/sbin/grpunconv", + "/usr/sbin/newusers", + "/usr/sbin/pwck", + "/usr/sbin/pwconv", + "/usr/sbin/pwunconv", + "/usr/sbin/shadowconfig", + "/usr/sbin/useradd", + "/usr/sbin/userdel", + "/usr/sbin/usermod", + "/usr/sbin/vipw", + "/usr/share/doc/passwd/NEWS.Debian.gz", + "/usr/share/doc/passwd/README.Debian", + "/usr/share/doc/passwd/TODO.Debian", + "/usr/share/doc/passwd/changelog.Debian.gz", + "/usr/share/doc/passwd/changelog.gz", + "/usr/share/doc/passwd/copyright", + "/usr/share/doc/passwd/examples/passwd.expire.cron", + "/usr/share/lintian/overrides/passwd", + "/usr/share/locale/bs/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ca/LC_MESSAGES/shadow.mo", + "/usr/share/locale/cs/LC_MESSAGES/shadow.mo", + "/usr/share/locale/da/LC_MESSAGES/shadow.mo", + "/usr/share/locale/de/LC_MESSAGES/shadow.mo", + "/usr/share/locale/dz/LC_MESSAGES/shadow.mo", + "/usr/share/locale/el/LC_MESSAGES/shadow.mo", + "/usr/share/locale/es/LC_MESSAGES/shadow.mo", + "/usr/share/locale/eu/LC_MESSAGES/shadow.mo", + "/usr/share/locale/fi/LC_MESSAGES/shadow.mo", + "/usr/share/locale/fr/LC_MESSAGES/shadow.mo", + "/usr/share/locale/gl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/he/LC_MESSAGES/shadow.mo", + "/usr/share/locale/hu/LC_MESSAGES/shadow.mo", + "/usr/share/locale/id/LC_MESSAGES/shadow.mo", + "/usr/share/locale/it/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ja/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ka/LC_MESSAGES/shadow.mo", + "/usr/share/locale/kk/LC_MESSAGES/shadow.mo", + "/usr/share/locale/km/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ko/LC_MESSAGES/shadow.mo", + "/usr/share/locale/nb/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ne/LC_MESSAGES/shadow.mo", + "/usr/share/locale/nl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/nn/LC_MESSAGES/shadow.mo", + "/usr/share/locale/pl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/pt/LC_MESSAGES/shadow.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ro/LC_MESSAGES/shadow.mo", + "/usr/share/locale/ru/LC_MESSAGES/shadow.mo", + "/usr/share/locale/sk/LC_MESSAGES/shadow.mo", + "/usr/share/locale/sq/LC_MESSAGES/shadow.mo", + "/usr/share/locale/sv/LC_MESSAGES/shadow.mo", + "/usr/share/locale/tl/LC_MESSAGES/shadow.mo", + "/usr/share/locale/tr/LC_MESSAGES/shadow.mo", + "/usr/share/locale/uk/LC_MESSAGES/shadow.mo", + "/usr/share/locale/vi/LC_MESSAGES/shadow.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/shadow.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/shadow.mo", + "/usr/share/man/cs/man1/expiry.1.gz", + "/usr/share/man/cs/man1/gpasswd.1.gz", + "/usr/share/man/cs/man5/gshadow.5.gz", + "/usr/share/man/cs/man5/passwd.5.gz", + "/usr/share/man/cs/man5/shadow.5.gz", + "/usr/share/man/cs/man8/groupadd.8.gz", + "/usr/share/man/cs/man8/groupdel.8.gz", + "/usr/share/man/cs/man8/groupmod.8.gz", + "/usr/share/man/cs/man8/grpck.8.gz", + "/usr/share/man/cs/man8/vipw.8.gz", + "/usr/share/man/da/man1/chfn.1.gz", + "/usr/share/man/da/man5/gshadow.5.gz", + "/usr/share/man/da/man8/groupdel.8.gz", + "/usr/share/man/da/man8/vipw.8.gz", + "/usr/share/man/de/man1/chage.1.gz", + "/usr/share/man/de/man1/chfn.1.gz", + "/usr/share/man/de/man1/chsh.1.gz", + "/usr/share/man/de/man1/expiry.1.gz", + "/usr/share/man/de/man1/gpasswd.1.gz", + "/usr/share/man/de/man1/passwd.1.gz", + "/usr/share/man/de/man5/gshadow.5.gz", + "/usr/share/man/de/man5/passwd.5.gz", + "/usr/share/man/de/man5/shadow.5.gz", + "/usr/share/man/de/man8/chgpasswd.8.gz", + "/usr/share/man/de/man8/chpasswd.8.gz", + "/usr/share/man/de/man8/groupadd.8.gz", + "/usr/share/man/de/man8/groupdel.8.gz", + "/usr/share/man/de/man8/groupmod.8.gz", + "/usr/share/man/de/man8/grpck.8.gz", + "/usr/share/man/de/man8/newusers.8.gz", + "/usr/share/man/de/man8/pwck.8.gz", + "/usr/share/man/de/man8/pwconv.8.gz", + "/usr/share/man/de/man8/useradd.8.gz", + "/usr/share/man/de/man8/userdel.8.gz", + "/usr/share/man/de/man8/usermod.8.gz", + "/usr/share/man/de/man8/vipw.8.gz", + "/usr/share/man/fi/man1/chfn.1.gz", + "/usr/share/man/fi/man1/chsh.1.gz", + "/usr/share/man/fr/man1/chage.1.gz", + "/usr/share/man/fr/man1/chfn.1.gz", + "/usr/share/man/fr/man1/chsh.1.gz", + "/usr/share/man/fr/man1/expiry.1.gz", + "/usr/share/man/fr/man1/gpasswd.1.gz", + "/usr/share/man/fr/man1/passwd.1.gz", + "/usr/share/man/fr/man5/gshadow.5.gz", + "/usr/share/man/fr/man5/passwd.5.gz", + "/usr/share/man/fr/man5/shadow.5.gz", + "/usr/share/man/fr/man5/subgid.5.gz", + "/usr/share/man/fr/man5/subuid.5.gz", + "/usr/share/man/fr/man8/chgpasswd.8.gz", + "/usr/share/man/fr/man8/chpasswd.8.gz", + "/usr/share/man/fr/man8/groupadd.8.gz", + "/usr/share/man/fr/man8/groupdel.8.gz", + "/usr/share/man/fr/man8/groupmod.8.gz", + "/usr/share/man/fr/man8/grpck.8.gz", + "/usr/share/man/fr/man8/newusers.8.gz", + "/usr/share/man/fr/man8/pwck.8.gz", + "/usr/share/man/fr/man8/pwconv.8.gz", + "/usr/share/man/fr/man8/useradd.8.gz", + "/usr/share/man/fr/man8/userdel.8.gz", + "/usr/share/man/fr/man8/usermod.8.gz", + "/usr/share/man/fr/man8/vipw.8.gz", + "/usr/share/man/hu/man1/chsh.1.gz", + "/usr/share/man/hu/man1/gpasswd.1.gz", + "/usr/share/man/hu/man1/passwd.1.gz", + "/usr/share/man/hu/man5/passwd.5.gz", + "/usr/share/man/id/man1/chsh.1.gz", + "/usr/share/man/id/man8/useradd.8.gz", + "/usr/share/man/it/man1/chage.1.gz", + "/usr/share/man/it/man1/chfn.1.gz", + "/usr/share/man/it/man1/chsh.1.gz", + "/usr/share/man/it/man1/expiry.1.gz", + "/usr/share/man/it/man1/gpasswd.1.gz", + "/usr/share/man/it/man1/passwd.1.gz", + "/usr/share/man/it/man5/gshadow.5.gz", + "/usr/share/man/it/man5/passwd.5.gz", + "/usr/share/man/it/man5/shadow.5.gz", + "/usr/share/man/it/man8/chgpasswd.8.gz", + "/usr/share/man/it/man8/chpasswd.8.gz", + "/usr/share/man/it/man8/groupadd.8.gz", + "/usr/share/man/it/man8/groupdel.8.gz", + "/usr/share/man/it/man8/groupmod.8.gz", + "/usr/share/man/it/man8/grpck.8.gz", + "/usr/share/man/it/man8/newusers.8.gz", + "/usr/share/man/it/man8/pwck.8.gz", + "/usr/share/man/it/man8/pwconv.8.gz", + "/usr/share/man/it/man8/useradd.8.gz", + "/usr/share/man/it/man8/userdel.8.gz", + "/usr/share/man/it/man8/usermod.8.gz", + "/usr/share/man/it/man8/vipw.8.gz", + "/usr/share/man/ja/man1/chage.1.gz", + "/usr/share/man/ja/man1/chfn.1.gz", + "/usr/share/man/ja/man1/chsh.1.gz", + "/usr/share/man/ja/man1/expiry.1.gz", + "/usr/share/man/ja/man1/gpasswd.1.gz", + "/usr/share/man/ja/man1/passwd.1.gz", + "/usr/share/man/ja/man5/passwd.5.gz", + "/usr/share/man/ja/man5/shadow.5.gz", + "/usr/share/man/ja/man8/chpasswd.8.gz", + "/usr/share/man/ja/man8/groupadd.8.gz", + "/usr/share/man/ja/man8/groupdel.8.gz", + "/usr/share/man/ja/man8/groupmod.8.gz", + "/usr/share/man/ja/man8/grpck.8.gz", + "/usr/share/man/ja/man8/newusers.8.gz", + "/usr/share/man/ja/man8/pwck.8.gz", + "/usr/share/man/ja/man8/pwconv.8.gz", + "/usr/share/man/ja/man8/useradd.8.gz", + "/usr/share/man/ja/man8/userdel.8.gz", + "/usr/share/man/ja/man8/usermod.8.gz", + "/usr/share/man/ja/man8/vipw.8.gz", + "/usr/share/man/ko/man1/chfn.1.gz", + "/usr/share/man/ko/man1/chsh.1.gz", + "/usr/share/man/ko/man5/passwd.5.gz", + "/usr/share/man/ko/man8/vipw.8.gz", + "/usr/share/man/man1/chage.1.gz", + "/usr/share/man/man1/chfn.1.gz", + "/usr/share/man/man1/chsh.1.gz", + "/usr/share/man/man1/expiry.1.gz", + "/usr/share/man/man1/gpasswd.1.gz", + "/usr/share/man/man1/passwd.1.gz", + "/usr/share/man/man5/gshadow.5.gz", + "/usr/share/man/man5/passwd.5.gz", + "/usr/share/man/man5/shadow.5.gz", + "/usr/share/man/man5/subgid.5.gz", + "/usr/share/man/man5/subuid.5.gz", + "/usr/share/man/man8/chgpasswd.8.gz", + "/usr/share/man/man8/chpasswd.8.gz", + "/usr/share/man/man8/groupadd.8.gz", + "/usr/share/man/man8/groupdel.8.gz", + "/usr/share/man/man8/groupmod.8.gz", + "/usr/share/man/man8/grpck.8.gz", + "/usr/share/man/man8/newusers.8.gz", + "/usr/share/man/man8/pwck.8.gz", + "/usr/share/man/man8/pwconv.8.gz", + "/usr/share/man/man8/shadowconfig.8.gz", + "/usr/share/man/man8/useradd.8.gz", + "/usr/share/man/man8/userdel.8.gz", + "/usr/share/man/man8/usermod.8.gz", + "/usr/share/man/man8/vipw.8.gz", + "/usr/share/man/pl/man1/chage.1.gz", + "/usr/share/man/pl/man1/chsh.1.gz", + "/usr/share/man/pl/man1/expiry.1.gz", + "/usr/share/man/pl/man8/groupadd.8.gz", + "/usr/share/man/pl/man8/groupdel.8.gz", + "/usr/share/man/pl/man8/groupmod.8.gz", + "/usr/share/man/pl/man8/grpck.8.gz", + "/usr/share/man/pl/man8/userdel.8.gz", + "/usr/share/man/pl/man8/usermod.8.gz", + "/usr/share/man/pl/man8/vipw.8.gz", + "/usr/share/man/pt_BR/man1/gpasswd.1.gz", + "/usr/share/man/pt_BR/man5/passwd.5.gz", + "/usr/share/man/pt_BR/man5/shadow.5.gz", + "/usr/share/man/pt_BR/man8/groupadd.8.gz", + "/usr/share/man/pt_BR/man8/groupdel.8.gz", + "/usr/share/man/pt_BR/man8/groupmod.8.gz", + "/usr/share/man/ru/man1/chage.1.gz", + "/usr/share/man/ru/man1/chfn.1.gz", + "/usr/share/man/ru/man1/chsh.1.gz", + "/usr/share/man/ru/man1/expiry.1.gz", + "/usr/share/man/ru/man1/gpasswd.1.gz", + "/usr/share/man/ru/man1/passwd.1.gz", + "/usr/share/man/ru/man5/gshadow.5.gz", + "/usr/share/man/ru/man5/passwd.5.gz", + "/usr/share/man/ru/man5/shadow.5.gz", + "/usr/share/man/ru/man8/chgpasswd.8.gz", + "/usr/share/man/ru/man8/chpasswd.8.gz", + "/usr/share/man/ru/man8/groupadd.8.gz", + "/usr/share/man/ru/man8/groupdel.8.gz", + "/usr/share/man/ru/man8/groupmod.8.gz", + "/usr/share/man/ru/man8/grpck.8.gz", + "/usr/share/man/ru/man8/newusers.8.gz", + "/usr/share/man/ru/man8/pwck.8.gz", + "/usr/share/man/ru/man8/pwconv.8.gz", + "/usr/share/man/ru/man8/useradd.8.gz", + "/usr/share/man/ru/man8/userdel.8.gz", + "/usr/share/man/ru/man8/usermod.8.gz", + "/usr/share/man/ru/man8/vipw.8.gz", + "/usr/share/man/sv/man1/chage.1.gz", + "/usr/share/man/sv/man1/chsh.1.gz", + "/usr/share/man/sv/man1/expiry.1.gz", + "/usr/share/man/sv/man1/passwd.1.gz", + "/usr/share/man/sv/man5/gshadow.5.gz", + "/usr/share/man/sv/man5/passwd.5.gz", + "/usr/share/man/sv/man8/groupadd.8.gz", + "/usr/share/man/sv/man8/groupdel.8.gz", + "/usr/share/man/sv/man8/groupmod.8.gz", + "/usr/share/man/sv/man8/grpck.8.gz", + "/usr/share/man/sv/man8/pwck.8.gz", + "/usr/share/man/sv/man8/userdel.8.gz", + "/usr/share/man/sv/man8/vipw.8.gz", + "/usr/share/man/tr/man1/chage.1.gz", + "/usr/share/man/tr/man1/chfn.1.gz", + "/usr/share/man/tr/man1/passwd.1.gz", + "/usr/share/man/tr/man5/passwd.5.gz", + "/usr/share/man/tr/man5/shadow.5.gz", + "/usr/share/man/tr/man8/groupadd.8.gz", + "/usr/share/man/tr/man8/groupdel.8.gz", + "/usr/share/man/tr/man8/groupmod.8.gz", + "/usr/share/man/tr/man8/useradd.8.gz", + "/usr/share/man/tr/man8/userdel.8.gz", + "/usr/share/man/tr/man8/usermod.8.gz", + "/usr/share/man/uk/man1/chage.1.gz", + "/usr/share/man/uk/man1/chfn.1.gz", + "/usr/share/man/uk/man1/chsh.1.gz", + "/usr/share/man/uk/man1/expiry.1.gz", + "/usr/share/man/uk/man1/gpasswd.1.gz", + "/usr/share/man/uk/man1/passwd.1.gz", + "/usr/share/man/uk/man5/gshadow.5.gz", + "/usr/share/man/uk/man5/passwd.5.gz", + "/usr/share/man/uk/man5/shadow.5.gz", + "/usr/share/man/uk/man8/chgpasswd.8.gz", + "/usr/share/man/uk/man8/chpasswd.8.gz", + "/usr/share/man/uk/man8/groupadd.8.gz", + "/usr/share/man/uk/man8/groupdel.8.gz", + "/usr/share/man/uk/man8/groupmod.8.gz", + "/usr/share/man/uk/man8/grpck.8.gz", + "/usr/share/man/uk/man8/newusers.8.gz", + "/usr/share/man/uk/man8/pwck.8.gz", + "/usr/share/man/uk/man8/pwconv.8.gz", + "/usr/share/man/uk/man8/useradd.8.gz", + "/usr/share/man/uk/man8/userdel.8.gz", + "/usr/share/man/uk/man8/usermod.8.gz", + "/usr/share/man/uk/man8/vipw.8.gz", + "/usr/share/man/zh_CN/man1/chage.1.gz", + "/usr/share/man/zh_CN/man1/chfn.1.gz", + "/usr/share/man/zh_CN/man1/chsh.1.gz", + "/usr/share/man/zh_CN/man1/expiry.1.gz", + "/usr/share/man/zh_CN/man1/gpasswd.1.gz", + "/usr/share/man/zh_CN/man1/passwd.1.gz", + "/usr/share/man/zh_CN/man5/gshadow.5.gz", + "/usr/share/man/zh_CN/man5/passwd.5.gz", + "/usr/share/man/zh_CN/man5/shadow.5.gz", + "/usr/share/man/zh_CN/man8/chgpasswd.8.gz", + "/usr/share/man/zh_CN/man8/chpasswd.8.gz", + "/usr/share/man/zh_CN/man8/groupadd.8.gz", + "/usr/share/man/zh_CN/man8/groupdel.8.gz", + "/usr/share/man/zh_CN/man8/groupmod.8.gz", + "/usr/share/man/zh_CN/man8/grpck.8.gz", + "/usr/share/man/zh_CN/man8/newusers.8.gz", + "/usr/share/man/zh_CN/man8/pwck.8.gz", + "/usr/share/man/zh_CN/man8/pwconv.8.gz", + "/usr/share/man/zh_CN/man8/useradd.8.gz", + "/usr/share/man/zh_CN/man8/userdel.8.gz", + "/usr/share/man/zh_CN/man8/usermod.8.gz", + "/usr/share/man/zh_CN/man8/vipw.8.gz", + "/usr/share/man/zh_TW/man1/chfn.1.gz", + "/usr/share/man/zh_TW/man1/chsh.1.gz", + "/usr/share/man/zh_TW/man5/passwd.5.gz", + "/usr/share/man/zh_TW/man8/chpasswd.8.gz", + "/usr/share/man/zh_TW/man8/groupadd.8.gz", + "/usr/share/man/zh_TW/man8/groupdel.8.gz", + "/usr/share/man/zh_TW/man8/groupmod.8.gz", + "/usr/share/man/zh_TW/man8/useradd.8.gz", + "/usr/share/man/zh_TW/man8/userdel.8.gz", + "/usr/share/man/zh_TW/man8/usermod.8.gz" + ] + }, + { + "ID": "pcre@8.32-17.el7", + "Name": "pcre", + "Identifier": { + "PURL": "pkg:rpm/centos/pcre@8.32-17.el7", + "UID": "bb3e738eb75d1a13", + "BOMRef": "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72" + }, + "Version": "8.32", + "Release": "17.el7", + "SrcName": "pcre", + "SrcVersion": "8.32", + "SrcRelease": "17.el7", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "perl-base@5.40.1-6", + "Name": "perl-base", + "Identifier": { + "PURL": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64\u0026distro=debian-13.6", + "UID": "17f06da2c02a11c6" + }, + "Version": "5.40.1", + "Release": "6", + "Arch": "amd64", + "SrcName": "perl", + "SrcVersion": "5.40.1", + "SrcRelease": "6", + "Licenses": [ + "GPL-1.0-or-later", + "Artistic-2.0", + "MIT", + "REGCOMP", + "GPL-2.0-with-bison-exception+", + "Unicode", + "BZIP", + "Zlib", + "GPL-2.0-or-later", + "FSFAP", + "BSD-3-clause-with-weird-numbering", + "CC0-1.0", + "TEXT-TABS", + "BSD-4-clause-POWERDOG", + "BSD-3-clause-GENERIC", + "BSD-3-Clause", + "SDBM-PUBLIC-DOMAIN", + "DONT-CHANGE-THE-GPL", + "Artistic-dist", + "LGPL-2.1-only", + "GPL-1.0-only", + "GPL-2.0-only", + "Artistic-2" + ], + "Maintainer": "Niko Tyni \u003cntyni@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/perl", + "/usr/bin/perl5.40.1", + "/usr/lib/x86_64-linux-gnu/perl-base/AutoLoader.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Carp.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Carp/Heavy.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Config.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Config_git.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/Config_heavy.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/Cwd.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/DynaLoader.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Errno.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Exporter.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Exporter/Heavy.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Fcntl.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Basename.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Glob.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Path.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Spec.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Spec/Unix.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/File/Temp.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/FileHandle.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Getopt/Long.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Getopt/Long/Parser.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Hash/Util.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/File.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Handle.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Pipe.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Seekable.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Select.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket/INET.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket/IP.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IO/Socket/UNIX.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IPC/Open2.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/IPC/Open3.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/List/Util.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/POSIX.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Scalar/Util.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/SelectSaver.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Socket.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Symbol.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Text/ParseWords.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Text/Tabs.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Text/Wrap.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/Tie/Hash.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/XSLoader.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/attributes.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Cwd/Cwd.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Fcntl/Fcntl.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/File/Glob/Glob.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Hash/Util/Util.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/IO/IO.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/List/Util/Util.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/POSIX/POSIX.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/Socket/Socket.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/attributes/attributes.so", + "/usr/lib/x86_64-linux-gnu/perl-base/auto/re/re.so", + "/usr/lib/x86_64-linux-gnu/perl-base/base.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/builtin.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/bytes.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/constant.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/feature.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/fields.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/integer.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/lib.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/locale.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/overload.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/overloading.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/parent.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/re.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/strict.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Age.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bmg.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bpb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Bpt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Cf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Ea.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/EqUIdeo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/GCB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Gc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Hst.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Identif2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Identifi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/InPC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/InSC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Isc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Jg.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Jt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Lb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Lc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFCQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFDQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFKCCF.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFKCQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NFKDQC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Na1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/NameAlia.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Nt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Nv.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/PerlDeci.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/SB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Sc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Scx.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Tc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Uc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/Vo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/WB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/_PerlLB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/To/_PerlSCX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/NA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V100.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V11.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V110.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V120.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V130.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V140.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V150.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V20.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V30.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V31.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V32.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V40.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V41.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V50.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V51.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V52.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V60.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V61.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V70.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V80.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Age/V90.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Alpha/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/AL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/AN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/B.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/BN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/CS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/EN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/ES.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/ET.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/L.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/NSM.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/ON.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/R.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bc/WS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/BidiC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/BidiM/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Blk/NB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bpt/C.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bpt/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Bpt/O.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CE/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CI/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWCF/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWCM/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWKCF/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWL/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWT/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CWU/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Cased/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/A.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/AL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/AR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/ATAR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/B.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/BR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/DB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/NK.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/NR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/OV.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ccc/VR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/CompEx/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/DI/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dash/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dep/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dia/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Com.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Enc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Fin.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Font.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Init.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Iso.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Med.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Nar.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Nb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/NonCanon.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Sqr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Sub.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Sup.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Dt/Vert.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/EBase/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/EComp/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/EPres/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/A.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/H.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/Na.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ea/W.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Emoji/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ext/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/ExtPict/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/CN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/LV.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/LVT.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/PP.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/SM.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GCB/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/C.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Cf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Cn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/L.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/LC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Ll.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Lm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Lo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Lu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/M.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Mc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Me.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Mn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Nd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Nl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/No.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/P.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pe.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Pi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Po.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Ps.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/S.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Sc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Sk.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Sm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/So.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Z.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Gc/Zs.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GrBase/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/GrExt/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Hex/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Hst/NA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Hyphen/T.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IDC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IDS/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdStatus/Allowed.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdStatus/Restrict.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/DefaultI.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Exclusio.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Inclusio.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/LimitedU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/NotChara.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/NotNFKC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/NotXID.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Obsolete.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Recommen.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Technica.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/IdType/Uncommon.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Ideo/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/10_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/11_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/12_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/12_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/13_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/14_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/15_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/2_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/2_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/3_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/3_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/3_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/4_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/4_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/5_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/5_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/5_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/6_3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/7_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/8_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/In/9_0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Bottom.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/BottomAn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Left.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/LeftAndR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/NA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Overstru.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Right.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/Top.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndBo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndL2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndLe.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/TopAndRi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InPC/VisualOr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Avagraha.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Bindu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Cantilla.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona5.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona6.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona7.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona8.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consona9.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Consonan.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Geminati.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Invisibl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Nukta.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Number.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Other.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/PureKill.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Syllable.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/ToneMark.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Virama.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Visarga.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/Vowel.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/VowelDep.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/InSC/VowelInd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Ain.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Alef.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Beh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Dal.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/FarsiYeh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Feh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Gaf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Hah.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/HanifiRo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Kaf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Lam.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/NoJoinin.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Noon.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Qaf.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Reh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Sad.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Seen.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Tah.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Waw.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jg/Yeh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/C.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/D.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/L.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/R.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/T.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Jt/U.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/AI.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/AL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/BA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/BB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/CJ.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/CL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/CM.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/GL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/ID.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/IN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/IS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/NS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/NU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/OP.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/PO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/PR.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/QU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/SA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lb/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Lower/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Math/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFCQC/M.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFCQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFDQC/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFDQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKCQC/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKCQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKDQC/N.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/NFKDQC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nt/Di.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nt/None.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nt/Nu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/0.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/10.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/100.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/10000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/100000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/11.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/12.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/13.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/14.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/15.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/16.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/17.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/18.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/19.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_16.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_6.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/1_8.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/20.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/200.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/2000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/20000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/2_3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/30.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/300.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/30000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3_16.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/3_4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/4.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/40.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/400.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/4000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/40000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/5.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/50.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/500.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/5000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/50000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/6.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/60.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/600.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/6000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/60000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/7.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/70.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/700.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/7000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/70000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/8.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/80.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/800.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/8000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/80000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/9.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/90.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/900.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/9000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Nv/90000.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/PCM/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/PatSyn/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Alnum.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Assigned.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Blank.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Graph.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/PerlWord.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/PosixPun.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Print.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/SpacePer.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Title.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/Word.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/XPosixPu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlAny.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlCh2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlCha.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlFol.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlIDC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlIDS.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlIsI.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlNch.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlPat.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlPr2.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlPro.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Perl/_PerlQuo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/QMark/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/AT.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/CL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/FO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/LE.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/LO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/NU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/SC.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/ST.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/Sp.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/UP.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SB/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/SD/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/STerm/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Arab.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Beng.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Cprt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Cyrl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Deva.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Dupl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Geor.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Glag.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gonm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gran.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Grek.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Gujr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Guru.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Han.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Hang.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Hira.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Kana.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Knda.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Latn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Limb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Linb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Mlym.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Mong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Mult.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Orya.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Sinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Syrc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Taml.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Telu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Zinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Sc/Zyyy.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Adlm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Arab.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Armn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Beng.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Bhks.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Bopo.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cakm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cham.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Copt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cprt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Cyrl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Deva.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Diak.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Dupl.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Ethi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Geor.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Glag.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gonm.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gran.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Grek.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Gujr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Guru.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Han.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hang.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hebr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hira.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hmng.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Hmnp.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Kana.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Khar.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Khmr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Khoj.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Knda.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Kthi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Lana.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Lao.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Latn.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Limb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Lina.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Linb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mlym.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mong.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mult.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Mymr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Nand.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Nko.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Orya.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Phlp.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Rohg.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Shrd.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Sind.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Sinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Syrc.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tagb.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Takr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Talu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Taml.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tang.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Telu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Thaa.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tibt.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Tirh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Vith.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Xsux.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Yezi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Yi.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Zinh.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Zyyy.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Scx/Zzzz.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Term/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/UIdeo/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Upper/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/VS/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/R.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/Tr.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/Tu.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/Vo/U.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/EX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/Extend.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/FO.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/HL.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/KA.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/LE.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/MB.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/ML.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/MN.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/NU.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/WSegSpac.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/WB/XX.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/XIDC/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/unicore/lib/XIDS/Y.pl", + "/usr/lib/x86_64-linux-gnu/perl-base/utf8.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/vars.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/warnings.pm", + "/usr/lib/x86_64-linux-gnu/perl-base/warnings/register.pm", + "/usr/share/doc/perl-base/changelog.Debian.gz", + "/usr/share/doc/perl-base/changelog.gz", + "/usr/share/doc/perl-base/copyright", + "/usr/share/doc/perl/AUTHORS.gz", + "/usr/share/doc/perl/Documentation", + "/usr/share/lintian/overrides/perl-base", + "/usr/share/man/man1/perl.1.gz" + ] + }, + { + "ID": "readline-common@8.2-6", + "Name": "readline-common", + "Identifier": { + "PURL": "pkg:deb/debian/readline-common@8.2-6?arch=all\u0026distro=debian-13.6", + "UID": "7f5f4bcfd1669a46" + }, + "Version": "8.2", + "Release": "6", + "Arch": "all", + "SrcName": "readline", + "SrcVersion": "8.2", + "SrcRelease": "6", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only", + "GFDL-1.3-no-invariants-or-later", + "GFDL-1.3-or-later", + "ISC-no-attribution" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "InstalledFiles": [ + "/usr/share/doc/readline-common/changelog.Debian.gz", + "/usr/share/doc/readline-common/changelog.gz", + "/usr/share/doc/readline-common/copyright", + "/usr/share/doc/readline-common/inputrc.arrows", + "/usr/share/info/rluserman.info.gz", + "/usr/share/lintian/overrides/readline-common", + "/usr/share/man/man3/history.3readline.gz", + "/usr/share/man/man3/readline.3readline.gz", + "/usr/share/readline/inputrc" + ] + }, + { + "ID": "sed@4.9-2+deb13u1", + "Name": "sed", + "Identifier": { + "PURL": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64\u0026distro=debian-13.6", + "UID": "d9e2231b96ca2bda" + }, + "Version": "4.9", + "Release": "2+deb13u1", + "Arch": "amd64", + "SrcName": "sed", + "SrcVersion": "4.9", + "SrcRelease": "2+deb13u1", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "X11", + "GFDL-1.3-no-invariants-or-later", + "GFDL-1.3-only", + "ISC", + "BSD-4-Clause-UC", + "BSL-1", + "pcre" + ], + "Maintainer": "Clint Adams \u003cclint@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/sed", + "/usr/share/doc/sed/AUTHORS", + "/usr/share/doc/sed/BUGS.gz", + "/usr/share/doc/sed/NEWS.gz", + "/usr/share/doc/sed/README", + "/usr/share/doc/sed/THANKS.gz", + "/usr/share/doc/sed/changelog.Debian.gz", + "/usr/share/doc/sed/changelog.gz", + "/usr/share/doc/sed/copyright", + "/usr/share/doc/sed/examples/dc.sed", + "/usr/share/doc/sed/sedfaq.txt.gz", + "/usr/share/info/sed.info.gz", + "/usr/share/locale/af/LC_MESSAGES/sed.mo", + "/usr/share/locale/ast/LC_MESSAGES/sed.mo", + "/usr/share/locale/bg/LC_MESSAGES/sed.mo", + "/usr/share/locale/ca/LC_MESSAGES/sed.mo", + "/usr/share/locale/cs/LC_MESSAGES/sed.mo", + "/usr/share/locale/da/LC_MESSAGES/sed.mo", + "/usr/share/locale/de/LC_MESSAGES/sed.mo", + "/usr/share/locale/el/LC_MESSAGES/sed.mo", + "/usr/share/locale/eo/LC_MESSAGES/sed.mo", + "/usr/share/locale/es/LC_MESSAGES/sed.mo", + "/usr/share/locale/et/LC_MESSAGES/sed.mo", + "/usr/share/locale/eu/LC_MESSAGES/sed.mo", + "/usr/share/locale/fi/LC_MESSAGES/sed.mo", + "/usr/share/locale/fr/LC_MESSAGES/sed.mo", + "/usr/share/locale/ga/LC_MESSAGES/sed.mo", + "/usr/share/locale/gl/LC_MESSAGES/sed.mo", + "/usr/share/locale/he/LC_MESSAGES/sed.mo", + "/usr/share/locale/hr/LC_MESSAGES/sed.mo", + "/usr/share/locale/hu/LC_MESSAGES/sed.mo", + "/usr/share/locale/id/LC_MESSAGES/sed.mo", + "/usr/share/locale/it/LC_MESSAGES/sed.mo", + "/usr/share/locale/ja/LC_MESSAGES/sed.mo", + "/usr/share/locale/ka/LC_MESSAGES/sed.mo", + "/usr/share/locale/ko/LC_MESSAGES/sed.mo", + "/usr/share/locale/nb/LC_MESSAGES/sed.mo", + "/usr/share/locale/nl/LC_MESSAGES/sed.mo", + "/usr/share/locale/pl/LC_MESSAGES/sed.mo", + "/usr/share/locale/pt/LC_MESSAGES/sed.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/sed.mo", + "/usr/share/locale/ro/LC_MESSAGES/sed.mo", + "/usr/share/locale/ru/LC_MESSAGES/sed.mo", + "/usr/share/locale/sk/LC_MESSAGES/sed.mo", + "/usr/share/locale/sl/LC_MESSAGES/sed.mo", + "/usr/share/locale/sr/LC_MESSAGES/sed.mo", + "/usr/share/locale/sv/LC_MESSAGES/sed.mo", + "/usr/share/locale/tr/LC_MESSAGES/sed.mo", + "/usr/share/locale/uk/LC_MESSAGES/sed.mo", + "/usr/share/locale/vi/LC_MESSAGES/sed.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/sed.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/sed.mo", + "/usr/share/man/man1/sed.1.gz" + ] + }, + { + "ID": "sqv@1.3.0-3+b2", + "Name": "sqv", + "Identifier": { + "PURL": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64\u0026distro=debian-13.6", + "UID": "82c65dd56fbcfd0e" + }, + "Version": "1.3.0", + "Release": "3+b2", + "Arch": "amd64", + "SrcName": "rust-sequoia-sqv", + "SrcVersion": "1.3.0", + "SrcRelease": "3", + "Licenses": [ + "LGPL-2.0-or-later", + "LGPL-2.0-only" + ], + "Maintainer": "Debian Rust Maintainers \u003cpkg-rust-maintainers@alioth-lists.debian.net\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3", + "libgcc-s1@14.2.0-19", + "libgmp10@2:6.3.0+dfsg-3", + "libhogweed6t64@3.10.1-1", + "libnettle8t64@3.10.1-1" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/sqv", + "/usr/share/bash-completion/completions/sqv.bash", + "/usr/share/doc/sqv/NEWS.gz", + "/usr/share/doc/sqv/changelog.Debian.amd64.gz", + "/usr/share/doc/sqv/changelog.Debian.gz", + "/usr/share/doc/sqv/copyright", + "/usr/share/fish/completions/sqv.fish", + "/usr/share/man/man1/sqv.1.gz", + "/usr/share/zsh/vendor-completions/_sqv" + ] + }, + { + "ID": "sysvinit-utils@3.14-4", + "Name": "sysvinit-utils", + "Identifier": { + "PURL": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64\u0026distro=debian-13.6", + "UID": "8699daa05d734d69" + }, + "Version": "3.14", + "Release": "4", + "Arch": "amd64", + "SrcName": "sysvinit", + "SrcVersion": "3.14", + "SrcRelease": "4", + "Licenses": [ + "GPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-only", + "GPL-2.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian sysvinit maintainers \u003cdebian-init-diversity@chiark.greenend.org.uk\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/init/init-d-script", + "/usr/lib/init/vars.sh", + "/usr/lib/lsb/init-functions", + "/usr/lib/lsb/init-functions.d/00-verbose", + "/usr/sbin/fstab-decode", + "/usr/sbin/killall5", + "/usr/share/doc/sysvinit-utils/changelog.Debian.gz", + "/usr/share/doc/sysvinit-utils/copyright", + "/usr/share/man/man5/init-d-script.5.gz", + "/usr/share/man/man8/fstab-decode.8.gz", + "/usr/share/man/man8/killall5.8.gz", + "/usr/share/man/man8/pidof.8.gz" + ] + }, + { + "ID": "tar@1.35+dfsg-3.1", + "Name": "tar", + "Identifier": { + "PURL": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64\u0026distro=debian-13.6", + "UID": "50aee76d081ea925" + }, + "Version": "1.35+dfsg", + "Release": "3.1", + "Arch": "amd64", + "SrcName": "tar", + "SrcVersion": "1.35+dfsg", + "SrcRelease": "3.1", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only", + "GPL-3+ with Bison exception", + "LGPL-2.1-or-later", + "LGPL-2.1-only", + "LGPL-3.0-or-later", + "LGPL-3.0-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Janos Lenart \u003cocsi@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/tar", + "/usr/lib/mime/packages/tar", + "/usr/sbin/rmt-tar", + "/usr/sbin/tarcat", + "/usr/share/doc/tar/AUTHORS", + "/usr/share/doc/tar/NEWS.gz", + "/usr/share/doc/tar/README.Debian", + "/usr/share/doc/tar/THANKS.gz", + "/usr/share/doc/tar/changelog.1.gz", + "/usr/share/doc/tar/changelog.Debian.gz", + "/usr/share/doc/tar/changelog.gz", + "/usr/share/doc/tar/copyright", + "/usr/share/locale/bg/LC_MESSAGES/tar.mo", + "/usr/share/locale/ca/LC_MESSAGES/tar.mo", + "/usr/share/locale/cs/LC_MESSAGES/tar.mo", + "/usr/share/locale/da/LC_MESSAGES/tar.mo", + "/usr/share/locale/de/LC_MESSAGES/tar.mo", + "/usr/share/locale/el/LC_MESSAGES/tar.mo", + "/usr/share/locale/eo/LC_MESSAGES/tar.mo", + "/usr/share/locale/es/LC_MESSAGES/tar.mo", + "/usr/share/locale/et/LC_MESSAGES/tar.mo", + "/usr/share/locale/eu/LC_MESSAGES/tar.mo", + "/usr/share/locale/fi/LC_MESSAGES/tar.mo", + "/usr/share/locale/fr/LC_MESSAGES/tar.mo", + "/usr/share/locale/ga/LC_MESSAGES/tar.mo", + "/usr/share/locale/gl/LC_MESSAGES/tar.mo", + "/usr/share/locale/hr/LC_MESSAGES/tar.mo", + "/usr/share/locale/hu/LC_MESSAGES/tar.mo", + "/usr/share/locale/id/LC_MESSAGES/tar.mo", + "/usr/share/locale/it/LC_MESSAGES/tar.mo", + "/usr/share/locale/ja/LC_MESSAGES/tar.mo", + "/usr/share/locale/ka/LC_MESSAGES/tar.mo", + "/usr/share/locale/ko/LC_MESSAGES/tar.mo", + "/usr/share/locale/ky/LC_MESSAGES/tar.mo", + "/usr/share/locale/ms/LC_MESSAGES/tar.mo", + "/usr/share/locale/nb/LC_MESSAGES/tar.mo", + "/usr/share/locale/nl/LC_MESSAGES/tar.mo", + "/usr/share/locale/pl/LC_MESSAGES/tar.mo", + "/usr/share/locale/pt/LC_MESSAGES/tar.mo", + "/usr/share/locale/pt_BR/LC_MESSAGES/tar.mo", + "/usr/share/locale/ro/LC_MESSAGES/tar.mo", + "/usr/share/locale/ru/LC_MESSAGES/tar.mo", + "/usr/share/locale/sk/LC_MESSAGES/tar.mo", + "/usr/share/locale/sl/LC_MESSAGES/tar.mo", + "/usr/share/locale/sr/LC_MESSAGES/tar.mo", + "/usr/share/locale/sv/LC_MESSAGES/tar.mo", + "/usr/share/locale/tr/LC_MESSAGES/tar.mo", + "/usr/share/locale/uk/LC_MESSAGES/tar.mo", + "/usr/share/locale/vi/LC_MESSAGES/tar.mo", + "/usr/share/locale/zh_CN/LC_MESSAGES/tar.mo", + "/usr/share/locale/zh_TW/LC_MESSAGES/tar.mo", + "/usr/share/man/man1/tar.1.gz", + "/usr/share/man/man1/tarcat.1.gz", + "/usr/share/man/man8/rmt-tar.8.gz" + ] + }, + { + "ID": "tzdata@2026b-0+deb13u1", + "Name": "tzdata", + "Identifier": { + "PURL": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all\u0026distro=debian-13.6", + "UID": "6698e2883de2f799" + }, + "Version": "2026b", + "Release": "0+deb13u1", + "Arch": "all", + "SrcName": "tzdata", + "SrcVersion": "2026b", + "SrcRelease": "0+deb13u1", + "Licenses": [ + "public-domain" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "DependsOn": [ + "debconf@1.5.91" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/share/doc/tzdata/NEWS.Debian.gz", + "/usr/share/doc/tzdata/README.Debian", + "/usr/share/doc/tzdata/changelog.Debian.gz", + "/usr/share/doc/tzdata/changelog.gz", + "/usr/share/doc/tzdata/copyright", + "/usr/share/lintian/overrides/tzdata", + "/usr/share/zoneinfo/Africa/Abidjan", + "/usr/share/zoneinfo/Africa/Accra", + "/usr/share/zoneinfo/Africa/Addis_Ababa", + "/usr/share/zoneinfo/Africa/Algiers", + "/usr/share/zoneinfo/Africa/Asmara", + "/usr/share/zoneinfo/Africa/Bamako", + "/usr/share/zoneinfo/Africa/Bangui", + "/usr/share/zoneinfo/Africa/Banjul", + "/usr/share/zoneinfo/Africa/Bissau", + "/usr/share/zoneinfo/Africa/Blantyre", + "/usr/share/zoneinfo/Africa/Brazzaville", + "/usr/share/zoneinfo/Africa/Bujumbura", + "/usr/share/zoneinfo/Africa/Cairo", + "/usr/share/zoneinfo/Africa/Casablanca", + "/usr/share/zoneinfo/Africa/Ceuta", + "/usr/share/zoneinfo/Africa/Conakry", + "/usr/share/zoneinfo/Africa/Dakar", + "/usr/share/zoneinfo/Africa/Dar_es_Salaam", + "/usr/share/zoneinfo/Africa/Djibouti", + "/usr/share/zoneinfo/Africa/Douala", + "/usr/share/zoneinfo/Africa/El_Aaiun", + "/usr/share/zoneinfo/Africa/Freetown", + "/usr/share/zoneinfo/Africa/Gaborone", + "/usr/share/zoneinfo/Africa/Harare", + "/usr/share/zoneinfo/Africa/Johannesburg", + "/usr/share/zoneinfo/Africa/Juba", + "/usr/share/zoneinfo/Africa/Kampala", + "/usr/share/zoneinfo/Africa/Khartoum", + "/usr/share/zoneinfo/Africa/Kigali", + "/usr/share/zoneinfo/Africa/Kinshasa", + "/usr/share/zoneinfo/Africa/Lagos", + "/usr/share/zoneinfo/Africa/Libreville", + "/usr/share/zoneinfo/Africa/Lome", + "/usr/share/zoneinfo/Africa/Luanda", + "/usr/share/zoneinfo/Africa/Lubumbashi", + "/usr/share/zoneinfo/Africa/Lusaka", + "/usr/share/zoneinfo/Africa/Malabo", + "/usr/share/zoneinfo/Africa/Maputo", + "/usr/share/zoneinfo/Africa/Maseru", + "/usr/share/zoneinfo/Africa/Mbabane", + "/usr/share/zoneinfo/Africa/Mogadishu", + "/usr/share/zoneinfo/Africa/Monrovia", + "/usr/share/zoneinfo/Africa/Nairobi", + "/usr/share/zoneinfo/Africa/Ndjamena", + "/usr/share/zoneinfo/Africa/Niamey", + "/usr/share/zoneinfo/Africa/Nouakchott", + "/usr/share/zoneinfo/Africa/Ouagadougou", + "/usr/share/zoneinfo/Africa/Porto-Novo", + "/usr/share/zoneinfo/Africa/Sao_Tome", + "/usr/share/zoneinfo/Africa/Tripoli", + "/usr/share/zoneinfo/Africa/Tunis", + "/usr/share/zoneinfo/Africa/Windhoek", + "/usr/share/zoneinfo/America/Adak", + "/usr/share/zoneinfo/America/Anchorage", + "/usr/share/zoneinfo/America/Anguilla", + "/usr/share/zoneinfo/America/Antigua", + "/usr/share/zoneinfo/America/Araguaina", + "/usr/share/zoneinfo/America/Argentina/Buenos_Aires", + "/usr/share/zoneinfo/America/Argentina/Catamarca", + "/usr/share/zoneinfo/America/Argentina/Cordoba", + "/usr/share/zoneinfo/America/Argentina/Jujuy", + "/usr/share/zoneinfo/America/Argentina/La_Rioja", + "/usr/share/zoneinfo/America/Argentina/Mendoza", + "/usr/share/zoneinfo/America/Argentina/Rio_Gallegos", + "/usr/share/zoneinfo/America/Argentina/Salta", + "/usr/share/zoneinfo/America/Argentina/San_Juan", + "/usr/share/zoneinfo/America/Argentina/San_Luis", + "/usr/share/zoneinfo/America/Argentina/Tucuman", + "/usr/share/zoneinfo/America/Argentina/Ushuaia", + "/usr/share/zoneinfo/America/Aruba", + "/usr/share/zoneinfo/America/Asuncion", + "/usr/share/zoneinfo/America/Atikokan", + "/usr/share/zoneinfo/America/Bahia", + "/usr/share/zoneinfo/America/Bahia_Banderas", + "/usr/share/zoneinfo/America/Barbados", + "/usr/share/zoneinfo/America/Belem", + "/usr/share/zoneinfo/America/Belize", + "/usr/share/zoneinfo/America/Blanc-Sablon", + "/usr/share/zoneinfo/America/Boa_Vista", + "/usr/share/zoneinfo/America/Bogota", + "/usr/share/zoneinfo/America/Boise", + "/usr/share/zoneinfo/America/Cambridge_Bay", + "/usr/share/zoneinfo/America/Campo_Grande", + "/usr/share/zoneinfo/America/Cancun", + "/usr/share/zoneinfo/America/Caracas", + "/usr/share/zoneinfo/America/Cayenne", + "/usr/share/zoneinfo/America/Cayman", + "/usr/share/zoneinfo/America/Chicago", + "/usr/share/zoneinfo/America/Chihuahua", + "/usr/share/zoneinfo/America/Ciudad_Juarez", + "/usr/share/zoneinfo/America/Costa_Rica", + "/usr/share/zoneinfo/America/Coyhaique", + "/usr/share/zoneinfo/America/Creston", + "/usr/share/zoneinfo/America/Cuiaba", + "/usr/share/zoneinfo/America/Curacao", + "/usr/share/zoneinfo/America/Danmarkshavn", + "/usr/share/zoneinfo/America/Dawson", + "/usr/share/zoneinfo/America/Dawson_Creek", + "/usr/share/zoneinfo/America/Denver", + "/usr/share/zoneinfo/America/Detroit", + "/usr/share/zoneinfo/America/Dominica", + "/usr/share/zoneinfo/America/Edmonton", + "/usr/share/zoneinfo/America/Eirunepe", + "/usr/share/zoneinfo/America/El_Salvador", + "/usr/share/zoneinfo/America/Fort_Nelson", + "/usr/share/zoneinfo/America/Fortaleza", + "/usr/share/zoneinfo/America/Glace_Bay", + "/usr/share/zoneinfo/America/Goose_Bay", + "/usr/share/zoneinfo/America/Grand_Turk", + "/usr/share/zoneinfo/America/Grenada", + "/usr/share/zoneinfo/America/Guadeloupe", + "/usr/share/zoneinfo/America/Guatemala", + "/usr/share/zoneinfo/America/Guayaquil", + "/usr/share/zoneinfo/America/Guyana", + "/usr/share/zoneinfo/America/Halifax", + "/usr/share/zoneinfo/America/Havana", + "/usr/share/zoneinfo/America/Hermosillo", + "/usr/share/zoneinfo/America/Indiana/Indianapolis", + "/usr/share/zoneinfo/America/Indiana/Knox", + "/usr/share/zoneinfo/America/Indiana/Marengo", + "/usr/share/zoneinfo/America/Indiana/Petersburg", + "/usr/share/zoneinfo/America/Indiana/Tell_City", + "/usr/share/zoneinfo/America/Indiana/Vevay", + "/usr/share/zoneinfo/America/Indiana/Vincennes", + "/usr/share/zoneinfo/America/Indiana/Winamac", + "/usr/share/zoneinfo/America/Inuvik", + "/usr/share/zoneinfo/America/Iqaluit", + "/usr/share/zoneinfo/America/Jamaica", + "/usr/share/zoneinfo/America/Juneau", + "/usr/share/zoneinfo/America/Kentucky/Louisville", + "/usr/share/zoneinfo/America/Kentucky/Monticello", + "/usr/share/zoneinfo/America/La_Paz", + "/usr/share/zoneinfo/America/Lima", + "/usr/share/zoneinfo/America/Los_Angeles", + "/usr/share/zoneinfo/America/Maceio", + "/usr/share/zoneinfo/America/Managua", + "/usr/share/zoneinfo/America/Manaus", + "/usr/share/zoneinfo/America/Martinique", + "/usr/share/zoneinfo/America/Matamoros", + "/usr/share/zoneinfo/America/Mazatlan", + "/usr/share/zoneinfo/America/Menominee", + "/usr/share/zoneinfo/America/Merida", + "/usr/share/zoneinfo/America/Metlakatla", + "/usr/share/zoneinfo/America/Mexico_City", + "/usr/share/zoneinfo/America/Miquelon", + "/usr/share/zoneinfo/America/Moncton", + "/usr/share/zoneinfo/America/Monterrey", + "/usr/share/zoneinfo/America/Montevideo", + "/usr/share/zoneinfo/America/Montserrat", + "/usr/share/zoneinfo/America/Nassau", + "/usr/share/zoneinfo/America/New_York", + "/usr/share/zoneinfo/America/Nome", + "/usr/share/zoneinfo/America/Noronha", + "/usr/share/zoneinfo/America/North_Dakota/Beulah", + "/usr/share/zoneinfo/America/North_Dakota/Center", + "/usr/share/zoneinfo/America/North_Dakota/New_Salem", + "/usr/share/zoneinfo/America/Nuuk", + "/usr/share/zoneinfo/America/Ojinaga", + "/usr/share/zoneinfo/America/Panama", + "/usr/share/zoneinfo/America/Paramaribo", + "/usr/share/zoneinfo/America/Phoenix", + "/usr/share/zoneinfo/America/Port-au-Prince", + "/usr/share/zoneinfo/America/Port_of_Spain", + "/usr/share/zoneinfo/America/Porto_Velho", + "/usr/share/zoneinfo/America/Puerto_Rico", + "/usr/share/zoneinfo/America/Punta_Arenas", + "/usr/share/zoneinfo/America/Rankin_Inlet", + "/usr/share/zoneinfo/America/Recife", + "/usr/share/zoneinfo/America/Regina", + "/usr/share/zoneinfo/America/Resolute", + "/usr/share/zoneinfo/America/Rio_Branco", + "/usr/share/zoneinfo/America/Santarem", + "/usr/share/zoneinfo/America/Santiago", + "/usr/share/zoneinfo/America/Santo_Domingo", + "/usr/share/zoneinfo/America/Sao_Paulo", + "/usr/share/zoneinfo/America/Scoresbysund", + "/usr/share/zoneinfo/America/Sitka", + "/usr/share/zoneinfo/America/St_Johns", + "/usr/share/zoneinfo/America/St_Kitts", + "/usr/share/zoneinfo/America/St_Lucia", + "/usr/share/zoneinfo/America/St_Thomas", + "/usr/share/zoneinfo/America/St_Vincent", + "/usr/share/zoneinfo/America/Swift_Current", + "/usr/share/zoneinfo/America/Tegucigalpa", + "/usr/share/zoneinfo/America/Thule", + "/usr/share/zoneinfo/America/Tijuana", + "/usr/share/zoneinfo/America/Toronto", + "/usr/share/zoneinfo/America/Tortola", + "/usr/share/zoneinfo/America/Vancouver", + "/usr/share/zoneinfo/America/Whitehorse", + "/usr/share/zoneinfo/America/Winnipeg", + "/usr/share/zoneinfo/America/Yakutat", + "/usr/share/zoneinfo/Antarctica/Casey", + "/usr/share/zoneinfo/Antarctica/Davis", + "/usr/share/zoneinfo/Antarctica/DumontDUrville", + "/usr/share/zoneinfo/Antarctica/Macquarie", + "/usr/share/zoneinfo/Antarctica/Mawson", + "/usr/share/zoneinfo/Antarctica/McMurdo", + "/usr/share/zoneinfo/Antarctica/Palmer", + "/usr/share/zoneinfo/Antarctica/Rothera", + "/usr/share/zoneinfo/Antarctica/Syowa", + "/usr/share/zoneinfo/Antarctica/Troll", + "/usr/share/zoneinfo/Antarctica/Vostok", + "/usr/share/zoneinfo/Asia/Aden", + "/usr/share/zoneinfo/Asia/Almaty", + "/usr/share/zoneinfo/Asia/Amman", + "/usr/share/zoneinfo/Asia/Anadyr", + "/usr/share/zoneinfo/Asia/Aqtau", + "/usr/share/zoneinfo/Asia/Aqtobe", + "/usr/share/zoneinfo/Asia/Ashgabat", + "/usr/share/zoneinfo/Asia/Atyrau", + "/usr/share/zoneinfo/Asia/Baghdad", + "/usr/share/zoneinfo/Asia/Bahrain", + "/usr/share/zoneinfo/Asia/Baku", + "/usr/share/zoneinfo/Asia/Bangkok", + "/usr/share/zoneinfo/Asia/Barnaul", + "/usr/share/zoneinfo/Asia/Beirut", + "/usr/share/zoneinfo/Asia/Bishkek", + "/usr/share/zoneinfo/Asia/Brunei", + "/usr/share/zoneinfo/Asia/Chita", + "/usr/share/zoneinfo/Asia/Colombo", + "/usr/share/zoneinfo/Asia/Damascus", + "/usr/share/zoneinfo/Asia/Dhaka", + "/usr/share/zoneinfo/Asia/Dili", + "/usr/share/zoneinfo/Asia/Dubai", + "/usr/share/zoneinfo/Asia/Dushanbe", + "/usr/share/zoneinfo/Asia/Famagusta", + "/usr/share/zoneinfo/Asia/Gaza", + "/usr/share/zoneinfo/Asia/Hebron", + "/usr/share/zoneinfo/Asia/Ho_Chi_Minh", + "/usr/share/zoneinfo/Asia/Hong_Kong", + "/usr/share/zoneinfo/Asia/Hovd", + "/usr/share/zoneinfo/Asia/Irkutsk", + "/usr/share/zoneinfo/Asia/Jakarta", + "/usr/share/zoneinfo/Asia/Jayapura", + "/usr/share/zoneinfo/Asia/Jerusalem", + "/usr/share/zoneinfo/Asia/Kabul", + "/usr/share/zoneinfo/Asia/Kamchatka", + "/usr/share/zoneinfo/Asia/Karachi", + "/usr/share/zoneinfo/Asia/Kathmandu", + "/usr/share/zoneinfo/Asia/Khandyga", + "/usr/share/zoneinfo/Asia/Kolkata", + "/usr/share/zoneinfo/Asia/Krasnoyarsk", + "/usr/share/zoneinfo/Asia/Kuala_Lumpur", + "/usr/share/zoneinfo/Asia/Kuching", + "/usr/share/zoneinfo/Asia/Kuwait", + "/usr/share/zoneinfo/Asia/Macau", + "/usr/share/zoneinfo/Asia/Magadan", + "/usr/share/zoneinfo/Asia/Makassar", + "/usr/share/zoneinfo/Asia/Manila", + "/usr/share/zoneinfo/Asia/Muscat", + "/usr/share/zoneinfo/Asia/Nicosia", + "/usr/share/zoneinfo/Asia/Novokuznetsk", + "/usr/share/zoneinfo/Asia/Novosibirsk", + "/usr/share/zoneinfo/Asia/Omsk", + "/usr/share/zoneinfo/Asia/Oral", + "/usr/share/zoneinfo/Asia/Phnom_Penh", + "/usr/share/zoneinfo/Asia/Pontianak", + "/usr/share/zoneinfo/Asia/Pyongyang", + "/usr/share/zoneinfo/Asia/Qatar", + "/usr/share/zoneinfo/Asia/Qostanay", + "/usr/share/zoneinfo/Asia/Qyzylorda", + "/usr/share/zoneinfo/Asia/Riyadh", + "/usr/share/zoneinfo/Asia/Sakhalin", + "/usr/share/zoneinfo/Asia/Samarkand", + "/usr/share/zoneinfo/Asia/Seoul", + "/usr/share/zoneinfo/Asia/Shanghai", + "/usr/share/zoneinfo/Asia/Singapore", + "/usr/share/zoneinfo/Asia/Srednekolymsk", + "/usr/share/zoneinfo/Asia/Taipei", + "/usr/share/zoneinfo/Asia/Tashkent", + "/usr/share/zoneinfo/Asia/Tbilisi", + "/usr/share/zoneinfo/Asia/Tehran", + "/usr/share/zoneinfo/Asia/Thimphu", + "/usr/share/zoneinfo/Asia/Tokyo", + "/usr/share/zoneinfo/Asia/Tomsk", + "/usr/share/zoneinfo/Asia/Ulaanbaatar", + "/usr/share/zoneinfo/Asia/Urumqi", + "/usr/share/zoneinfo/Asia/Ust-Nera", + "/usr/share/zoneinfo/Asia/Vientiane", + "/usr/share/zoneinfo/Asia/Vladivostok", + "/usr/share/zoneinfo/Asia/Yakutsk", + "/usr/share/zoneinfo/Asia/Yangon", + "/usr/share/zoneinfo/Asia/Yekaterinburg", + "/usr/share/zoneinfo/Asia/Yerevan", + "/usr/share/zoneinfo/Atlantic/Azores", + "/usr/share/zoneinfo/Atlantic/Bermuda", + "/usr/share/zoneinfo/Atlantic/Canary", + "/usr/share/zoneinfo/Atlantic/Cape_Verde", + "/usr/share/zoneinfo/Atlantic/Faroe", + "/usr/share/zoneinfo/Atlantic/Madeira", + "/usr/share/zoneinfo/Atlantic/Reykjavik", + "/usr/share/zoneinfo/Atlantic/South_Georgia", + "/usr/share/zoneinfo/Atlantic/St_Helena", + "/usr/share/zoneinfo/Atlantic/Stanley", + "/usr/share/zoneinfo/Australia/Adelaide", + "/usr/share/zoneinfo/Australia/Brisbane", + "/usr/share/zoneinfo/Australia/Broken_Hill", + "/usr/share/zoneinfo/Australia/Darwin", + "/usr/share/zoneinfo/Australia/Eucla", + "/usr/share/zoneinfo/Australia/Hobart", + "/usr/share/zoneinfo/Australia/Lindeman", + "/usr/share/zoneinfo/Australia/Lord_Howe", + "/usr/share/zoneinfo/Australia/Melbourne", + "/usr/share/zoneinfo/Australia/Perth", + "/usr/share/zoneinfo/Australia/Sydney", + "/usr/share/zoneinfo/Etc/GMT", + "/usr/share/zoneinfo/Etc/GMT+1", + "/usr/share/zoneinfo/Etc/GMT+10", + "/usr/share/zoneinfo/Etc/GMT+11", + "/usr/share/zoneinfo/Etc/GMT+12", + "/usr/share/zoneinfo/Etc/GMT+2", + "/usr/share/zoneinfo/Etc/GMT+3", + "/usr/share/zoneinfo/Etc/GMT+4", + "/usr/share/zoneinfo/Etc/GMT+5", + "/usr/share/zoneinfo/Etc/GMT+6", + "/usr/share/zoneinfo/Etc/GMT+7", + "/usr/share/zoneinfo/Etc/GMT+8", + "/usr/share/zoneinfo/Etc/GMT+9", + "/usr/share/zoneinfo/Etc/GMT-1", + "/usr/share/zoneinfo/Etc/GMT-10", + "/usr/share/zoneinfo/Etc/GMT-11", + "/usr/share/zoneinfo/Etc/GMT-12", + "/usr/share/zoneinfo/Etc/GMT-13", + "/usr/share/zoneinfo/Etc/GMT-14", + "/usr/share/zoneinfo/Etc/GMT-2", + "/usr/share/zoneinfo/Etc/GMT-3", + "/usr/share/zoneinfo/Etc/GMT-4", + "/usr/share/zoneinfo/Etc/GMT-5", + "/usr/share/zoneinfo/Etc/GMT-6", + "/usr/share/zoneinfo/Etc/GMT-7", + "/usr/share/zoneinfo/Etc/GMT-8", + "/usr/share/zoneinfo/Etc/GMT-9", + "/usr/share/zoneinfo/Etc/UTC", + "/usr/share/zoneinfo/Europe/Amsterdam", + "/usr/share/zoneinfo/Europe/Andorra", + "/usr/share/zoneinfo/Europe/Astrakhan", + "/usr/share/zoneinfo/Europe/Athens", + "/usr/share/zoneinfo/Europe/Belgrade", + "/usr/share/zoneinfo/Europe/Berlin", + "/usr/share/zoneinfo/Europe/Brussels", + "/usr/share/zoneinfo/Europe/Bucharest", + "/usr/share/zoneinfo/Europe/Budapest", + "/usr/share/zoneinfo/Europe/Chisinau", + "/usr/share/zoneinfo/Europe/Copenhagen", + "/usr/share/zoneinfo/Europe/Dublin", + "/usr/share/zoneinfo/Europe/Gibraltar", + "/usr/share/zoneinfo/Europe/Guernsey", + "/usr/share/zoneinfo/Europe/Helsinki", + "/usr/share/zoneinfo/Europe/Isle_of_Man", + "/usr/share/zoneinfo/Europe/Istanbul", + "/usr/share/zoneinfo/Europe/Jersey", + "/usr/share/zoneinfo/Europe/Kaliningrad", + "/usr/share/zoneinfo/Europe/Kirov", + "/usr/share/zoneinfo/Europe/Kyiv", + "/usr/share/zoneinfo/Europe/Lisbon", + "/usr/share/zoneinfo/Europe/Ljubljana", + "/usr/share/zoneinfo/Europe/London", + "/usr/share/zoneinfo/Europe/Luxembourg", + "/usr/share/zoneinfo/Europe/Madrid", + "/usr/share/zoneinfo/Europe/Malta", + "/usr/share/zoneinfo/Europe/Minsk", + "/usr/share/zoneinfo/Europe/Monaco", + "/usr/share/zoneinfo/Europe/Moscow", + "/usr/share/zoneinfo/Europe/Oslo", + "/usr/share/zoneinfo/Europe/Paris", + "/usr/share/zoneinfo/Europe/Prague", + "/usr/share/zoneinfo/Europe/Riga", + "/usr/share/zoneinfo/Europe/Rome", + "/usr/share/zoneinfo/Europe/Samara", + "/usr/share/zoneinfo/Europe/Sarajevo", + "/usr/share/zoneinfo/Europe/Saratov", + "/usr/share/zoneinfo/Europe/Simferopol", + "/usr/share/zoneinfo/Europe/Skopje", + "/usr/share/zoneinfo/Europe/Sofia", + "/usr/share/zoneinfo/Europe/Stockholm", + "/usr/share/zoneinfo/Europe/Tallinn", + "/usr/share/zoneinfo/Europe/Tirane", + "/usr/share/zoneinfo/Europe/Ulyanovsk", + "/usr/share/zoneinfo/Europe/Vaduz", + "/usr/share/zoneinfo/Europe/Vienna", + "/usr/share/zoneinfo/Europe/Vilnius", + "/usr/share/zoneinfo/Europe/Volgograd", + "/usr/share/zoneinfo/Europe/Warsaw", + "/usr/share/zoneinfo/Europe/Zagreb", + "/usr/share/zoneinfo/Europe/Zurich", + "/usr/share/zoneinfo/Factory", + "/usr/share/zoneinfo/Indian/Antananarivo", + "/usr/share/zoneinfo/Indian/Chagos", + "/usr/share/zoneinfo/Indian/Christmas", + "/usr/share/zoneinfo/Indian/Cocos", + "/usr/share/zoneinfo/Indian/Comoro", + "/usr/share/zoneinfo/Indian/Kerguelen", + "/usr/share/zoneinfo/Indian/Mahe", + "/usr/share/zoneinfo/Indian/Maldives", + "/usr/share/zoneinfo/Indian/Mauritius", + "/usr/share/zoneinfo/Indian/Mayotte", + "/usr/share/zoneinfo/Indian/Reunion", + "/usr/share/zoneinfo/Pacific/Apia", + "/usr/share/zoneinfo/Pacific/Auckland", + "/usr/share/zoneinfo/Pacific/Bougainville", + "/usr/share/zoneinfo/Pacific/Chatham", + "/usr/share/zoneinfo/Pacific/Chuuk", + "/usr/share/zoneinfo/Pacific/Easter", + "/usr/share/zoneinfo/Pacific/Efate", + "/usr/share/zoneinfo/Pacific/Fakaofo", + "/usr/share/zoneinfo/Pacific/Fiji", + "/usr/share/zoneinfo/Pacific/Funafuti", + "/usr/share/zoneinfo/Pacific/Galapagos", + "/usr/share/zoneinfo/Pacific/Gambier", + "/usr/share/zoneinfo/Pacific/Guadalcanal", + "/usr/share/zoneinfo/Pacific/Guam", + "/usr/share/zoneinfo/Pacific/Honolulu", + "/usr/share/zoneinfo/Pacific/Kanton", + "/usr/share/zoneinfo/Pacific/Kiritimati", + "/usr/share/zoneinfo/Pacific/Kosrae", + "/usr/share/zoneinfo/Pacific/Kwajalein", + "/usr/share/zoneinfo/Pacific/Majuro", + "/usr/share/zoneinfo/Pacific/Marquesas", + "/usr/share/zoneinfo/Pacific/Midway", + "/usr/share/zoneinfo/Pacific/Nauru", + "/usr/share/zoneinfo/Pacific/Niue", + "/usr/share/zoneinfo/Pacific/Norfolk", + "/usr/share/zoneinfo/Pacific/Noumea", + "/usr/share/zoneinfo/Pacific/Pago_Pago", + "/usr/share/zoneinfo/Pacific/Palau", + "/usr/share/zoneinfo/Pacific/Pitcairn", + "/usr/share/zoneinfo/Pacific/Pohnpei", + "/usr/share/zoneinfo/Pacific/Port_Moresby", + "/usr/share/zoneinfo/Pacific/Rarotonga", + "/usr/share/zoneinfo/Pacific/Saipan", + "/usr/share/zoneinfo/Pacific/Tahiti", + "/usr/share/zoneinfo/Pacific/Tarawa", + "/usr/share/zoneinfo/Pacific/Tongatapu", + "/usr/share/zoneinfo/Pacific/Wake", + "/usr/share/zoneinfo/Pacific/Wallis", + "/usr/share/zoneinfo/iso3166.tab", + "/usr/share/zoneinfo/leap-seconds.list", + "/usr/share/zoneinfo/leapseconds", + "/usr/share/zoneinfo/tzdata.zi", + "/usr/share/zoneinfo/zone.tab", + "/usr/share/zoneinfo/zone1970.tab", + "/usr/share/zoneinfo/zonenow.tab" + ] + }, + { + "ID": "util-linux@2.41-5", + "Name": "util-linux", + "Identifier": { + "PURL": "pkg:deb/debian/util-linux@2.41-5?arch=amd64\u0026distro=debian-13.6", + "UID": "38be4846f19b7fa" + }, + "Version": "2.41", + "Release": "5", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.41", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "GPL-3.0-or-later", + "LGPL-2.1-or-later", + "public-domain", + "BSD-4-Clause", + "MIT", + "ISC", + "BSD-3-Clause", + "BSLA", + "LGPL-2.0-or-later", + "BSD-2-Clause", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "Chris Hofstaedtler \u003czeha@debian.org\u003e", + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/bin/choom", + "/usr/bin/chrt", + "/usr/bin/dmesg", + "/usr/bin/fallocate", + "/usr/bin/findmnt", + "/usr/bin/flock", + "/usr/bin/getopt", + "/usr/bin/hardlink", + "/usr/bin/ionice", + "/usr/bin/ipcmk", + "/usr/bin/ipcrm", + "/usr/bin/ipcs", + "/usr/bin/lsblk", + "/usr/bin/lscpu", + "/usr/bin/lsipc", + "/usr/bin/lslocks", + "/usr/bin/lslogins", + "/usr/bin/lsmem", + "/usr/bin/lsns", + "/usr/bin/mcookie", + "/usr/bin/more", + "/usr/bin/mountpoint", + "/usr/bin/namei", + "/usr/bin/nsenter", + "/usr/bin/partx", + "/usr/bin/prlimit", + "/usr/bin/rename.ul", + "/usr/bin/rev", + "/usr/bin/setarch", + "/usr/bin/setpriv", + "/usr/bin/setsid", + "/usr/bin/setterm", + "/usr/bin/su", + "/usr/bin/taskset", + "/usr/bin/uclampset", + "/usr/bin/unshare", + "/usr/bin/wdctl", + "/usr/bin/whereis", + "/usr/lib/mime/packages/util-linux", + "/usr/lib/systemd/system/fstrim.service", + "/usr/lib/systemd/system/fstrim.timer", + "/usr/sbin/agetty", + "/usr/sbin/blkdiscard", + "/usr/sbin/blkid", + "/usr/sbin/blkzone", + "/usr/sbin/blockdev", + "/usr/sbin/chcpu", + "/usr/sbin/chmem", + "/usr/sbin/findfs", + "/usr/sbin/fsck", + "/usr/sbin/fsfreeze", + "/usr/sbin/fstrim", + "/usr/sbin/isosize", + "/usr/sbin/ldattach", + "/usr/sbin/mkfs", + "/usr/sbin/mkswap", + "/usr/sbin/pivot_root", + "/usr/sbin/readprofile", + "/usr/sbin/rtcwake", + "/usr/sbin/runuser", + "/usr/sbin/sulogin", + "/usr/sbin/swaplabel", + "/usr/sbin/switch_root", + "/usr/sbin/wipefs", + "/usr/sbin/zramctl", + "/usr/share/bash-completion/completions/blkdiscard", + "/usr/share/bash-completion/completions/blkid", + "/usr/share/bash-completion/completions/blkzone", + "/usr/share/bash-completion/completions/blockdev", + "/usr/share/bash-completion/completions/chcpu", + "/usr/share/bash-completion/completions/chmem", + "/usr/share/bash-completion/completions/chrt", + "/usr/share/bash-completion/completions/dmesg", + "/usr/share/bash-completion/completions/fallocate", + "/usr/share/bash-completion/completions/findfs", + "/usr/share/bash-completion/completions/findmnt", + "/usr/share/bash-completion/completions/flock", + "/usr/share/bash-completion/completions/fsck", + "/usr/share/bash-completion/completions/fsfreeze", + "/usr/share/bash-completion/completions/fstrim", + "/usr/share/bash-completion/completions/getopt", + "/usr/share/bash-completion/completions/hardlink", + "/usr/share/bash-completion/completions/ionice", + "/usr/share/bash-completion/completions/ipcmk", + "/usr/share/bash-completion/completions/ipcrm", + "/usr/share/bash-completion/completions/ipcs", + "/usr/share/bash-completion/completions/isosize", + "/usr/share/bash-completion/completions/ldattach", + "/usr/share/bash-completion/completions/lsblk", + "/usr/share/bash-completion/completions/lscpu", + "/usr/share/bash-completion/completions/lsipc", + "/usr/share/bash-completion/completions/lslocks", + "/usr/share/bash-completion/completions/lslogins", + "/usr/share/bash-completion/completions/lsmem", + "/usr/share/bash-completion/completions/lsns", + "/usr/share/bash-completion/completions/mcookie", + "/usr/share/bash-completion/completions/mkfs", + "/usr/share/bash-completion/completions/mkswap", + "/usr/share/bash-completion/completions/more", + "/usr/share/bash-completion/completions/mountpoint", + "/usr/share/bash-completion/completions/namei", + "/usr/share/bash-completion/completions/nsenter", + "/usr/share/bash-completion/completions/partx", + "/usr/share/bash-completion/completions/pivot_root", + "/usr/share/bash-completion/completions/prlimit", + "/usr/share/bash-completion/completions/readprofile", + "/usr/share/bash-completion/completions/rename.ul", + "/usr/share/bash-completion/completions/rev", + "/usr/share/bash-completion/completions/rtcwake", + "/usr/share/bash-completion/completions/setarch", + "/usr/share/bash-completion/completions/setpriv", + "/usr/share/bash-completion/completions/setsid", + "/usr/share/bash-completion/completions/setterm", + "/usr/share/bash-completion/completions/su", + "/usr/share/bash-completion/completions/swaplabel", + "/usr/share/bash-completion/completions/taskset", + "/usr/share/bash-completion/completions/uclampset", + "/usr/share/bash-completion/completions/unshare", + "/usr/share/bash-completion/completions/wdctl", + "/usr/share/bash-completion/completions/whereis", + "/usr/share/bash-completion/completions/wipefs", + "/usr/share/bash-completion/completions/zramctl", + "/usr/share/doc/util-linux/00-about-docs.txt", + "/usr/share/doc/util-linux/AUTHORS.gz", + "/usr/share/doc/util-linux/NEWS.Debian.gz", + "/usr/share/doc/util-linux/PAM-configuration.txt", + "/usr/share/doc/util-linux/README.Debian", + "/usr/share/doc/util-linux/blkid.txt", + "/usr/share/doc/util-linux/cal.txt", + "/usr/share/doc/util-linux/changelog.Debian.gz", + "/usr/share/doc/util-linux/changelog.gz", + "/usr/share/doc/util-linux/col.txt", + "/usr/share/doc/util-linux/copyright", + "/usr/share/doc/util-linux/deprecated.txt", + "/usr/share/doc/util-linux/examples/getopt-example.bash", + "/usr/share/doc/util-linux/getopt.txt", + "/usr/share/doc/util-linux/getopt_changelog.txt", + "/usr/share/doc/util-linux/howto-build-sys.txt", + "/usr/share/doc/util-linux/howto-compilation.txt", + "/usr/share/doc/util-linux/howto-contribute.txt.gz", + "/usr/share/doc/util-linux/howto-debug.txt", + "/usr/share/doc/util-linux/howto-man-page.txt", + "/usr/share/doc/util-linux/howto-pull-request.txt.gz", + "/usr/share/doc/util-linux/howto-tests.txt", + "/usr/share/doc/util-linux/howto-usage-function.txt.gz", + "/usr/share/doc/util-linux/hwclock.txt", + "/usr/share/doc/util-linux/modems-with-agetty.txt", + "/usr/share/doc/util-linux/mount.txt", + "/usr/share/doc/util-linux/parse-date.txt.gz", + "/usr/share/doc/util-linux/pg.txt", + "/usr/share/doc/util-linux/poeigl.txt.gz", + "/usr/share/doc/util-linux/release-schedule.txt", + "/usr/share/doc/util-linux/releases/v2.13-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.14-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.15-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.16-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.17-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.18-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.19-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.20-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.21-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.22-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.23-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.24-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.25-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.26-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.27-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.28-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.29-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.30-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.31-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.32-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.33-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.34-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.35-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.36-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.37-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.38-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.39-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.40-ReleaseNotes.gz", + "/usr/share/doc/util-linux/releases/v2.41-ReleaseNotes.gz", + "/usr/share/lintian/overrides/util-linux", + "/usr/share/man/man1/choom.1.gz", + "/usr/share/man/man1/chrt.1.gz", + "/usr/share/man/man1/dmesg.1.gz", + "/usr/share/man/man1/fallocate.1.gz", + "/usr/share/man/man1/flock.1.gz", + "/usr/share/man/man1/getopt.1.gz", + "/usr/share/man/man1/hardlink.1.gz", + "/usr/share/man/man1/ionice.1.gz", + "/usr/share/man/man1/ipcmk.1.gz", + "/usr/share/man/man1/ipcrm.1.gz", + "/usr/share/man/man1/ipcs.1.gz", + "/usr/share/man/man1/lscpu.1.gz", + "/usr/share/man/man1/lsipc.1.gz", + "/usr/share/man/man1/lslogins.1.gz", + "/usr/share/man/man1/lsmem.1.gz", + "/usr/share/man/man1/mcookie.1.gz", + "/usr/share/man/man1/more.1.gz", + "/usr/share/man/man1/mountpoint.1.gz", + "/usr/share/man/man1/namei.1.gz", + "/usr/share/man/man1/nsenter.1.gz", + "/usr/share/man/man1/prlimit.1.gz", + "/usr/share/man/man1/rename.ul.1.gz", + "/usr/share/man/man1/rev.1.gz", + "/usr/share/man/man1/runuser.1.gz", + "/usr/share/man/man1/setpriv.1.gz", + "/usr/share/man/man1/setsid.1.gz", + "/usr/share/man/man1/setterm.1.gz", + "/usr/share/man/man1/su.1.gz", + "/usr/share/man/man1/taskset.1.gz", + "/usr/share/man/man1/uclampset.1.gz", + "/usr/share/man/man1/unshare.1.gz", + "/usr/share/man/man1/whereis.1.gz", + "/usr/share/man/man5/adjtime_config.5.gz", + "/usr/share/man/man5/scols-filter.5.gz", + "/usr/share/man/man5/terminal-colors.d.5.gz", + "/usr/share/man/man8/agetty.8.gz", + "/usr/share/man/man8/blkdiscard.8.gz", + "/usr/share/man/man8/blkid.8.gz", + "/usr/share/man/man8/blkzone.8.gz", + "/usr/share/man/man8/blockdev.8.gz", + "/usr/share/man/man8/chcpu.8.gz", + "/usr/share/man/man8/chmem.8.gz", + "/usr/share/man/man8/findfs.8.gz", + "/usr/share/man/man8/findmnt.8.gz", + "/usr/share/man/man8/fsck.8.gz", + "/usr/share/man/man8/fsfreeze.8.gz", + "/usr/share/man/man8/fstrim.8.gz", + "/usr/share/man/man8/isosize.8.gz", + "/usr/share/man/man8/ldattach.8.gz", + "/usr/share/man/man8/lsblk.8.gz", + "/usr/share/man/man8/lslocks.8.gz", + "/usr/share/man/man8/lsns.8.gz", + "/usr/share/man/man8/mkfs.8.gz", + "/usr/share/man/man8/mkswap.8.gz", + "/usr/share/man/man8/partx.8.gz", + "/usr/share/man/man8/pivot_root.8.gz", + "/usr/share/man/man8/readprofile.8.gz", + "/usr/share/man/man8/rtcwake.8.gz", + "/usr/share/man/man8/setarch.8.gz", + "/usr/share/man/man8/sulogin.8.gz", + "/usr/share/man/man8/swaplabel.8.gz", + "/usr/share/man/man8/switch_root.8.gz", + "/usr/share/man/man8/wdctl.8.gz", + "/usr/share/man/man8/wipefs.8.gz", + "/usr/share/man/man8/zramctl.8.gz", + "/usr/share/util-linux/logcheck/ignore.d.server/util-linux" + ] + }, + { + "ID": "zlib1g@1:1.3.dfsg+really1.3.1-1+b1", + "Name": "zlib1g", + "Identifier": { + "PURL": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64\u0026distro=debian-13.6\u0026epoch=1", + "UID": "202a4bb3bdd0a341" + }, + "Version": "1.3.dfsg+really1.3.1", + "Release": "1+b1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "zlib", + "SrcVersion": "1.3.dfsg+really1.3.1", + "SrcRelease": "1", + "SrcEpoch": 1, + "Licenses": [ + "Zlib" + ], + "Maintainer": "Mark Brown \u003cbroonie@debian.org\u003e", + "DependsOn": [ + "libc6@2.41-12+deb13u3" + ], + "Layer": { + "DiffID": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + "InstalledFiles": [ + "/usr/lib/x86_64-linux-gnu/libz.so.1.3.1", + "/usr/share/doc/zlib1g/changelog.Debian.amd64.gz", + "/usr/share/doc/zlib1g/changelog.Debian.gz", + "/usr/share/doc/zlib1g/changelog.gz", + "/usr/share/doc/zlib1g/copyright" + ] + } + ] + }, + { + "Target": "Python", + "Class": "lang-pkgs", + "Type": "python-pkg", + "Packages": [ + { + "Name": "PyJWT", + "Identifier": { + "PURL": "pkg:pypi/pyjwt@2.13.0", + "UID": "f4d7837dd486cbcf" + }, + "Version": "2.13.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/METADATA" + }, + { + "Name": "annotated-types", + "Identifier": { + "PURL": "pkg:pypi/annotated-types@0.7.0", + "UID": "a99a645f3910035b" + }, + "Version": "0.7.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA" + }, + { + "Name": "anthropic", + "Identifier": { + "PURL": "pkg:pypi/anthropic@0.104.1", + "UID": "ca201891b230d4bb" + }, + "Version": "0.104.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/anthropic-0.104.1.dist-info/METADATA" + }, + { + "Name": "anyio", + "Identifier": { + "PURL": "pkg:pypi/anyio@4.13.0", + "UID": "78cf51208adea85c" + }, + "Version": "4.13.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/anyio-4.13.0.dist-info/METADATA" + }, + { + "Name": "attrs", + "Identifier": { + "PURL": "pkg:pypi/attrs@26.1.0", + "UID": "8f465452effcf3f9" + }, + "Version": "26.1.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/attrs-26.1.0.dist-info/METADATA" + }, + { + "Name": "boto3", + "Identifier": { + "PURL": "pkg:pypi/boto3@1.43.56", + "UID": "8eb29918eab19e65" + }, + "Version": "1.43.56", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/boto3-1.43.56.dist-info/METADATA" + }, + { + "Name": "botocore", + "Identifier": { + "PURL": "pkg:pypi/botocore@1.43.56", + "UID": "843d6fb6c16227e1" + }, + "Version": "1.43.56", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/botocore-1.43.56.dist-info/METADATA" + }, + { + "Name": "certifi", + "Identifier": { + "PURL": "pkg:pypi/certifi@2026.5.20", + "UID": "3bdd0c600b32a18b" + }, + "Version": "2026.5.20", + "Licenses": [ + "MPL-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/certifi-2026.5.20.dist-info/METADATA" + }, + { + "Name": "cffi", + "Identifier": { + "PURL": "pkg:pypi/cffi@2.0.0", + "UID": "a5634fada0c463fb" + }, + "Version": "2.0.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/METADATA" + }, + { + "Name": "click", + "Identifier": { + "PURL": "pkg:pypi/click@8.4.1", + "UID": "ef23f255bd3c7aee" + }, + "Version": "8.4.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/click-8.4.1.dist-info/METADATA" + }, + { + "Name": "cryptography", + "Identifier": { + "PURL": "pkg:pypi/cryptography@49.0.0", + "UID": "951dcd36e1ea33b" + }, + "Version": "49.0.0", + "Licenses": [ + "Apache-2.0 OR BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/cryptography-49.0.0.dist-info/METADATA" + }, + { + "Name": "distro", + "Identifier": { + "PURL": "pkg:pypi/distro@1.9.0", + "UID": "25523b069f98486e" + }, + "Version": "1.9.0", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/distro-1.9.0.dist-info/METADATA" + }, + { + "Name": "docstring_parser", + "Identifier": { + "PURL": "pkg:pypi/docstring-parser@0.18.0", + "UID": "3a7bde97f8e3daaf" + }, + "Version": "0.18.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/docstring_parser-0.18.0.dist-info/METADATA" + }, + { + "Name": "h11", + "Identifier": { + "PURL": "pkg:pypi/h11@0.16.0", + "UID": "680805ad174c70d5" + }, + "Version": "0.16.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA" + }, + { + "Name": "httpcore", + "Identifier": { + "PURL": "pkg:pypi/httpcore@1.0.9", + "UID": "95d8f0a84544f78b" + }, + "Version": "1.0.9", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA" + }, + { + "Name": "httpx", + "Identifier": { + "PURL": "pkg:pypi/httpx@0.28.1", + "UID": "5e8f812d1d305e74" + }, + "Version": "0.28.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA" + }, + { + "Name": "httpx-sse", + "Identifier": { + "PURL": "pkg:pypi/httpx-sse@0.4.3", + "UID": "f5b099f8cdc33946" + }, + "Version": "0.4.3", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/httpx_sse-0.4.3.dist-info/METADATA" + }, + { + "Name": "idna", + "Identifier": { + "PURL": "pkg:pypi/idna@3.16", + "UID": "7c7fd4fce939f423" + }, + "Version": "3.16", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/idna-3.16.dist-info/METADATA" + }, + { + "Name": "jiter", + "Identifier": { + "PURL": "pkg:pypi/jiter@0.15.0", + "UID": "bbe6e5a734f374ee" + }, + "Version": "0.15.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jiter-0.15.0.dist-info/METADATA" + }, + { + "Name": "jmespath", + "Identifier": { + "PURL": "pkg:pypi/jmespath@1.1.0", + "UID": "46583dd718d8c745" + }, + "Version": "1.1.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jmespath-1.1.0.dist-info/METADATA" + }, + { + "Name": "jsonschema", + "Identifier": { + "PURL": "pkg:pypi/jsonschema@4.26.0", + "UID": "d56df30beb9ba43f" + }, + "Version": "4.26.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jsonschema-4.26.0.dist-info/METADATA" + }, + { + "Name": "jsonschema-specifications", + "Identifier": { + "PURL": "pkg:pypi/jsonschema-specifications@2025.9.1", + "UID": "5d770867a8968d3e" + }, + "Version": "2025.9.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA" + }, + { + "Name": "mcp", + "Identifier": { + "PURL": "pkg:pypi/mcp@1.28.1", + "UID": "a0cb1f0f51dbe65e" + }, + "Version": "1.28.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/mcp-1.28.1.dist-info/METADATA" + }, + { + "Name": "openai", + "Identifier": { + "PURL": "pkg:pypi/openai@2.38.0", + "UID": "8a16dec2f2dda139" + }, + "Version": "2.38.0", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/openai-2.38.0.dist-info/METADATA" + }, + { + "Name": "openrath", + "Identifier": { + "PURL": "pkg:pypi/openrath@1.3.0", + "UID": "a8edf7fa75f5aec9" + }, + "Version": "1.3.0", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/openrath-1.3.0.dist-info/METADATA" + }, + { + "Name": "opentelemetry-api", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-api@1.42.1", + "UID": "a3eb2cda00eccf47" + }, + "Version": "1.42.1", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/opentelemetry_api-1.42.1.dist-info/METADATA" + }, + { + "Name": "opentelemetry-sdk", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-sdk@1.42.1", + "UID": "fd2ac5c873caf297" + }, + "Version": "1.42.1", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/opentelemetry_sdk-1.42.1.dist-info/METADATA" + }, + { + "Name": "opentelemetry-semantic-conventions", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "UID": "807ed19efd6b801f" + }, + "Version": "0.63b1", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/opentelemetry_semantic_conventions-0.63b1.dist-info/METADATA" + }, + { + "Name": "pip", + "Identifier": { + "PURL": "pkg:pypi/pip@25.0.1", + "UID": "6c029e6e913de377" + }, + "Version": "25.0.1", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + "FilePath": "usr/local/lib/python3.12/site-packages/pip-25.0.1.dist-info/METADATA" + }, + { + "Name": "psycopg", + "Identifier": { + "PURL": "pkg:pypi/psycopg@3.3.4", + "UID": "a25eee15aab7acdd" + }, + "Version": "3.3.4", + "Licenses": [ + "LGPL-3.0-only" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/psycopg-3.3.4.dist-info/METADATA" + }, + { + "Name": "psycopg-binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4", + "UID": "cddad4642fcf0c56" + }, + "Version": "3.3.4", + "Licenses": [ + "LGPL-3.0-only" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/psycopg_binary-3.3.4.dist-info/METADATA" + }, + { + "Name": "psycopg-pool", + "Identifier": { + "PURL": "pkg:pypi/psycopg-pool@3.3.1", + "UID": "1bb6269f6fec0cf5" + }, + "Version": "3.3.1", + "Licenses": [ + "LGPL-3.0-only" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/psycopg_pool-3.3.1.dist-info/METADATA" + }, + { + "ID": "psycopg_binary@3.3.4", + "Name": "psycopg_binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "UID": "76f4c37cac088f9b", + "BOMRef": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + "Version": "3.3.4", + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "ID": "psycopg_binary@3.3.4", + "Name": "psycopg_binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "UID": "56fa9b57c57faa13", + "BOMRef": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + "Version": "3.3.4", + "DependsOn": [ + "krb5-libs@1.15.1-55.el7_9", + "krb5-libs@1.15.1-55.el7_9", + "krb5-libs@1.15.1-55.el7_9", + "krb5-libs@1.15.1-55.el7_9", + "cyrus-sasl-lib@2.1.26-24.el7_9", + "pcre@8.32-17.el7", + "libselinux@2.5-15.el7", + "libcom_err@1.42.9-19.el7", + "keyutils-libs@1.5.8-3.el7" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + } + }, + { + "Name": "pycparser", + "Identifier": { + "PURL": "pkg:pypi/pycparser@3.0", + "UID": "2f3cbfabd774e0bb" + }, + "Version": "3.0", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/METADATA" + }, + { + "Name": "pydantic", + "Identifier": { + "PURL": "pkg:pypi/pydantic@2.13.4", + "UID": "85f1de8b45483025" + }, + "Version": "2.13.4", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pydantic-2.13.4.dist-info/METADATA" + }, + { + "Name": "pydantic-settings", + "Identifier": { + "PURL": "pkg:pypi/pydantic-settings@2.14.2", + "UID": "6082c4467e82801d" + }, + "Version": "2.14.2", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pydantic_settings-2.14.2.dist-info/METADATA" + }, + { + "Name": "pydantic_core", + "Identifier": { + "PURL": "pkg:pypi/pydantic-core@2.46.4", + "UID": "133a20c0f254cad5" + }, + "Version": "2.46.4", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/pydantic_core-2.46.4.dist-info/METADATA" + }, + { + "Name": "python-dateutil", + "Identifier": { + "PURL": "pkg:pypi/python-dateutil@2.9.0.post0", + "UID": "a82076284c91d9e0" + }, + "Version": "2.9.0.post0", + "Licenses": [ + "BSD-3-Clause", + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA" + }, + { + "Name": "python-dotenv", + "Identifier": { + "PURL": "pkg:pypi/python-dotenv@1.2.2", + "UID": "1d5512fb54c0f899" + }, + "Version": "1.2.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/METADATA" + }, + { + "Name": "python-multipart", + "Identifier": { + "PURL": "pkg:pypi/python-multipart@0.0.32", + "UID": "ed71009179a5e27" + }, + "Version": "0.0.32", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/METADATA" + }, + { + "Name": "redis", + "Identifier": { + "PURL": "pkg:pypi/redis@6.4.0", + "UID": "543c60c2609d9982" + }, + "Version": "6.4.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/redis-6.4.0.dist-info/METADATA" + }, + { + "Name": "referencing", + "Identifier": { + "PURL": "pkg:pypi/referencing@0.37.0", + "UID": "757400f54970ea7f" + }, + "Version": "0.37.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/referencing-0.37.0.dist-info/METADATA" + }, + { + "Name": "rpds-py", + "Identifier": { + "PURL": "pkg:pypi/rpds-py@0.30.0", + "UID": "894f633cf7410059" + }, + "Version": "0.30.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/rpds_py-0.30.0.dist-info/METADATA" + }, + { + "Name": "s3transfer", + "Identifier": { + "PURL": "pkg:pypi/s3transfer@0.19.2", + "UID": "ee16e76cffe26363" + }, + "Version": "0.19.2", + "Licenses": [ + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/s3transfer-0.19.2.dist-info/METADATA" + }, + { + "Name": "six", + "Identifier": { + "PURL": "pkg:pypi/six@1.17.0", + "UID": "3714ed1c9848d5c6" + }, + "Version": "1.17.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/six-1.17.0.dist-info/METADATA" + }, + { + "Name": "sniffio", + "Identifier": { + "PURL": "pkg:pypi/sniffio@1.3.1", + "UID": "143876ac193a8cad" + }, + "Version": "1.3.1", + "Licenses": [ + "MIT", + "Apache-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/sniffio-1.3.1.dist-info/METADATA" + }, + { + "Name": "sse-starlette", + "Identifier": { + "PURL": "pkg:pypi/sse-starlette@3.4.4", + "UID": "3f2a0e09c4935f68" + }, + "Version": "3.4.4", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/sse_starlette-3.4.4.dist-info/METADATA" + }, + { + "Name": "starlette", + "Identifier": { + "PURL": "pkg:pypi/starlette@1.3.1", + "UID": "1b21780decf6d95c" + }, + "Version": "1.3.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/starlette-1.3.1.dist-info/METADATA" + }, + { + "Name": "tqdm", + "Identifier": { + "PURL": "pkg:pypi/tqdm@4.67.3", + "UID": "bf2ffdca8ce62d66" + }, + "Version": "4.67.3", + "Licenses": [ + "MPL-2.0 AND MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/tqdm-4.67.3.dist-info/METADATA" + }, + { + "Name": "typing-inspection", + "Identifier": { + "PURL": "pkg:pypi/typing-inspection@0.4.2", + "UID": "74af1e4539fb7b0a" + }, + "Version": "0.4.2", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/METADATA" + }, + { + "Name": "typing_extensions", + "Identifier": { + "PURL": "pkg:pypi/typing-extensions@4.15.0", + "UID": "6e16270e669f33cc" + }, + "Version": "4.15.0", + "Licenses": [ + "PSF-2.0" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/METADATA" + }, + { + "Name": "urllib3", + "Identifier": { + "PURL": "pkg:pypi/urllib3@2.7.0", + "UID": "1ee89e08a0369db1" + }, + "Version": "2.7.0", + "Licenses": [ + "MIT" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/urllib3-2.7.0.dist-info/METADATA" + }, + { + "Name": "uvicorn", + "Identifier": { + "PURL": "pkg:pypi/uvicorn@0.47.0", + "UID": "5e575d6807ee87fc" + }, + "Version": "0.47.0", + "Licenses": [ + "BSD-3-Clause" + ], + "Layer": { + "DiffID": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + "FilePath": "opt/venv/lib/python3.12/site-packages/uvicorn-0.47.0.dist-info/METADATA" + } + ] + } + ] +} diff --git a/release/evidence/v2.0.0-review/manifest.json b/release/evidence/v2.0.0-review/manifest.json new file mode 100644 index 0000000..eaf7c6f --- /dev/null +++ b/release/evidence/v2.0.0-review/manifest.json @@ -0,0 +1,86 @@ +{ + "schema": "openrath.review-evidence/1", + "generated_at": "2026-07-28T12:19:38+08:00", + "baseline_commit": "ec0ac921fe95de4d925113f63b4bda0934880f42", + "implementation_commit": "51a26c4a23850876548c61340da2d2da3bc834ce", + "branch": "codex/v2-review-remediation", + "intended_release": "2.0.0", + "package_version": "1.3.0", + "release_approved": false, + "source_tree_clean_at_implementation_commit": true, + "artifacts": { + "wheel": { + "path": "dist/openrath-1.3.0-py3-none-any.whl", + "sha256": "296705e4af199393ff81cfeab0034faa07c3366bb635bc25e8e2c1f19324f388" + }, + "sdist": { + "path": "dist/openrath-1.3.0.tar.gz", + "sha256": "d34ca3adf76e5b3c99bde107a884189699c0d9de7df6da88d1b4a57217581016" + }, + "openapi": { + "path": "deploy/docs/openapi-v2.json", + "sha256": "d2af9657b8c7346d1d8ef1097418af0548fc9ad6ca2817c82eb5895497c1d648" + }, + "local_image": { + "reference": "openrath:review", + "local_image_id": "sha256:181247207c0f57e438c42676579a32d98cd7f9ea66c2c6b1bf2c233526723b14", + "registry_digest": null + }, + "sbom": { + "path": "release/evidence/v2.0.0-review/openrath-v2-review.sbom.cdx.json", + "sha256": "cd38253ce326ee89c2e1075a2aa39aa7374cbdefe840c0e087527f198e9398ad" + }, + "image_scan": { + "path": "release/evidence/v2.0.0-review/image-scan-high-critical.json", + "sha256": "824fd0f2b1eff05156c58ba16df4c08ca2eacb7635536291a37405e291c3afdb", + "fixed_high_or_critical_findings": 0 + }, + "repository_secret_scan": { + "path": "release/evidence/v2.0.0-review/repository-secret-scan.json", + "sha256": "8fc6fd9d2056aa23cc1d3e1719364096fccfae487164a990ea201df085df281d", + "findings": 0 + } + }, + "validation": { + "ruff": "passed", + "mypy_source_files": 166, + "offline_tests": { + "passed": 1043, + "skipped": 20, + "failed": 0 + }, + "openapi_contract_tests": { + "passed": 2, + "failed": 0 + }, + "opensandbox_real": { + "full_run_passed": 45, + "full_run_skipped": 3, + "environment_sensitive_timing_failures": 1, + "corrected_focused_rerun_passed": 1 + }, + "postgres_redis_s3_integration": { + "passed": 10, + "failed": 0 + }, + "dependency_audits": { + "production": "no known vulnerabilities", + "all_extras_and_groups": "no known vulnerabilities" + }, + "review_soak": { + "duration_seconds": 10.032811900004162, + "completed_runs": 143, + "failed_runs": 0, + "profile": "sqlite-single-worker-one-step" + } + }, + "blocking_gates": [ + "approved live LLM/provider lifecycle", + "approved live OpenViking lifecycle", + "published immutable OpenRath registry image digest", + "eight-hour target-like soak", + "one-to-four worker scale test", + "target-cluster backup/restore and rollout/rollback drills", + "final CI and evidence regeneration on the frozen RC commit" + ] +} diff --git a/release/evidence/v2.0.0-review/openrath-v2-review.sbom.cdx.json b/release/evidence/v2.0.0-review/openrath-v2-review.sbom.cdx.json new file mode 100644 index 0000000..96c1b38 --- /dev/null +++ b/release/evidence/v2.0.0-review/openrath-v2-review.sbom.cdx.json @@ -0,0 +1,9408 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:c25b7b2c-da51-49ea-b75a-85137e8113de", + "version": 1, + "metadata": { + "timestamp": "2026-07-28T04:18:22+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "0.67.2" + } + ] + }, + "component": { + "bom-ref": "f11d519f-8176-41a9-9ea2-c3f1841f1fa9", + "type": "container", + "name": "openrath:review", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:705f755ad342993f1a9bbe9922cbab983321521117c79d796018013fab05e4d8" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b456d050d640df9ffbe456b81bbf11ed446fb23372063f6ce701e29fe74eb1a1" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b80f3ed1ee6de85c788d9ae7203207c44724eab4baac8697390ca1412954ad2f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:f2776a24e2368b14a6e05e79b2abd1f2b85fb5217de0b9b924fb2a926538d169" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:181247207c0f57e438c42676579a32d98cd7f9ea66c2c6b1bf2c233526723b14" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "openrath:review" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:Size", + "value": "239066624" + } + ] + } + }, + "components": [ + { + "bom-ref": "f68ad4e5-afcc-4b65-9e7f-75fa6ce2c944", + "type": "operating-system", + "name": "debian", + "version": "13.6", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Adduser Developers " + }, + "name": "adduser", + "version": "3.152", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "adduser@3.152" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "adduser" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.152" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "APT Development Team " + }, + "name": "apt", + "version": "3.0.3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "curl" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "apt@3.0.3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "apt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.0.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "13.8+deb13u6", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "verbatim" + } + } + ], + "purl": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@13.8+deb13u6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "13.8+deb13u6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "base-passwd", + "version": "3.6.7", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-passwd@3.6.7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-passwd" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.6.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "bash", + "version": "5.2.37-2+b9", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "name": "Latex2e" + } + }, + { + "license": { + "id": "BSD-4-Clause-UC" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "permissive" + } + } + ], + "purl": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "bash@5.2.37-2+b9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.2.37" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "bsdutils", + "version": "1:2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "bsdutils@1:2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Julien Cristau " + }, + "name": "ca-certificates", + "version": "20250419", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "MPL-2.0" + } + } + ], + "purl": "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ca-certificates@20250419" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ca-certificates" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "20250419" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Michael Stone " + }, + "name": "coreutils", + "version": "9.7-3", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "BSD-4-Clause-UC" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + } + ], + "purl": "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "coreutils@9.7-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "coreutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "9.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Andrej Shadura " + }, + "name": "dash", + "version": "0.5.12-12", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "dash@0.5.12-12" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "dash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.5.12" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debconf Developers " + }, + "name": "debconf", + "version": "1.5.91", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + } + ], + "purl": "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debconf@1.5.91" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debconf" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.91" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Release Team " + }, + "name": "debian-archive-keyring", + "version": "2025.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debian-archive-keyring@2025.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debian-archive-keyring" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2025.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ileana Dumitrescu " + }, + "name": "debianutils", + "version": "5.23.2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "SMAIL-GPL" + } + } + ], + "purl": "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debianutils@5.23.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debianutils" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.23.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "diffutils", + "version": "1:3.10-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "GPL-3.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH texinfo-exception" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-only" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + } + ], + "purl": "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "diffutils@1:3.10-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "diffutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.10" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Dpkg Developers " + }, + "name": "dpkg", + "version": "1.22.22", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "public-domain-s-s-d" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "dpkg@1.22.22" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "dpkg" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.22.22" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Andreas Metzler " + }, + "name": "findutils", + "version": "4.10.0-3", + "licenses": [ + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH Autoconf-data-exception" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Autoconf-data-exception" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH automake-exception" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-2.2-exception" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "findutils@4.10.0-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "findutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.10.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "gcc-14-base", + "version": "14.2.0-19", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.2-only" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gcc-14-base@14.2.0-19" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-14" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "14.2.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Anibal Monsalve Salazar " + }, + "name": "grep", + "version": "3.11-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "grep@3.11-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "grep" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.11" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Milan Kupcevic " + }, + "name": "gzip", + "version": "1.13-1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "GFDL-1.3--no-invariant" + } + }, + { + "license": { + "name": "FSF-manpages" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GFDL-3" + } + } + ], + "purl": "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gzip@1.13-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gzip" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Michael Meskes " + }, + "name": "hostname", + "version": "3.25", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "hostname@3.25" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "hostname" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.25" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "init-system-helpers", + "version": "1.69~deb13u1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "init-system-helpers@1.69~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "init-system-helpers" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.69~deb13u1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libacl1", + "version": "2.3.2-2+b1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libacl1@2.3.2-2+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "acl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.3.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "APT Development Team " + }, + "name": "libapt-pkg7.0", + "version": "3.0.3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "curl" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libapt-pkg7.0@3.0.3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "apt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.0.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libattr1", + "version": "1:2.5.2-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libattr1@1:2.5.2-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "attr" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Laurent Bigonville " + }, + "name": "libaudit-common", + "version": "1:4.0.2-2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libaudit-common@1:4.0.2-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "audit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.0.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Laurent Bigonville " + }, + "name": "libaudit1", + "version": "1:4.0.2-2+b2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libaudit1@1:4.0.2-2+b2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "audit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.0.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libblkid1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libblkid1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libbsd0", + "version": "0.12.2-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Regents" + } + }, + { + "license": { + "id": "BSD-2-Clause-NetBSD" + } + }, + { + "license": { + "name": "BSD-3-clause-author" + } + }, + { + "license": { + "name": "BSD-3-clause-John-Birrell" + } + }, + { + "license": { + "name": "BSD-5-clause-Peter-Wemm" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-2-clause-verbatim" + } + }, + { + "license": { + "name": "BSD-2-clause-author" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "ISC-Original" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libbsd0@0.12.2-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libbsd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.12.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Anibal Monsalve Salazar " + }, + "name": "libbz2-1.0", + "version": "1.0.8-6", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libbz2-1.0@1.0.8-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bzip2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.0.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "libc-bin", + "version": "2.41-12+deb13u3", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "name": "LGPL-2.1-or-later WITH link-exception" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH link-exception" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "Carnegie" + } + }, + { + "license": { + "name": "Inner-Net" + } + }, + { + "license": { + "name": "MIT-like-Lord" + } + }, + { + "license": { + "name": "BSD-like-Spencer" + } + }, + { + "license": { + "name": "PCRE" + } + }, + { + "license": { + "name": "BSD-3-clause-Carnegie" + } + }, + { + "license": { + "id": "Unicode-DFS-2016" + } + }, + { + "license": { + "id": "BSL-1.0" + } + }, + { + "license": { + "name": "SunPro" + } + }, + { + "license": { + "name": "CORE-MATH" + } + }, + { + "license": { + "name": "BSD-3-clause-Berkeley" + } + }, + { + "license": { + "name": "BSD-3-clause-WIDE" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Oracle" + } + }, + { + "license": { + "name": "DEC" + } + }, + { + "license": { + "name": "IBM" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "Univ-Coimbra" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc-bin@2.41-12+deb13u3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12+deb13u3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "libc6", + "version": "2.41-12+deb13u3", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "name": "LGPL-2.1-or-later WITH link-exception" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH link-exception" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "Carnegie" + } + }, + { + "license": { + "name": "Inner-Net" + } + }, + { + "license": { + "name": "MIT-like-Lord" + } + }, + { + "license": { + "name": "BSD-like-Spencer" + } + }, + { + "license": { + "name": "PCRE" + } + }, + { + "license": { + "name": "BSD-3-clause-Carnegie" + } + }, + { + "license": { + "id": "Unicode-DFS-2016" + } + }, + { + "license": { + "id": "BSL-1.0" + } + }, + { + "license": { + "name": "SunPro" + } + }, + { + "license": { + "name": "CORE-MATH" + } + }, + { + "license": { + "name": "BSD-3-clause-Berkeley" + } + }, + { + "license": { + "name": "BSD-3-clause-WIDE" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Oracle" + } + }, + { + "license": { + "name": "DEC" + } + }, + { + "license": { + "name": "IBM" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "Univ-Coimbra" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc6@2.41-12+deb13u3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12+deb13u3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Håvard F. Aasen " + }, + "name": "libcap-ng0", + "version": "0.8.5-4+b1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcap-ng0@0.8.5-4+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcap-ng" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.8.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Christian Kastner " + }, + "name": "libcap2", + "version": "1:2.75-10+deb13u1+b1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcap2@1:2.75-10+deb13u1+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcap2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "10+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.75" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "libcrypt1", + "version": "1:4.4.38-1", + "purl": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcrypt1@1:4.4.38-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libxcrypt" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.4.38" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian QA Group " + }, + "name": "libdb5.3t64", + "version": "5.3.28+dfsg2-9", + "licenses": [ + { + "license": { + "id": "Sleepycat" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MS-PL" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "name": "MIT-old" + } + }, + { + "license": { + "name": "TCL-like" + } + }, + { + "license": { + "name": "BSD-3-clause-fjord" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "Zlib" + } + } + ], + "purl": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libdb5.3t64@5.3.28+dfsg2-9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "db5.3" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.3.28+dfsg2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Install System Team " + }, + "name": "libdebconfclient0", + "version": "0.280", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libdebconfclient0@0.280" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "cdebconf" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.280" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libffi8", + "version": "3.4.8-2", + "licenses": [ + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "MPL-1.1" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libffi8@3.4.8-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libffi" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.4.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libgcc-s1", + "version": "14.2.0-19", + "purl": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgcc-s1@14.2.0-19" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-14" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "14.2.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Nicolas Mora " + }, + "name": "libgdbm6t64", + "version": "1.24-2", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgdbm6t64@1.24-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gdbm" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.24" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "type": "library", + "supplier": { + "name": "Debian Science Maintainers " + }, + "name": "libgmp10", + "version": "2:6.3.0+dfsg-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgmp10@2:6.3.0+dfsg-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gmp" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.3.0+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Magnus Holmgren " + }, + "name": "libhogweed6t64", + "version": "3.10.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "GPL-3.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "GAP" + } + } + ], + "purl": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libhogweed6t64@3.10.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "nettle" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.10.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "liblastlog2-2", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblastlog2-2@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Nobuhiro Iwamatsu " + }, + "name": "liblz4-1", + "version": "1.10.0-4", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblz4-1@1.10.0-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "lz4" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.10.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sebastian Andrzej Siewior " + }, + "name": "liblzma5", + "version": "5.8.1-1+deb13u1", + "licenses": [ + { + "license": { + "id": "0BSD" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "FSFULLR" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Autoconf-exception-macro" + } + }, + { + "license": { + "name": "none" + } + }, + { + "license": { + "name": "PD" + } + }, + { + "license": { + "name": "permissive-nowarranty" + } + }, + { + "license": { + "name": "FSFUL" + } + }, + { + "license": { + "name": "noderivs" + } + }, + { + "license": { + "name": "PD-debian" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblzma5@5.8.1-1+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "xz-utils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libmd0", + "version": "1.1.0-2+b1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSD-3-clause-Aaron-D-Gifford" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-2-Clause-NetBSD" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "Beerware" + } + }, + { + "license": { + "name": "public-domain-md4" + } + }, + { + "license": { + "name": "public-domain-md5" + } + }, + { + "license": { + "name": "public-domain-sha1" + } + } + ], + "purl": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libmd0@1.1.0-2+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libmd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.1.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libmount1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libmount1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "libncursesw6", + "version": "6.5+20250216-2", + "purl": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libncursesw6@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Magnus Holmgren " + }, + "name": "libnettle8t64", + "version": "3.10.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "GPL-3.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "GAP" + } + } + ], + "purl": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libnettle8t64@3.10.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "nettle" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.10.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam-modules-bin", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-modules-bin@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam-modules", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-modules@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam-runtime", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-runtime@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Sam Hartman " + }, + "name": "libpam0g", + "version": "1.7.0-5", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "name": "BSD-tcp-wrappers" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "name": "Beerware" + } + } + ], + "purl": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam0g@1.7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthew Vernon " + }, + "name": "libpcre2-8-0", + "version": "10.46-1~deb13u1", + "licenses": [ + { + "license": { + "name": "BSD-3-clause-Cambridge WITH BINARY-LIBRARY-LIKE-PACKAGES-exception" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpcre2-8-0@10.46-1~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pcre2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "10.46" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "libreadline8t64", + "version": "8.2-6", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + }, + { + "license": { + "name": "ISC-no-attribution" + } + } + ], + "purl": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libreadline8t64@8.2-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "readline" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Kees Cook " + }, + "name": "libseccomp2", + "version": "2.6.0-2", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libseccomp2@2.6.0-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libseccomp" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.6.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libselinux1", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libselinux1@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libselinux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsemanage-common", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsemanage-common@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsemanage" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsemanage2", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsemanage2@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsemanage" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsepol2", + "version": "3.8.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsepol2@3.8.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsepol" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.8.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libsmartcols1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsmartcols1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Laszlo Boszormenyi (GCS) " + }, + "name": "libsqlite3-0", + "version": "3.46.1-7+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsqlite3-0@3.46.1-7+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sqlite3" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "7+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.46.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "libssl3t64", + "version": "3.5.6-1~deb13u2", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libssl3t64@3.5.6-1~deb13u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libstdc++6", + "version": "14.2.0-19", + "purl": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libstdc++6@14.2.0-19" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-14" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "14.2.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "libsystemd0", + "version": "257.13-1~deb13u1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH Linux-syscall-note-exception" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsystemd0@257.13-1~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "systemd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "257.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "libtinfo6", + "version": "6.5+20250216-2", + "licenses": [ + { + "license": { + "name": "MIT-X11" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libtinfo6@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "libudev1", + "version": "257.13-1~deb13u1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH Linux-syscall-note-exception" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libudev1@257.13-1~deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "systemd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "257.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "libuuid1", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libuuid1@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Josue Ortega " + }, + "name": "libxxhash0", + "version": "0.8.3-2", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libxxhash0@0.8.3-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "xxhash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.8.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "RPM packaging team " + }, + "name": "libzstd1", + "version": "1.5.7+dfsg-1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libzstd1@1.5.7+dfsg-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libzstd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.7+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "login.defs", + "version": "1:4.17.4-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "login.defs@1:4.17.4-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "shadow" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.17.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "login", + "version": "1:4.16.0-2+really2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "login@1:4.16.0-2+really2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Boyuan Yang " + }, + "name": "mawk", + "version": "1.3.4.20250131-1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "CC-BY-3.0" + } + } + ], + "purl": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "mawk@1.3.4.20250131-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "mawk" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.4.20250131" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "mount", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "mount@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "ncurses-base", + "version": "6.5+20250216-2", + "licenses": [ + { + "license": { + "name": "MIT-X11" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ncurses-base@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Ncurses Maintainers " + }, + "name": "ncurses-bin", + "version": "6.5+20250216-2", + "licenses": [ + { + "license": { + "name": "MIT-X11" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ncurses-bin@6.5+20250216-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5+20250216" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Marco d'Itri " + }, + "name": "netbase", + "version": "6.5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "netbase@6.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "netbase" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "openssl-provider-legacy", + "version": "3.5.6-1~deb13u2", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "openssl-provider-legacy@3.5.6-1~deb13u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "openssl", + "version": "3.5.6-1~deb13u2", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:ccbaccfc0388284959cf106031557105fad2067d6c4435937b3414ce90760167" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "openssl@3.5.6-1~deb13u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1~deb13u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "passwd", + "version": "1:4.17.4-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "passwd@1:4.17.4-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "shadow" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.17.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Niko Tyni " + }, + "name": "perl-base", + "version": "5.40.1-6", + "licenses": [ + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "REGCOMP" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH bison-exception+" + } + }, + { + "license": { + "name": "Unicode" + } + }, + { + "license": { + "name": "BZIP" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "FSFAP" + } + }, + { + "license": { + "name": "BSD-3-Clause WITH weird-numbering" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "TEXT-TABS" + } + }, + { + "license": { + "name": "BSD-4-clause-POWERDOG" + } + }, + { + "license": { + "name": "BSD-3-clause-GENERIC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "SDBM-PUBLIC-DOMAIN" + } + }, + { + "license": { + "name": "DONT-CHANGE-THE-GPL" + } + }, + { + "license": { + "name": "Artistic-dist" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "Artistic-2" + } + } + ], + "purl": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "perl-base@5.40.1-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "perl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.40.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "readline-common", + "version": "8.2-6", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + }, + { + "license": { + "name": "ISC-no-attribution" + } + } + ], + "purl": "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "readline-common@8.2-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "readline" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Clint Adams " + }, + "name": "sed", + "version": "4.9-2+deb13u1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "X11" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-4-Clause-UC" + } + }, + { + "license": { + "name": "BSL-1" + } + }, + { + "license": { + "name": "pcre" + } + } + ], + "purl": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sed@4.9-2+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sed" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.9" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian Rust Maintainers " + }, + "name": "sqv", + "version": "1.3.0-3+b2", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sqv@1.3.0-3+b2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "rust-sequoia-sqv" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Debian sysvinit maintainers " + }, + "name": "sysvinit-utils", + "version": "3.14-4", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sysvinit-utils@3.14-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sysvinit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.14" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Janos Lenart " + }, + "name": "tar", + "version": "1.35+dfsg-3.1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "name": "GPL-3.0-or-later WITH Bison-exception" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tar@1.35+dfsg-3.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tar" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.35+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2026b-0+deb13u1", + "licenses": [ + { + "license": { + "name": "public-domain" + } + } + ], + "purl": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2026b-0+deb13u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb13u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2026b" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "type": "library", + "supplier": { + "name": "Chris Hofstaedtler " + }, + "name": "util-linux", + "version": "2.41-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "BSLA" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "util-linux@2.41-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.41" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "type": "library", + "supplier": { + "name": "Mark Brown " + }, + "name": "zlib1g", + "version": "1:1.3.dfsg+really1.3.1-1+b1", + "licenses": [ + { + "license": { + "id": "Zlib" + } + } + ], + "purl": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "zlib1g@1:1.3.dfsg+really1.3.1-1+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "zlib" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.dfsg+really1.3.1" + } + ] + }, + { + "bom-ref": "pkg:pypi/annotated-types@0.7.0", + "type": "library", + "name": "annotated-types", + "version": "0.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "b11011181822ac765c9f66c8aa42c26952de6a96" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/annotated-types@0.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/anthropic@0.104.1", + "type": "library", + "name": "anthropic", + "version": "0.104.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "6a0fce5932599b482bf25ed3db3c06e9211f1358" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/anthropic@0.104.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/anthropic-0.104.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/anyio@4.13.0", + "type": "library", + "name": "anyio", + "version": "4.13.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "5f30168435645daddf756ecef34992631c6e778b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/anyio@4.13.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/anyio-4.13.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/attrs@26.1.0", + "type": "library", + "name": "attrs", + "version": "26.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "89068272cc1dc340d8fd910a62be241c42414339" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/attrs@26.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/attrs-26.1.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/boto3@1.43.56", + "type": "library", + "name": "boto3", + "version": "1.43.56", + "hashes": [ + { + "alg": "SHA-1", + "content": "f5c7842f2414d0cd2cefdb918d07c52018450ff7" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/boto3@1.43.56", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/boto3-1.43.56.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/botocore@1.43.56", + "type": "library", + "name": "botocore", + "version": "1.43.56", + "hashes": [ + { + "alg": "SHA-1", + "content": "b8760d1db82eba72c06ec96252b62d541a21b09e" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/botocore@1.43.56", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/botocore-1.43.56.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/certifi@2026.5.20", + "type": "library", + "name": "certifi", + "version": "2026.5.20", + "hashes": [ + { + "alg": "SHA-1", + "content": "cb42a7b0ba6491d51e71ff594a39bfaf5b8f9d22" + } + ], + "licenses": [ + { + "license": { + "id": "MPL-2.0" + } + } + ], + "purl": "pkg:pypi/certifi@2026.5.20", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/certifi-2026.5.20.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/cffi@2.0.0", + "type": "library", + "name": "cffi", + "version": "2.0.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "87e9c9d276c4f4c31f5a314d6a5472f45655674c" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/cffi@2.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/cffi-2.0.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/click@8.4.1", + "type": "library", + "name": "click", + "version": "8.4.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "c486957c59cc28072021bdcfe6d681e9ddafc860" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/click@8.4.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/click-8.4.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/cryptography@49.0.0", + "type": "library", + "name": "cryptography", + "version": "49.0.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "739372eb2cc71602103a206e7a42a926930cecb6" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR BSD-3-Clause" + } + ], + "purl": "pkg:pypi/cryptography@49.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/cryptography-49.0.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/distro@1.9.0", + "type": "library", + "name": "distro", + "version": "1.9.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "ce14620cf14e15a64d2ff574796543f99619e7f3" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/distro@1.9.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/distro-1.9.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/docstring-parser@0.18.0", + "type": "library", + "name": "docstring_parser", + "version": "0.18.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "cd475f73b404c399cf87b9fb990fc14669479d24" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/docstring-parser@0.18.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/docstring_parser-0.18.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/h11@0.16.0", + "type": "library", + "name": "h11", + "version": "0.16.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "5d41eddffefef5f6e8ff383a2537e81a38a37807" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/h11@0.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/httpcore@1.0.9", + "type": "library", + "name": "httpcore", + "version": "1.0.9", + "hashes": [ + { + "alg": "SHA-1", + "content": "2981d359ae33f31d339189a9680db85785339a56" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/httpcore@1.0.9", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/httpx-sse@0.4.3", + "type": "library", + "name": "httpx-sse", + "version": "0.4.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "df05446be58f0a3a306e50c7ceebef24bb383a6d" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/httpx-sse@0.4.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/httpx_sse-0.4.3.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/httpx@0.28.1", + "type": "library", + "name": "httpx", + "version": "0.28.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "537da7e4f29438278e124e10e02d1a500fe33bcc" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/httpx@0.28.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/idna@3.16", + "type": "library", + "name": "idna", + "version": "3.16", + "hashes": [ + { + "alg": "SHA-1", + "content": "191a7bf1dac83b0cc997024ebb4b8da5c962ae5b" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/idna@3.16", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/idna-3.16.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jiter@0.15.0", + "type": "library", + "name": "jiter", + "version": "0.15.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9de68ecc913d85eafa70a1252b2f5b1b2d2829f5" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jiter@0.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jiter-0.15.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jmespath@1.1.0", + "type": "library", + "name": "jmespath", + "version": "1.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "d3f297922cf04b0cc127a18994ad6e6b947f0cc7" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jmespath@1.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jmespath-1.1.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jsonschema-specifications@2025.9.1", + "type": "library", + "name": "jsonschema-specifications", + "version": "2025.9.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "ac33f477be9d3336ae67bc454f68a9ff39c91cf3" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jsonschema-specifications@2025.9.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/jsonschema@4.26.0", + "type": "library", + "name": "jsonschema", + "version": "4.26.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "94b3d1a46cf55d74e42c401eaf4a4b71c76cee31" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/jsonschema@4.26.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/jsonschema-4.26.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/mcp@1.28.1", + "type": "library", + "name": "mcp", + "version": "1.28.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "22fedbbf2f1d94917eba0c0c156781325bd3d786" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/mcp@1.28.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/mcp-1.28.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/openai@2.38.0", + "type": "library", + "name": "openai", + "version": "2.38.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "ea17f685d13b1a896fb055b3bedbfcb5ed8da63a" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/openai@2.38.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/openai-2.38.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/openrath@1.3.0", + "type": "library", + "name": "openrath", + "version": "1.3.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "8009f0cff7caeee9b8be0fe36779e85219f6d112" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/openrath@1.3.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/openrath-1.3.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/opentelemetry-api@1.42.1", + "type": "library", + "name": "opentelemetry-api", + "version": "1.42.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "a54ebbf560cfb13acfd3d7f7a5c385f03e36be08" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/opentelemetry-api@1.42.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/opentelemetry_api-1.42.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/opentelemetry-sdk@1.42.1", + "type": "library", + "name": "opentelemetry-sdk", + "version": "1.42.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "cdfb9c90d3e4765c62ffce81a835445a8efef7c4" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/opentelemetry-sdk@1.42.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/opentelemetry_sdk-1.42.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "type": "library", + "name": "opentelemetry-semantic-conventions", + "version": "0.63b1", + "hashes": [ + { + "alg": "SHA-1", + "content": "5c7aaa298ae1ba489d7214ebec1427bd3f602556" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/opentelemetry_semantic_conventions-0.63b1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pip@25.0.1", + "type": "library", + "name": "pip", + "version": "25.0.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "7e11d8be93f8cb02d43d078fc3a87cceb5b6cac9" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pip@25.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "usr/local/lib/python3.12/site-packages/pip-25.0.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:83fdf57f71f28b640f11d5072c284c81eadefc5ea538050fedcedba6149879bd" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg-binary@3.3.4", + "type": "library", + "name": "psycopg-binary", + "version": "3.3.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "e8c69d729c969a8a81834cce24f8947ad560813d" + } + ], + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:pypi/psycopg-binary@3.3.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/psycopg_binary-3.3.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg-pool@3.3.1", + "type": "library", + "name": "psycopg-pool", + "version": "3.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "60d07aa411067306f5244d0481227a78a5b11135" + } + ], + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:pypi/psycopg-pool@3.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/psycopg_pool-3.3.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg@3.3.4", + "type": "library", + "name": "psycopg", + "version": "3.3.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "dc38178dd59b090117f63d60bc627e6a168205c8" + } + ], + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:pypi/psycopg@3.3.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/psycopg-3.3.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "type": "library", + "name": "psycopg_binary", + "version": "3.3.4", + "purl": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "psycopg_binary@3.3.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "type": "library", + "name": "psycopg_binary", + "version": "3.3.4", + "purl": "pkg:pypi/psycopg-binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "psycopg_binary@3.3.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pycparser@3.0", + "type": "library", + "name": "pycparser", + "version": "3.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "ec46323dcd4dd2f7742b74b09cc0b030e330e46f" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/pycparser@3.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pycparser-3.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pydantic-core@2.46.4", + "type": "library", + "name": "pydantic_core", + "version": "2.46.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "f44318e9ae79f745f1f5a7a59b43044ab6fff485" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pydantic-core@2.46.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pydantic_core-2.46.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pydantic-settings@2.14.2", + "type": "library", + "name": "pydantic-settings", + "version": "2.14.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "cdbe89c33fcb95fd706f07af245dea549c06d52b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pydantic-settings@2.14.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pydantic_settings-2.14.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pydantic@2.13.4", + "type": "library", + "name": "pydantic", + "version": "2.13.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "291e482df82749c7e52c21e8275551f04de3034b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pydantic@2.13.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pydantic-2.13.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/pyjwt@2.13.0", + "type": "library", + "name": "PyJWT", + "version": "2.13.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "78125d2bb60e70bc168fd8787075ceb6a69d4f65" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/pyjwt@2.13.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/python-dateutil@2.9.0.post0", + "type": "library", + "name": "python-dateutil", + "version": "2.9.0.post0", + "hashes": [ + { + "alg": "SHA-1", + "content": "7a3c35abd86cd96034d5afb0d4b241dc9e13e6f8" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/python-dateutil@2.9.0.post0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/python-dotenv@1.2.2", + "type": "library", + "name": "python-dotenv", + "version": "1.2.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "a70b92340410dfaf8ce628e658f176278fa2e557" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/python-dotenv@1.2.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/python_dotenv-1.2.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/python-multipart@0.0.32", + "type": "library", + "name": "python-multipart", + "version": "0.0.32", + "hashes": [ + { + "alg": "SHA-1", + "content": "9f79572b3702bdac7487183b498470341276b17e" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/python-multipart@0.0.32", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/redis@6.4.0", + "type": "library", + "name": "redis", + "version": "6.4.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9a3de9ffc83addb0d845a4f16c6a415db91a3eda" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/redis@6.4.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/redis-6.4.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/referencing@0.37.0", + "type": "library", + "name": "referencing", + "version": "0.37.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "f6fa004340bef5d23995b09dab75ce12e3f367a7" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/referencing@0.37.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/referencing-0.37.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/rpds-py@0.30.0", + "type": "library", + "name": "rpds-py", + "version": "0.30.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9eef46c842a0ca6229680d7bfc1272958efdcc5a" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/rpds-py@0.30.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/rpds_py-0.30.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/s3transfer@0.19.2", + "type": "library", + "name": "s3transfer", + "version": "0.19.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "453d1af3240f56bb5d0bd093eb3d21a250cc5777" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/s3transfer@0.19.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/s3transfer-0.19.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/six@1.17.0", + "type": "library", + "name": "six", + "version": "1.17.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "483a26554261f6c839703c0e1183f3ef33ff97f1" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/six@1.17.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/six-1.17.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/sniffio@1.3.1", + "type": "library", + "name": "sniffio", + "version": "1.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "bc1d7aead770fe23c8d22666b84558edb3686da3" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:pypi/sniffio@1.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/sniffio-1.3.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/sse-starlette@3.4.4", + "type": "library", + "name": "sse-starlette", + "version": "3.4.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "58e4ad3946eafb572e0219f89bb599bee2b7cca2" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/sse-starlette@3.4.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/sse_starlette-3.4.4.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/starlette@1.3.1", + "type": "library", + "name": "starlette", + "version": "1.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "9e43f99dc64bcf4498d65999f9cb36a40fa5e94c" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/starlette@1.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/starlette-1.3.1.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/tqdm@4.67.3", + "type": "library", + "name": "tqdm", + "version": "4.67.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "0135af1981d2b0f1326020f991192d869693b873" + } + ], + "licenses": [ + { + "expression": "MPL-2.0 AND MIT" + } + ], + "purl": "pkg:pypi/tqdm@4.67.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/tqdm-4.67.3.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/typing-extensions@4.15.0", + "type": "library", + "name": "typing_extensions", + "version": "4.15.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "c5c2ce18351f8f2ae0f4a6f7c84c523f342010ee" + } + ], + "licenses": [ + { + "license": { + "name": "PSF-2.0" + } + } + ], + "purl": "pkg:pypi/typing-extensions@4.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/typing_extensions-4.15.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/typing-inspection@0.4.2", + "type": "library", + "name": "typing-inspection", + "version": "0.4.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "455fdb9c8e246ba02c2a28655287401b62028b60" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/typing-inspection@0.4.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/typing_inspection-0.4.2.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/urllib3@2.7.0", + "type": "library", + "name": "urllib3", + "version": "2.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "d20520d0598c114ced8d55ed14209a2a3bbee22c" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:pypi/urllib3@2.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/urllib3-2.7.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:pypi/uvicorn@0.47.0", + "type": "library", + "name": "uvicorn", + "version": "0.47.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "f55652a6d9d6137b9be4cfc40b4c3d30b63717f8" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:pypi/uvicorn@0.47.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "opt/venv/lib/python3.12/site-packages/uvicorn-0.47.0.dist-info/METADATA" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "python-pkg" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2", + "type": "library", + "name": "cyrus-sasl-lib", + "version": "2.1.26-24.el7_9", + "purl": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "cyrus-sasl-lib@2.1.26-24.el7_9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "cyrus-sasl-lib" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "24.el7_9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.1.26" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9", + "type": "library", + "name": "keyutils-libs", + "version": "1.5.8-3.el7", + "purl": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "keyutils-libs@1.5.8-3.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "keyutils-libs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.8" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#e140566d00db0f579c699b6d9e76106beaf31c2b7fb445a03fa070ecbba23222", + "type": "library", + "name": "krb5-libs", + "version": "1.15.1-55.el7_9", + "purl": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "krb5-libs@1.15.1-55.el7_9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "krb5-libs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "55.el7_9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.15.1" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce", + "type": "library", + "name": "libcom_err", + "version": "1.42.9-19.el7", + "purl": "pkg:rpm/centos/libcom_err@1.42.9-19.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcom_err@1.42.9-19.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcom_err" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "19.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.42.9" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed", + "type": "library", + "name": "libselinux", + "version": "2.5-15.el7", + "purl": "pkg:rpm/centos/libselinux@2.5-15.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libselinux@2.5-15.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libselinux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "15.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72", + "type": "library", + "name": "pcre", + "version": "8.32-17.el7", + "purl": "pkg:rpm/centos/pcre@8.32-17.el7", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b8469e6975a0e7c33e72820e0400106aa5a6ac23f24fc64a3e3ab85b4713777f" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "pcre@8.32-17.el7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pcre" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "17.el7" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.32" + } + ] + } + ], + "dependencies": [ + { + "ref": "f11d519f-8176-41a9-9ea2-c3f1841f1fa9", + "dependsOn": [ + "f68ad4e5-afcc-4b65-9e7f-75fa6ce2c944", + "pkg:pypi/annotated-types@0.7.0", + "pkg:pypi/anthropic@0.104.1", + "pkg:pypi/anyio@4.13.0", + "pkg:pypi/attrs@26.1.0", + "pkg:pypi/boto3@1.43.56", + "pkg:pypi/botocore@1.43.56", + "pkg:pypi/certifi@2026.5.20", + "pkg:pypi/cffi@2.0.0", + "pkg:pypi/click@8.4.1", + "pkg:pypi/cryptography@49.0.0", + "pkg:pypi/distro@1.9.0", + "pkg:pypi/docstring-parser@0.18.0", + "pkg:pypi/h11@0.16.0", + "pkg:pypi/httpcore@1.0.9", + "pkg:pypi/httpx-sse@0.4.3", + "pkg:pypi/httpx@0.28.1", + "pkg:pypi/idna@3.16", + "pkg:pypi/jiter@0.15.0", + "pkg:pypi/jmespath@1.1.0", + "pkg:pypi/jsonschema-specifications@2025.9.1", + "pkg:pypi/jsonschema@4.26.0", + "pkg:pypi/mcp@1.28.1", + "pkg:pypi/openai@2.38.0", + "pkg:pypi/openrath@1.3.0", + "pkg:pypi/opentelemetry-api@1.42.1", + "pkg:pypi/opentelemetry-sdk@1.42.1", + "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "pkg:pypi/pip@25.0.1", + "pkg:pypi/psycopg-binary@3.3.4", + "pkg:pypi/psycopg-pool@3.3.1", + "pkg:pypi/psycopg@3.3.4", + "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "pkg:pypi/pycparser@3.0", + "pkg:pypi/pydantic-core@2.46.4", + "pkg:pypi/pydantic-settings@2.14.2", + "pkg:pypi/pydantic@2.13.4", + "pkg:pypi/pyjwt@2.13.0", + "pkg:pypi/python-dateutil@2.9.0.post0", + "pkg:pypi/python-dotenv@1.2.2", + "pkg:pypi/python-multipart@0.0.32", + "pkg:pypi/redis@6.4.0", + "pkg:pypi/referencing@0.37.0", + "pkg:pypi/rpds-py@0.30.0", + "pkg:pypi/s3transfer@0.19.2", + "pkg:pypi/six@1.17.0", + "pkg:pypi/sniffio@1.3.1", + "pkg:pypi/sse-starlette@3.4.4", + "pkg:pypi/starlette@1.3.1", + "pkg:pypi/tqdm@4.67.3", + "pkg:pypi/typing-extensions@4.15.0", + "pkg:pypi/typing-inspection@0.4.2", + "pkg:pypi/urllib3@2.7.0", + "pkg:pypi/uvicorn@0.47.0" + ] + }, + { + "ref": "f68ad4e5-afcc-4b65-9e7f-75fa6ce2c944", + "dependsOn": [ + "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2", + "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9", + "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#e140566d00db0f579c699b6d9e76106beaf31c2b7fb445a03fa070ecbba23222", + "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce", + "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed", + "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72" + ] + }, + { + "ref": "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/apt@3.0.3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/adduser@3.152?arch=all&distro=debian-13.6", + "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/bash@5.2.37-2%2Bb9?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/base-files@13.8%2Bdeb13u6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/bsdutils@2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ca-certificates@20250419?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/coreutils@9.7-3?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/dash@0.5.12-12?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/debian-archive-keyring@2025.1?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/debianutils@5.23.2?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/diffutils@3.10-4?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/dpkg@1.22.22?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/findutils@4.10.0-3?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/grep@3.11-4?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/gzip@1.13-1?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/hostname@3.25?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/init-system-helpers@1.69~deb13u1?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libapt-pkg7.0@3.0.3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libaudit-common@4.0.2-2?arch=all&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libc-bin@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libcap-ng0@0.8.5-4%2Bb1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libdb5.3t64@5.3.28%2Bdfsg2-9?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libdebconfclient0@0.280?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libffi8@3.4.8-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libgdbm6t64@1.24-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/liblastlog2-2@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/liblz4-1@1.10.0-4?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/liblzma5@5.8.1-1%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libmd0@1.1.0-2%2Bb1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libmount1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libblkid1@2.41-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libncursesw6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpam-modules-bin@1.7.0-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6", + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libreadline8t64@8.2-6?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libseccomp2@2.6.0-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpcre2-8-0@10.46-1~deb13u1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libbz2-1.0@1.0.8-6?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsemanage-common@3.8.1-1?arch=all&distro=debian-13.6", + "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsepol2@3.8.1-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsmartcols1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsqlite3-0@3.46.1-7%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libstdc%2B%2B6@14.2.0-19?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/gcc-14-base@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libsystemd0@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libtinfo6@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libudev1@257.13-1~deb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcap2@2.75-10%2Bdeb13u1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libuuid1@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libxxhash0@0.8.3-2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/libzstd1@1.5.7%2Bdfsg-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/login@4.16.0-2%2Breally2.41-5?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpam-runtime@1.7.0-5?arch=all&distro=debian-13.6", + "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/mawk@1.3.4.20250131-1?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/mount@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ncurses-base@6.5%2B20250216-2?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ncurses-bin@6.5%2B20250216-2?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/netbase@6.5?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/openssl-provider-legacy@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/openssl@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libssl3t64@3.5.6-1~deb13u2?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/passwd@4.17.4-2?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/base-passwd@3.6.7?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libacl1@2.3.2-2%2Bb1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libattr1@2.5.2-3?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libaudit1@4.0.2-2%2Bb2?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libbsd0@0.12.2-2?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libcrypt1@4.4.38-1?arch=amd64&distro=debian-13.6&epoch=1", + "pkg:deb/debian/libpam-modules@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libselinux1@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libsemanage2@3.8.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/login.defs@4.17.4-2?arch=all&distro=debian-13.6&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/perl-base@5.40.1-6?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/readline-common@8.2-6?arch=all&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/sed@4.9-2%2Bdeb13u1?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/sqv@1.3.0-3%2Bb2?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgcc-s1@14.2.0-19?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libgmp10@6.3.0%2Bdfsg-3?arch=amd64&distro=debian-13.6&epoch=2", + "pkg:deb/debian/libhogweed6t64@3.10.1-1?arch=amd64&distro=debian-13.6", + "pkg:deb/debian/libnettle8t64@3.10.1-1?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/sysvinit-utils@3.14-4?arch=amd64&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/tar@1.35%2Bdfsg-3.1?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2026b-0%2Bdeb13u1?arch=all&distro=debian-13.6", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.91?arch=all&distro=debian-13.6" + ] + }, + { + "ref": "pkg:deb/debian/util-linux@2.41-5?arch=amd64&distro=debian-13.6", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/zlib1g@1.3.dfsg%2Breally1.3.1-1%2Bb1?arch=amd64&distro=debian-13.6&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.41-12%2Bdeb13u3?arch=amd64&distro=debian-13.6" + ] + }, + { + "ref": "pkg:pypi/annotated-types@0.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/anthropic@0.104.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/anyio@4.13.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/attrs@26.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/boto3@1.43.56", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/botocore@1.43.56", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/certifi@2026.5.20", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/cffi@2.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/click@8.4.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/cryptography@49.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/distro@1.9.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/docstring-parser@0.18.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/h11@0.16.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/httpcore@1.0.9", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/httpx-sse@0.4.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/httpx@0.28.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/idna@3.16", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jiter@0.15.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jmespath@1.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jsonschema-specifications@2025.9.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/jsonschema@4.26.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/mcp@1.28.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/openai@2.38.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/openrath@1.3.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/opentelemetry-api@1.42.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/opentelemetry-sdk@1.42.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pip@25.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg-binary@3.3.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg-pool@3.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg@3.3.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/psycopg_binary@3.3.4?file_name=psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pycparser@3.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pydantic-core@2.46.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pydantic-settings@2.14.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pydantic@2.13.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/pyjwt@2.13.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/python-dateutil@2.9.0.post0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/python-dotenv@1.2.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/python-multipart@0.0.32", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/redis@6.4.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/referencing@0.37.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/rpds-py@0.30.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/s3transfer@0.19.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/six@1.17.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/sse-starlette@3.4.4", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/starlette@1.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/tqdm@4.67.3", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/typing-extensions@4.15.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/typing-inspection@0.4.2", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/urllib3@2.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:pypi/uvicorn@0.47.0", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/cyrus-sasl-lib@2.1.26-24.el7_9#31c73dc5f009ba5a48504f874a39167409302b93174b17cecb1e4e2033f1b9b2", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/keyutils-libs@1.5.8-3.el7#b0804f4bd8708c97010e5324dbe6e1ed8cd5e622524afc3f44b4cf95c9e6cfd9", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/krb5-libs@1.15.1-55.el7_9#e140566d00db0f579c699b6d9e76106beaf31c2b7fb445a03fa070ecbba23222", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/libcom_err@1.42.9-19.el7#acf5d4191003325e79febc61cc2cc17ecbb1c49f03b73edbc4677777f25b75ce", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/libselinux@2.5-15.el7#02193ff4a4eff6fcc27e9c3cf39839797d150f578de0826f36a41de8ede637ed", + "dependsOn": [] + }, + { + "ref": "pkg:rpm/centos/pcre@8.32-17.el7#13c83851f49804fee35d2a5d04c7c9838574be59111e142a6f19d928b13e7f72", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/release/evidence/v2.0.0-review/repository-secret-scan.json b/release/evidence/v2.0.0-review/repository-secret-scan.json new file mode 100644 index 0000000..af2ad48 --- /dev/null +++ b/release/evidence/v2.0.0-review/repository-secret-scan.json @@ -0,0 +1,2733 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2026-07-28T04:19:38.200413949Z", + "ArtifactName": "/workspace", + "ArtifactType": "filesystem", + "Results": [ + { + "Target": "uv.lock", + "Class": "lang-pkgs", + "Type": "uv", + "Packages": [ + { + "ID": "openrath@1.3.0", + "Name": "openrath", + "Identifier": { + "PURL": "pkg:pypi/openrath@1.3.0", + "UID": "21a32c273ff0fe1a" + }, + "Version": "1.3.0", + "Relationship": "root", + "DependsOn": [ + "aiohttp@3.14.3", + "anthropic@0.104.1", + "boto3@1.43.56", + "httpx@0.28.1", + "json-repair@0.61.7", + "litellm@1.85.1", + "mcp@1.28.1", + "openai@2.38.0", + "opensandbox-code-interpreter@0.1.2", + "opensandbox-server@0.2.2", + "opensandbox@0.1.15", + "opentelemetry-api@1.42.1", + "opentelemetry-sdk@1.42.1", + "openviking@0.4.11", + "pillow@12.3.0", + "psycopg@3.3.4", + "pydantic@2.13.4", + "redis@6.4.0", + "soupsieve@2.9.1", + "starlette@1.3.1", + "uvicorn@0.47.0" + ] + }, + { + "ID": "aiohttp@3.14.3", + "Name": "aiohttp", + "Identifier": { + "PURL": "pkg:pypi/aiohttp@3.14.3", + "UID": "4464ad9667e0cd52" + }, + "Version": "3.14.3", + "Relationship": "direct", + "DependsOn": [ + "aiohappyeyeballs@2.6.2", + "aiosignal@1.4.0", + "async-timeout@5.0.1", + "attrs@26.1.0", + "frozenlist@1.8.0", + "multidict@6.7.1", + "propcache@0.5.2", + "typing-extensions@4.15.0", + "yarl@1.24.2" + ] + }, + { + "ID": "anthropic@0.104.1", + "Name": "anthropic", + "Identifier": { + "PURL": "pkg:pypi/anthropic@0.104.1", + "UID": "c5caf0507f8b092" + }, + "Version": "0.104.1", + "Relationship": "direct", + "DependsOn": [ + "anyio@4.13.0", + "distro@1.9.0", + "docstring-parser@0.18.0", + "httpx@0.28.1", + "jiter@0.15.0", + "pydantic@2.13.4", + "sniffio@1.3.1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "boto3@1.43.56", + "Name": "boto3", + "Identifier": { + "PURL": "pkg:pypi/boto3@1.43.56", + "UID": "60cfd1667c43e1ea" + }, + "Version": "1.43.56", + "Relationship": "direct", + "DependsOn": [ + "botocore@1.43.56", + "jmespath@1.1.0", + "s3transfer@0.19.2" + ] + }, + { + "ID": "httpx@0.28.1", + "Name": "httpx", + "Identifier": { + "PURL": "pkg:pypi/httpx@0.28.1", + "UID": "c2cb02c3c7a09654" + }, + "Version": "0.28.1", + "Relationship": "direct", + "DependsOn": [ + "anyio@4.13.0", + "certifi@2026.5.20", + "httpcore@1.0.9", + "idna@3.16", + "socksio@1.0.0" + ] + }, + { + "ID": "json-repair@0.61.7", + "Name": "json-repair", + "Identifier": { + "PURL": "pkg:pypi/json-repair@0.61.7", + "UID": "45c719c89f96c8d6" + }, + "Version": "0.61.7", + "Relationship": "direct" + }, + { + "ID": "litellm@1.85.1", + "Name": "litellm", + "Identifier": { + "PURL": "pkg:pypi/litellm@1.85.1", + "UID": "d3fe251d8a6dd307" + }, + "Version": "1.85.1", + "Relationship": "direct", + "DependsOn": [ + "aiohttp@3.14.3", + "click@8.4.1", + "fastuuid@0.14.0", + "httpx@0.28.1", + "importlib-metadata@8.9.0", + "jinja2@3.1.6", + "jsonschema@4.26.0", + "openai@2.38.0", + "pydantic@2.13.4", + "python-dotenv@1.2.2", + "tiktoken@0.13.0", + "tokenizers@0.23.1" + ] + }, + { + "ID": "mcp@1.28.1", + "Name": "mcp", + "Identifier": { + "PURL": "pkg:pypi/mcp@1.28.1", + "UID": "ffa4e48e15c9ecad" + }, + "Version": "1.28.1", + "Relationship": "direct", + "DependsOn": [ + "anyio@4.13.0", + "httpx-sse@0.4.3", + "httpx@0.28.1", + "jsonschema@4.26.0", + "pydantic-settings@2.14.2", + "pydantic@2.13.4", + "pyjwt@2.13.0", + "python-multipart@0.0.32", + "pywin32@311", + "sse-starlette@3.4.4", + "starlette@1.3.1", + "typing-extensions@4.15.0", + "typing-inspection@0.4.2", + "uvicorn@0.47.0" + ] + }, + { + "ID": "openai@2.38.0", + "Name": "openai", + "Identifier": { + "PURL": "pkg:pypi/openai@2.38.0", + "UID": "a31f95dee98c2929" + }, + "Version": "2.38.0", + "Relationship": "direct", + "DependsOn": [ + "anyio@4.13.0", + "distro@1.9.0", + "httpx@0.28.1", + "jiter@0.15.0", + "pydantic@2.13.4", + "sniffio@1.3.1", + "tqdm@4.67.3", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "opensandbox@0.1.15", + "Name": "opensandbox", + "Identifier": { + "PURL": "pkg:pypi/opensandbox@0.1.15", + "UID": "14d373b6037593d6" + }, + "Version": "0.1.15", + "Relationship": "direct", + "DependsOn": [ + "attrs@26.1.0", + "httpx@0.28.1", + "pydantic@2.13.4", + "python-dateutil@2.9.0.post0" + ] + }, + { + "ID": "opensandbox-code-interpreter@0.1.2", + "Name": "opensandbox-code-interpreter", + "Identifier": { + "PURL": "pkg:pypi/opensandbox-code-interpreter@0.1.2", + "UID": "a384211ffdbd1ed3" + }, + "Version": "0.1.2", + "Relationship": "direct", + "DependsOn": [ + "opensandbox@0.1.15", + "pydantic@2.13.4" + ] + }, + { + "ID": "opensandbox-server@0.2.2", + "Name": "opensandbox-server", + "Identifier": { + "PURL": "pkg:pypi/opensandbox-server@0.2.2", + "UID": "399d8dc3b4929198" + }, + "Version": "0.2.2", + "Relationship": "direct", + "DependsOn": [ + "docker@7.1.0", + "fastapi@0.139.0", + "httpx@0.28.1", + "kubernetes@36.0.0", + "opentelemetry-api@1.42.1", + "opentelemetry-exporter-otlp-proto-http@1.42.1", + "opentelemetry-sdk@1.42.1", + "pydantic-settings@2.14.2", + "pydantic@2.13.4", + "python-multipart@0.0.32", + "pyyaml@6.0.3", + "redis@6.4.0", + "starlette@1.3.1", + "tomli@2.4.1", + "uvicorn@0.47.0", + "websockets@16.0" + ] + }, + { + "ID": "opentelemetry-api@1.42.1", + "Name": "opentelemetry-api", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-api@1.42.1", + "UID": "f7c8ca6452999cc2" + }, + "Version": "1.42.1", + "Relationship": "direct", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "opentelemetry-sdk@1.42.1", + "Name": "opentelemetry-sdk", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-sdk@1.42.1", + "UID": "cbf596f7e9ece60" + }, + "Version": "1.42.1", + "Relationship": "direct", + "DependsOn": [ + "opentelemetry-api@1.42.1", + "opentelemetry-semantic-conventions@0.63b1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "openviking@0.4.11", + "Name": "openviking", + "Identifier": { + "PURL": "pkg:pypi/openviking@0.4.11", + "UID": "fbe7652768e34e80" + }, + "Version": "0.4.11", + "Relationship": "direct", + "DependsOn": [ + "apscheduler@3.11.2", + "argon2-cffi@25.1.0", + "charset-normalizer@3.4.7", + "cryptography@49.0.0", + "defusedxml@0.7.1", + "ebooklib@0.20", + "fastapi@0.139.0", + "feedparser@6.0.12", + "httpx@0.28.1", + "jinja2@3.1.6", + "json-repair@0.61.7", + "lark-oapi@1.5.5", + "litellm@1.85.1", + "loguru@0.7.3", + "mcp@1.28.1", + "olefile@0.47", + "openai@2.38.0", + "openpyxl@3.1.5", + "opentelemetry-api@1.42.1", + "opentelemetry-exporter-otlp-proto-grpc@1.42.1", + "opentelemetry-exporter-otlp-proto-http@1.42.1", + "opentelemetry-instrumentation-asyncio@0.63b1", + "opentelemetry-sdk@1.42.1", + "openviking-sdk@0.1.5", + "pathspec@1.1.1", + "pdfminer-six@20251230", + "pdfplumber@0.11.9", + "protobuf@6.33.6", + "pydantic@2.13.4", + "python-docx@1.2.0", + "python-multipart@0.0.32", + "python-pptx@1.0.2", + "pyyaml@6.0.3", + "requests@2.34.2", + "scrapy@2.17.0", + "tabulate@0.10.0", + "trafilatura@2.1.0", + "tree-sitter-c-sharp@0.23.5", + "tree-sitter-cpp@0.23.4", + "tree-sitter-go@0.25.0", + "tree-sitter-java@0.23.5", + "tree-sitter-javascript@0.25.0", + "tree-sitter-lua@0.5.0", + "tree-sitter-php@0.24.1", + "tree-sitter-python@0.25.0", + "tree-sitter-rust@0.24.2", + "tree-sitter-typescript@0.23.2", + "tree-sitter@0.25.2", + "typer@0.25.1", + "typing-extensions@4.15.0", + "urllib3@2.7.0", + "uvicorn@0.47.0", + "volcengine-python-sdk@5.0.28", + "volcengine@1.0.222", + "xlrd@2.0.2", + "xxhash@3.7.0" + ] + }, + { + "ID": "pillow@12.3.0", + "Name": "pillow", + "Identifier": { + "PURL": "pkg:pypi/pillow@12.3.0", + "UID": "ca54c7626e3d7537" + }, + "Version": "12.3.0", + "Relationship": "direct" + }, + { + "ID": "psycopg@3.3.4", + "Name": "psycopg", + "Identifier": { + "PURL": "pkg:pypi/psycopg@3.3.4", + "UID": "a7fcc28e4c71e88b" + }, + "Version": "3.3.4", + "Relationship": "direct", + "DependsOn": [ + "psycopg-binary@3.3.4", + "psycopg-pool@3.3.1", + "typing-extensions@4.15.0", + "tzdata@2026.2" + ] + }, + { + "ID": "pydantic@2.13.4", + "Name": "pydantic", + "Identifier": { + "PURL": "pkg:pypi/pydantic@2.13.4", + "UID": "1143b24984ee2cd6" + }, + "Version": "2.13.4", + "Relationship": "direct", + "DependsOn": [ + "annotated-types@0.7.0", + "pydantic-core@2.46.4", + "typing-extensions@4.15.0", + "typing-inspection@0.4.2" + ] + }, + { + "ID": "redis@6.4.0", + "Name": "redis", + "Identifier": { + "PURL": "pkg:pypi/redis@6.4.0", + "UID": "11fbd3b587a47e23" + }, + "Version": "6.4.0", + "Relationship": "direct", + "DependsOn": [ + "async-timeout@5.0.1" + ] + }, + { + "ID": "soupsieve@2.9.1", + "Name": "soupsieve", + "Identifier": { + "PURL": "pkg:pypi/soupsieve@2.9.1", + "UID": "eb32fb90af49d2f3" + }, + "Version": "2.9.1", + "Relationship": "direct" + }, + { + "ID": "starlette@1.3.1", + "Name": "starlette", + "Identifier": { + "PURL": "pkg:pypi/starlette@1.3.1", + "UID": "24c1e0349df55f94" + }, + "Version": "1.3.1", + "Relationship": "direct", + "DependsOn": [ + "anyio@4.13.0", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "uvicorn@0.47.0", + "Name": "uvicorn", + "Identifier": { + "PURL": "pkg:pypi/uvicorn@0.47.0", + "UID": "17cbb6ef525da623" + }, + "Version": "0.47.0", + "Relationship": "direct", + "DependsOn": [ + "click@8.4.1", + "colorama@0.4.6", + "h11@0.16.0", + "httptools@0.7.1", + "python-dotenv@1.2.2", + "pyyaml@6.0.3", + "typing-extensions@4.15.0", + "uvloop@0.22.1", + "watchfiles@1.2.0", + "websockets@16.0" + ] + }, + { + "ID": "aiohappyeyeballs@2.6.2", + "Name": "aiohappyeyeballs", + "Identifier": { + "PURL": "pkg:pypi/aiohappyeyeballs@2.6.2", + "UID": "3138cd82fb6bf479" + }, + "Version": "2.6.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "aiosignal@1.4.0", + "Name": "aiosignal", + "Identifier": { + "PURL": "pkg:pypi/aiosignal@1.4.0", + "UID": "f44b45e60a76e658" + }, + "Version": "1.4.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "frozenlist@1.8.0", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "annotated-doc@0.0.4", + "Name": "annotated-doc", + "Identifier": { + "PURL": "pkg:pypi/annotated-doc@0.0.4", + "UID": "b22c8e29cb6dcb45" + }, + "Version": "0.0.4", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "annotated-types@0.7.0", + "Name": "annotated-types", + "Identifier": { + "PURL": "pkg:pypi/annotated-types@0.7.0", + "UID": "11cf816d4baeb496" + }, + "Version": "0.7.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "anyio@4.13.0", + "Name": "anyio", + "Identifier": { + "PURL": "pkg:pypi/anyio@4.13.0", + "UID": "51357db313006c72" + }, + "Version": "4.13.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "exceptiongroup@1.3.1", + "idna@3.16", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "apscheduler@3.11.2", + "Name": "apscheduler", + "Identifier": { + "PURL": "pkg:pypi/apscheduler@3.11.2", + "UID": "874346a401c38a88" + }, + "Version": "3.11.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "tzlocal@5.3.1" + ] + }, + { + "ID": "argon2-cffi@25.1.0", + "Name": "argon2-cffi", + "Identifier": { + "PURL": "pkg:pypi/argon2-cffi@25.1.0", + "UID": "b19c636eb5c69eb7" + }, + "Version": "25.1.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "argon2-cffi-bindings@25.1.0" + ] + }, + { + "ID": "argon2-cffi-bindings@25.1.0", + "Name": "argon2-cffi-bindings", + "Identifier": { + "PURL": "pkg:pypi/argon2-cffi-bindings@25.1.0", + "UID": "6fd05fced56d1f87" + }, + "Version": "25.1.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "cffi@2.0.0" + ] + }, + { + "ID": "async-timeout@5.0.1", + "Name": "async-timeout", + "Identifier": { + "PURL": "pkg:pypi/async-timeout@5.0.1", + "UID": "d96510162b86cfa" + }, + "Version": "5.0.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "attrs@26.1.0", + "Name": "attrs", + "Identifier": { + "PURL": "pkg:pypi/attrs@26.1.0", + "UID": "e79bb2f7c1ab75e6" + }, + "Version": "26.1.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "automat@25.4.16", + "Name": "automat", + "Identifier": { + "PURL": "pkg:pypi/automat@25.4.16", + "UID": "f7a0cdce26c33e15" + }, + "Version": "25.4.16", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "babel@2.18.0", + "Name": "babel", + "Identifier": { + "PURL": "pkg:pypi/babel@2.18.0", + "UID": "63b032d4c841398" + }, + "Version": "2.18.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "beautifulsoup4@4.14.3", + "Name": "beautifulsoup4", + "Identifier": { + "PURL": "pkg:pypi/beautifulsoup4@4.14.3", + "UID": "c258519877024c2b" + }, + "Version": "4.14.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "soupsieve@2.9.1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "botocore@1.43.56", + "Name": "botocore", + "Identifier": { + "PURL": "pkg:pypi/botocore@1.43.56", + "UID": "2ebc9c1eb8303c4" + }, + "Version": "1.43.56", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "jmespath@1.1.0", + "python-dateutil@2.9.0.post0", + "urllib3@2.7.0" + ] + }, + { + "ID": "certifi@2026.5.20", + "Name": "certifi", + "Identifier": { + "PURL": "pkg:pypi/certifi@2026.5.20", + "UID": "a1f3abeb0afcd7a0" + }, + "Version": "2026.5.20", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "cffi@2.0.0", + "Name": "cffi", + "Identifier": { + "PURL": "pkg:pypi/cffi@2.0.0", + "UID": "17b62359c3dde9fe" + }, + "Version": "2.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "pycparser@3.0" + ] + }, + { + "ID": "charset-normalizer@3.4.7", + "Name": "charset-normalizer", + "Identifier": { + "PURL": "pkg:pypi/charset-normalizer@3.4.7", + "UID": "86b1f758bd996aaa" + }, + "Version": "3.4.7", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "click@8.4.1", + "Name": "click", + "Identifier": { + "PURL": "pkg:pypi/click@8.4.1", + "UID": "1bfc14795c497ded" + }, + "Version": "8.4.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "colorama@0.4.6" + ] + }, + { + "ID": "colorama@0.4.6", + "Name": "colorama", + "Identifier": { + "PURL": "pkg:pypi/colorama@0.4.6", + "UID": "49acc401742db23d" + }, + "Version": "0.4.6", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "constantly@23.10.4", + "Name": "constantly", + "Identifier": { + "PURL": "pkg:pypi/constantly@23.10.4", + "UID": "646532fbe0743280" + }, + "Version": "23.10.4", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "courlan@1.4.0", + "Name": "courlan", + "Identifier": { + "PURL": "pkg:pypi/courlan@1.4.0", + "UID": "f529508ae553eb4" + }, + "Version": "1.4.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "babel@2.18.0", + "tld@0.13.2", + "urllib3@2.7.0" + ] + }, + { + "ID": "cryptography@49.0.0", + "Name": "cryptography", + "Identifier": { + "PURL": "pkg:pypi/cryptography@49.0.0", + "UID": "3155e393a29b8cf" + }, + "Version": "49.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "cffi@2.0.0", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "cssselect@1.5.0", + "Name": "cssselect", + "Identifier": { + "PURL": "pkg:pypi/cssselect@1.5.0", + "UID": "ee20042c34045be4" + }, + "Version": "1.5.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "dateparser@1.4.1", + "Name": "dateparser", + "Identifier": { + "PURL": "pkg:pypi/dateparser@1.4.1", + "UID": "6323da64da6c74ab" + }, + "Version": "1.4.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "python-dateutil@2.9.0.post0", + "pytz@2026.2", + "regex@2026.5.9", + "tzlocal@5.3.1" + ] + }, + { + "ID": "decorator@5.3.1", + "Name": "decorator", + "Identifier": { + "PURL": "pkg:pypi/decorator@5.3.1", + "UID": "b4b00eb3b1242618" + }, + "Version": "5.3.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "defusedxml@0.7.1", + "Name": "defusedxml", + "Identifier": { + "PURL": "pkg:pypi/defusedxml@0.7.1", + "UID": "7ee6826d9010842f" + }, + "Version": "0.7.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "distro@1.9.0", + "Name": "distro", + "Identifier": { + "PURL": "pkg:pypi/distro@1.9.0", + "UID": "8e2597fd6ac81338" + }, + "Version": "1.9.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "docker@7.1.0", + "Name": "docker", + "Identifier": { + "PURL": "pkg:pypi/docker@7.1.0", + "UID": "2bd3c21667ff80f0" + }, + "Version": "7.1.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "pywin32@311", + "requests@2.34.2", + "urllib3@2.7.0" + ] + }, + { + "ID": "docstring-parser@0.18.0", + "Name": "docstring-parser", + "Identifier": { + "PURL": "pkg:pypi/docstring-parser@0.18.0", + "UID": "e88afd9925d31571" + }, + "Version": "0.18.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "durationpy@0.10", + "Name": "durationpy", + "Identifier": { + "PURL": "pkg:pypi/durationpy@0.10", + "UID": "96b0639e4e6a2fb5" + }, + "Version": "0.10", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "ebooklib@0.20", + "Name": "ebooklib", + "Identifier": { + "PURL": "pkg:pypi/ebooklib@0.20", + "UID": "4f383fac356d0fb1" + }, + "Version": "0.20", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "lxml@6.1.1", + "six@1.17.0" + ] + }, + { + "ID": "et-xmlfile@2.0.0", + "Name": "et-xmlfile", + "Identifier": { + "PURL": "pkg:pypi/et-xmlfile@2.0.0", + "UID": "bba70b2b7d16f35" + }, + "Version": "2.0.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "exceptiongroup@1.3.1", + "Name": "exceptiongroup", + "Identifier": { + "PURL": "pkg:pypi/exceptiongroup@1.3.1", + "UID": "409f1274ee18054c" + }, + "Version": "1.3.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "fastapi@0.139.0", + "Name": "fastapi", + "Identifier": { + "PURL": "pkg:pypi/fastapi@0.139.0", + "UID": "6caf70d612778d61" + }, + "Version": "0.139.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "annotated-doc@0.0.4", + "pydantic@2.13.4", + "starlette@1.3.1", + "typing-extensions@4.15.0", + "typing-inspection@0.4.2" + ] + }, + { + "ID": "fastuuid@0.14.0", + "Name": "fastuuid", + "Identifier": { + "PURL": "pkg:pypi/fastuuid@0.14.0", + "UID": "eeeb8ee5daaa04fa" + }, + "Version": "0.14.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "feedparser@6.0.12", + "Name": "feedparser", + "Identifier": { + "PURL": "pkg:pypi/feedparser@6.0.12", + "UID": "158e6c8d5cbd238f" + }, + "Version": "6.0.12", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "sgmllib3k@1.0.0" + ] + }, + { + "ID": "filelock@3.29.0", + "Name": "filelock", + "Identifier": { + "PURL": "pkg:pypi/filelock@3.29.0", + "UID": "7828d67563657a86" + }, + "Version": "3.29.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "frozenlist@1.8.0", + "Name": "frozenlist", + "Identifier": { + "PURL": "pkg:pypi/frozenlist@1.8.0", + "UID": "ceb29e6f4c30b4f4" + }, + "Version": "1.8.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "fsspec@2026.4.0", + "Name": "fsspec", + "Identifier": { + "PURL": "pkg:pypi/fsspec@2026.4.0", + "UID": "3ff12fc77e668ef6" + }, + "Version": "2026.4.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "google@3.0.0", + "Name": "google", + "Identifier": { + "PURL": "pkg:pypi/google@3.0.0", + "UID": "796c803dde2dd11a" + }, + "Version": "3.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "beautifulsoup4@4.14.3" + ] + }, + { + "ID": "googleapis-common-protos@1.75.0", + "Name": "googleapis-common-protos", + "Identifier": { + "PURL": "pkg:pypi/googleapis-common-protos@1.75.0", + "UID": "7e29147907b8f5b0" + }, + "Version": "1.75.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "protobuf@6.33.6" + ] + }, + { + "ID": "grpcio@1.80.0", + "Name": "grpcio", + "Identifier": { + "PURL": "pkg:pypi/grpcio@1.80.0", + "UID": "eea11b30053eb26" + }, + "Version": "1.80.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "h11@0.16.0", + "Name": "h11", + "Identifier": { + "PURL": "pkg:pypi/h11@0.16.0", + "UID": "f98d44af252a461e" + }, + "Version": "0.16.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "hf-xet@1.5.0", + "Name": "hf-xet", + "Identifier": { + "PURL": "pkg:pypi/hf-xet@1.5.0", + "UID": "c5bbf1acd9dd4de8" + }, + "Version": "1.5.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "htmldate@1.10.0", + "Name": "htmldate", + "Identifier": { + "PURL": "pkg:pypi/htmldate@1.10.0", + "UID": "c7a7d9da06bbfe15" + }, + "Version": "1.10.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "charset-normalizer@3.4.7", + "dateparser@1.4.1", + "lxml@6.1.1", + "python-dateutil@2.9.0.post0", + "urllib3@2.7.0" + ] + }, + { + "ID": "httpcore@1.0.9", + "Name": "httpcore", + "Identifier": { + "PURL": "pkg:pypi/httpcore@1.0.9", + "UID": "8a4f5f5c03660848" + }, + "Version": "1.0.9", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "certifi@2026.5.20", + "h11@0.16.0" + ] + }, + { + "ID": "httptools@0.7.1", + "Name": "httptools", + "Identifier": { + "PURL": "pkg:pypi/httptools@0.7.1", + "UID": "935b13449a70e6a8" + }, + "Version": "0.7.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "httpx-sse@0.4.3", + "Name": "httpx-sse", + "Identifier": { + "PURL": "pkg:pypi/httpx-sse@0.4.3", + "UID": "87c1f59b65c4e08e" + }, + "Version": "0.4.3", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "huggingface-hub@1.16.1", + "Name": "huggingface-hub", + "Identifier": { + "PURL": "pkg:pypi/huggingface-hub@1.16.1", + "UID": "d187308b5c4de4c1" + }, + "Version": "1.16.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "filelock@3.29.0", + "fsspec@2026.4.0", + "hf-xet@1.5.0", + "httpx@0.28.1", + "packaging@26.2", + "pyyaml@6.0.3", + "tqdm@4.67.3", + "typer@0.25.1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "hyperlink@21.0.0", + "Name": "hyperlink", + "Identifier": { + "PURL": "pkg:pypi/hyperlink@21.0.0", + "UID": "9a8716f7de74750b" + }, + "Version": "21.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "idna@3.16" + ] + }, + { + "ID": "idna@3.16", + "Name": "idna", + "Identifier": { + "PURL": "pkg:pypi/idna@3.16", + "UID": "5bca8aece3b138ad" + }, + "Version": "3.16", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "importlib-metadata@8.9.0", + "Name": "importlib-metadata", + "Identifier": { + "PURL": "pkg:pypi/importlib-metadata@8.9.0", + "UID": "3596d46d555ede11" + }, + "Version": "8.9.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "zipp@4.1.0" + ] + }, + { + "ID": "incremental@24.11.0", + "Name": "incremental", + "Identifier": { + "PURL": "pkg:pypi/incremental@24.11.0", + "UID": "81ec074f91c1b630" + }, + "Version": "24.11.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "packaging@26.2", + "tomli@2.4.1" + ] + }, + { + "ID": "itemadapter@0.13.1", + "Name": "itemadapter", + "Identifier": { + "PURL": "pkg:pypi/itemadapter@0.13.1", + "UID": "48b35965ca4ba913" + }, + "Version": "0.13.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "itemloaders@1.4.0", + "Name": "itemloaders", + "Identifier": { + "PURL": "pkg:pypi/itemloaders@1.4.0", + "UID": "bb3d98f1a645722a" + }, + "Version": "1.4.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "itemadapter@0.13.1", + "jmespath@1.1.0", + "parsel@1.11.0" + ] + }, + { + "ID": "jinja2@3.1.6", + "Name": "jinja2", + "Identifier": { + "PURL": "pkg:pypi/jinja2@3.1.6", + "UID": "7665ffa0d224a020" + }, + "Version": "3.1.6", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "markupsafe@3.0.3" + ] + }, + { + "ID": "jiter@0.15.0", + "Name": "jiter", + "Identifier": { + "PURL": "pkg:pypi/jiter@0.15.0", + "UID": "8b9915803c7cc55a" + }, + "Version": "0.15.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "jmespath@1.1.0", + "Name": "jmespath", + "Identifier": { + "PURL": "pkg:pypi/jmespath@1.1.0", + "UID": "a7947903a898d0ac" + }, + "Version": "1.1.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "jsonschema@4.26.0", + "Name": "jsonschema", + "Identifier": { + "PURL": "pkg:pypi/jsonschema@4.26.0", + "UID": "6cde932ef1ea2a36" + }, + "Version": "4.26.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "attrs@26.1.0", + "jsonschema-specifications@2025.9.1", + "referencing@0.37.0", + "rpds-py@0.30.0" + ] + }, + { + "ID": "jsonschema-specifications@2025.9.1", + "Name": "jsonschema-specifications", + "Identifier": { + "PURL": "pkg:pypi/jsonschema-specifications@2025.9.1", + "UID": "867f65dbe20d7caf" + }, + "Version": "2025.9.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "referencing@0.37.0" + ] + }, + { + "ID": "justext@3.0.2", + "Name": "justext", + "Identifier": { + "PURL": "pkg:pypi/justext@3.0.2", + "UID": "79912f2980a181fc" + }, + "Version": "3.0.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "lxml@6.1.1" + ] + }, + { + "ID": "kubernetes@36.0.0", + "Name": "kubernetes", + "Identifier": { + "PURL": "pkg:pypi/kubernetes@36.0.0", + "UID": "223729affe6a5cfc" + }, + "Version": "36.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "aiohttp@3.14.3", + "certifi@2026.5.20", + "durationpy@0.10", + "python-dateutil@2.9.0.post0", + "pyyaml@6.0.3", + "requests-oauthlib@2.0.0", + "requests@2.34.2", + "six@1.17.0", + "urllib3@2.7.0", + "websocket-client@1.9.0" + ] + }, + { + "ID": "lark-oapi@1.5.5", + "Name": "lark-oapi", + "Identifier": { + "PURL": "pkg:pypi/lark-oapi@1.5.5", + "UID": "4913f86a0bc67ce4" + }, + "Version": "1.5.5", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "httpx@0.28.1", + "pycryptodome@3.23.0", + "requests-toolbelt@1.0.0", + "requests@2.34.2", + "websockets@16.0" + ] + }, + { + "ID": "loguru@0.7.3", + "Name": "loguru", + "Identifier": { + "PURL": "pkg:pypi/loguru@0.7.3", + "UID": "1a048f1512302e8b" + }, + "Version": "0.7.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "colorama@0.4.6", + "win32-setctime@1.2.0" + ] + }, + { + "ID": "lxml@6.1.1", + "Name": "lxml", + "Identifier": { + "PURL": "pkg:pypi/lxml@6.1.1", + "UID": "ed7e75bfa63b559b" + }, + "Version": "6.1.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "lxml-html-clean@0.4.5" + ] + }, + { + "ID": "lxml-html-clean@0.4.5", + "Name": "lxml-html-clean", + "Identifier": { + "PURL": "pkg:pypi/lxml-html-clean@0.4.5", + "UID": "e298af70adf97e61" + }, + "Version": "0.4.5", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "lxml@6.1.1" + ] + }, + { + "ID": "markdown-it-py@3.0.0", + "Name": "markdown-it-py", + "Identifier": { + "PURL": "pkg:pypi/markdown-it-py@3.0.0", + "UID": "215996bc09c94a4f" + }, + "Version": "3.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "mdurl@0.1.2" + ] + }, + { + "ID": "markdown-it-py@4.2.0", + "Name": "markdown-it-py", + "Identifier": { + "PURL": "pkg:pypi/markdown-it-py@4.2.0", + "UID": "aac12c738988211a" + }, + "Version": "4.2.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "mdurl@0.1.2" + ] + }, + { + "ID": "markupsafe@3.0.3", + "Name": "markupsafe", + "Identifier": { + "PURL": "pkg:pypi/markupsafe@3.0.3", + "UID": "6e3d7916e912131a" + }, + "Version": "3.0.3", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "mdurl@0.1.2", + "Name": "mdurl", + "Identifier": { + "PURL": "pkg:pypi/mdurl@0.1.2", + "UID": "d262cfb9f949997f" + }, + "Version": "0.1.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "multidict@6.7.1", + "Name": "multidict", + "Identifier": { + "PURL": "pkg:pypi/multidict@6.7.1", + "UID": "529d708d203464c" + }, + "Version": "6.7.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "oauthlib@3.3.1", + "Name": "oauthlib", + "Identifier": { + "PURL": "pkg:pypi/oauthlib@3.3.1", + "UID": "8a4916a10042a47f" + }, + "Version": "3.3.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "olefile@0.47", + "Name": "olefile", + "Identifier": { + "PURL": "pkg:pypi/olefile@0.47", + "UID": "70a33159957baaf9" + }, + "Version": "0.47", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "openpyxl@3.1.5", + "Name": "openpyxl", + "Identifier": { + "PURL": "pkg:pypi/openpyxl@3.1.5", + "UID": "7b09753dc900e457" + }, + "Version": "3.1.5", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "et-xmlfile@2.0.0" + ] + }, + { + "ID": "opentelemetry-exporter-otlp-proto-common@1.42.1", + "Name": "opentelemetry-exporter-otlp-proto-common", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-exporter-otlp-proto-common@1.42.1", + "UID": "87322e48d562e1" + }, + "Version": "1.42.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "opentelemetry-proto@1.42.1" + ] + }, + { + "ID": "opentelemetry-exporter-otlp-proto-grpc@1.42.1", + "Name": "opentelemetry-exporter-otlp-proto-grpc", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-exporter-otlp-proto-grpc@1.42.1", + "UID": "5d1152f1911a8444" + }, + "Version": "1.42.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "googleapis-common-protos@1.75.0", + "grpcio@1.80.0", + "opentelemetry-api@1.42.1", + "opentelemetry-exporter-otlp-proto-common@1.42.1", + "opentelemetry-proto@1.42.1", + "opentelemetry-sdk@1.42.1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "opentelemetry-exporter-otlp-proto-http@1.42.1", + "Name": "opentelemetry-exporter-otlp-proto-http", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-exporter-otlp-proto-http@1.42.1", + "UID": "75787693aca0dae6" + }, + "Version": "1.42.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "googleapis-common-protos@1.75.0", + "opentelemetry-api@1.42.1", + "opentelemetry-exporter-otlp-proto-common@1.42.1", + "opentelemetry-proto@1.42.1", + "opentelemetry-sdk@1.42.1", + "requests@2.34.2", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "opentelemetry-instrumentation@0.63b1", + "Name": "opentelemetry-instrumentation", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-instrumentation@0.63b1", + "UID": "4fa0f62795b46fbc" + }, + "Version": "0.63b1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "opentelemetry-api@1.42.1", + "opentelemetry-semantic-conventions@0.63b1", + "packaging@26.2", + "wrapt@2.2.1" + ] + }, + { + "ID": "opentelemetry-instrumentation-asyncio@0.63b1", + "Name": "opentelemetry-instrumentation-asyncio", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-instrumentation-asyncio@0.63b1", + "UID": "6b2615e3539d8be1" + }, + "Version": "0.63b1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "opentelemetry-api@1.42.1", + "opentelemetry-instrumentation@0.63b1", + "opentelemetry-semantic-conventions@0.63b1", + "wrapt@2.2.1" + ] + }, + { + "ID": "opentelemetry-proto@1.42.1", + "Name": "opentelemetry-proto", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-proto@1.42.1", + "UID": "90d02bc53514c0a6" + }, + "Version": "1.42.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "protobuf@6.33.6" + ] + }, + { + "ID": "opentelemetry-semantic-conventions@0.63b1", + "Name": "opentelemetry-semantic-conventions", + "Identifier": { + "PURL": "pkg:pypi/opentelemetry-semantic-conventions@0.63b1", + "UID": "361682bb3ff3a740" + }, + "Version": "0.63b1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "opentelemetry-api@1.42.1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "openviking-sdk@0.1.5", + "Name": "openviking-sdk", + "Identifier": { + "PURL": "pkg:pypi/openviking-sdk@0.1.5", + "UID": "5fbf4f602388cac8" + }, + "Version": "0.1.5", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "httpx@0.28.1" + ] + }, + { + "ID": "packaging@26.2", + "Name": "packaging", + "Identifier": { + "PURL": "pkg:pypi/packaging@26.2", + "UID": "78ec05430e68bf11" + }, + "Version": "26.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "parsel@1.11.0", + "Name": "parsel", + "Identifier": { + "PURL": "pkg:pypi/parsel@1.11.0", + "UID": "b5b1b1edbf84974c" + }, + "Version": "1.11.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "cssselect@1.5.0", + "jmespath@1.1.0", + "lxml@6.1.1", + "packaging@26.2", + "w3lib@2.4.1" + ] + }, + { + "ID": "pathspec@1.1.1", + "Name": "pathspec", + "Identifier": { + "PURL": "pkg:pypi/pathspec@1.1.1", + "UID": "f417e9bd635ac2ff" + }, + "Version": "1.1.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pdfminer-six@20251230", + "Name": "pdfminer-six", + "Identifier": { + "PURL": "pkg:pypi/pdfminer-six@20251230", + "UID": "7c6ab0beb183d479" + }, + "Version": "20251230", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "charset-normalizer@3.4.7", + "cryptography@49.0.0" + ] + }, + { + "ID": "pdfplumber@0.11.9", + "Name": "pdfplumber", + "Identifier": { + "PURL": "pkg:pypi/pdfplumber@0.11.9", + "UID": "36bd3356488181b0" + }, + "Version": "0.11.9", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "pdfminer-six@20251230", + "pillow@12.3.0", + "pypdfium2@5.8.0" + ] + }, + { + "ID": "propcache@0.5.2", + "Name": "propcache", + "Identifier": { + "PURL": "pkg:pypi/propcache@0.5.2", + "UID": "f9ffe97c9009ad1c" + }, + "Version": "0.5.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "protego@0.6.2", + "Name": "protego", + "Identifier": { + "PURL": "pkg:pypi/protego@0.6.2", + "UID": "a42402938b1817fe" + }, + "Version": "0.6.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "protobuf@6.33.6", + "Name": "protobuf", + "Identifier": { + "PURL": "pkg:pypi/protobuf@6.33.6", + "UID": "3dc30339a9cf63dd" + }, + "Version": "6.33.6", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "psycopg-binary@3.3.4", + "Name": "psycopg-binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg-binary@3.3.4", + "UID": "57eaf6cb0321067a" + }, + "Version": "3.3.4", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "psycopg-pool@3.3.1", + "Name": "psycopg-pool", + "Identifier": { + "PURL": "pkg:pypi/psycopg-pool@3.3.1", + "UID": "72a99c34cd88e626" + }, + "Version": "3.3.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "py@1.11.0", + "Name": "py", + "Identifier": { + "PURL": "pkg:pypi/py@1.11.0", + "UID": "54a331165a4640d2" + }, + "Version": "1.11.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pycparser@3.0", + "Name": "pycparser", + "Identifier": { + "PURL": "pkg:pypi/pycparser@3.0", + "UID": "a030070ae7a187b2" + }, + "Version": "3.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pycryptodome@3.23.0", + "Name": "pycryptodome", + "Identifier": { + "PURL": "pkg:pypi/pycryptodome@3.23.0", + "UID": "9d1c5c172635c793" + }, + "Version": "3.23.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pydantic-core@2.46.4", + "Name": "pydantic-core", + "Identifier": { + "PURL": "pkg:pypi/pydantic-core@2.46.4", + "UID": "979f2e0bdfa56772" + }, + "Version": "2.46.4", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "pydantic-settings@2.14.2", + "Name": "pydantic-settings", + "Identifier": { + "PURL": "pkg:pypi/pydantic-settings@2.14.2", + "UID": "9c593c1e826a3acc" + }, + "Version": "2.14.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "pydantic@2.13.4", + "python-dotenv@1.2.2", + "typing-inspection@0.4.2" + ] + }, + { + "ID": "pydispatcher@2.0.7", + "Name": "pydispatcher", + "Identifier": { + "PURL": "pkg:pypi/pydispatcher@2.0.7", + "UID": "a84eb20b6c22d2dd" + }, + "Version": "2.0.7", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pygments@2.20.0", + "Name": "pygments", + "Identifier": { + "PURL": "pkg:pypi/pygments@2.20.0", + "UID": "412fcfe0e35c3c57" + }, + "Version": "2.20.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pyjwt@2.13.0", + "Name": "pyjwt", + "Identifier": { + "PURL": "pkg:pypi/pyjwt@2.13.0", + "UID": "3171e9daeb0b9d05" + }, + "Version": "2.13.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "cryptography@49.0.0", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "pyopenssl@26.3.0", + "Name": "pyopenssl", + "Identifier": { + "PURL": "pkg:pypi/pyopenssl@26.3.0", + "UID": "c2dbce487c92169" + }, + "Version": "26.3.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "cryptography@49.0.0", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "pypdfium2@5.8.0", + "Name": "pypdfium2", + "Identifier": { + "PURL": "pkg:pypi/pypdfium2@5.8.0", + "UID": "cb12848faa434a66" + }, + "Version": "5.8.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pypydispatcher@2.1.2", + "Name": "pypydispatcher", + "Identifier": { + "PURL": "pkg:pypi/pypydispatcher@2.1.2", + "UID": "65bcfe1679f61073" + }, + "Version": "2.1.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "python-dateutil@2.9.0.post0", + "Name": "python-dateutil", + "Identifier": { + "PURL": "pkg:pypi/python-dateutil@2.9.0.post0", + "UID": "f076f675da73225b" + }, + "Version": "2.9.0.post0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "six@1.17.0" + ] + }, + { + "ID": "python-docx@1.2.0", + "Name": "python-docx", + "Identifier": { + "PURL": "pkg:pypi/python-docx@1.2.0", + "UID": "e78a7343a4694472" + }, + "Version": "1.2.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "lxml@6.1.1", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "python-dotenv@1.2.2", + "Name": "python-dotenv", + "Identifier": { + "PURL": "pkg:pypi/python-dotenv@1.2.2", + "UID": "14d8dd609e7b32bb" + }, + "Version": "1.2.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "python-multipart@0.0.32", + "Name": "python-multipart", + "Identifier": { + "PURL": "pkg:pypi/python-multipart@0.0.32", + "UID": "eadf759a4737b46b" + }, + "Version": "0.0.32", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "python-pptx@1.0.2", + "Name": "python-pptx", + "Identifier": { + "PURL": "pkg:pypi/python-pptx@1.0.2", + "UID": "769c0b09031f42bb" + }, + "Version": "1.0.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "lxml@6.1.1", + "pillow@12.3.0", + "typing-extensions@4.15.0", + "xlsxwriter@3.2.9" + ] + }, + { + "ID": "pytz@2026.2", + "Name": "pytz", + "Identifier": { + "PURL": "pkg:pypi/pytz@2026.2", + "UID": "433c0d251b5b021c" + }, + "Version": "2026.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pywin32@311", + "Name": "pywin32", + "Identifier": { + "PURL": "pkg:pypi/pywin32@311", + "UID": "b301115ac354b51d" + }, + "Version": "311", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "pyyaml@6.0.3", + "Name": "pyyaml", + "Identifier": { + "PURL": "pkg:pypi/pyyaml@6.0.3", + "UID": "107b31a534619272" + }, + "Version": "6.0.3", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "queuelib@1.9.0", + "Name": "queuelib", + "Identifier": { + "PURL": "pkg:pypi/queuelib@1.9.0", + "UID": "5c8692655e664f79" + }, + "Version": "1.9.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "referencing@0.37.0", + "Name": "referencing", + "Identifier": { + "PURL": "pkg:pypi/referencing@0.37.0", + "UID": "df12688160a5997" + }, + "Version": "0.37.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "attrs@26.1.0", + "rpds-py@0.30.0", + "typing-extensions@4.15.0" + ] + }, + { + "ID": "regex@2026.5.9", + "Name": "regex", + "Identifier": { + "PURL": "pkg:pypi/regex@2026.5.9", + "UID": "668888c9689a83e6" + }, + "Version": "2026.5.9", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "requests@2.34.2", + "Name": "requests", + "Identifier": { + "PURL": "pkg:pypi/requests@2.34.2", + "UID": "ea0a1eef368ee39b" + }, + "Version": "2.34.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "certifi@2026.5.20", + "charset-normalizer@3.4.7", + "idna@3.16", + "urllib3@2.7.0" + ] + }, + { + "ID": "requests-file@3.0.1", + "Name": "requests-file", + "Identifier": { + "PURL": "pkg:pypi/requests-file@3.0.1", + "UID": "3475fa97fb85873d" + }, + "Version": "3.0.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "requests@2.34.2" + ] + }, + { + "ID": "requests-oauthlib@2.0.0", + "Name": "requests-oauthlib", + "Identifier": { + "PURL": "pkg:pypi/requests-oauthlib@2.0.0", + "UID": "b26b8619f6db397b" + }, + "Version": "2.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "oauthlib@3.3.1", + "requests@2.34.2" + ] + }, + { + "ID": "requests-toolbelt@1.0.0", + "Name": "requests-toolbelt", + "Identifier": { + "PURL": "pkg:pypi/requests-toolbelt@1.0.0", + "UID": "d73d767e21e45a30" + }, + "Version": "1.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "requests@2.34.2" + ] + }, + { + "ID": "retry@0.9.2", + "Name": "retry", + "Identifier": { + "PURL": "pkg:pypi/retry@0.9.2", + "UID": "5547c39c7c3cf0d9" + }, + "Version": "0.9.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "decorator@5.3.1", + "py@1.11.0" + ] + }, + { + "ID": "rich@15.0.0", + "Name": "rich", + "Identifier": { + "PURL": "pkg:pypi/rich@15.0.0", + "UID": "9a38a0981856043e" + }, + "Version": "15.0.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "markdown-it-py@4.2.0", + "pygments@2.20.0" + ] + }, + { + "ID": "rpds-py@0.30.0", + "Name": "rpds-py", + "Identifier": { + "PURL": "pkg:pypi/rpds-py@0.30.0", + "UID": "6772cf180012274b" + }, + "Version": "0.30.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "s3transfer@0.19.2", + "Name": "s3transfer", + "Identifier": { + "PURL": "pkg:pypi/s3transfer@0.19.2", + "UID": "12120efcef72eedc" + }, + "Version": "0.19.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "botocore@1.43.56" + ] + }, + { + "ID": "scrapy@2.17.0", + "Name": "scrapy", + "Identifier": { + "PURL": "pkg:pypi/scrapy@2.17.0", + "UID": "e74369fc0946fa09" + }, + "Version": "2.17.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "cryptography@49.0.0", + "cssselect@1.5.0", + "defusedxml@0.7.1", + "itemadapter@0.13.1", + "itemloaders@1.4.0", + "lxml@6.1.1", + "packaging@26.2", + "parsel@1.11.0", + "protego@0.6.2", + "pydispatcher@2.0.7", + "pyopenssl@26.3.0", + "pypydispatcher@2.1.2", + "queuelib@1.9.0", + "service-identity@26.1.0", + "tldextract@5.3.1", + "twisted@26.4.0", + "w3lib@2.4.1", + "zope-interface@8.5" + ] + }, + { + "ID": "service-identity@26.1.0", + "Name": "service-identity", + "Identifier": { + "PURL": "pkg:pypi/service-identity@26.1.0", + "UID": "eb6e07fabc9f8a8b" + }, + "Version": "26.1.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "attrs@26.1.0", + "cryptography@49.0.0" + ] + }, + { + "ID": "sgmllib3k@1.0.0", + "Name": "sgmllib3k", + "Identifier": { + "PURL": "pkg:pypi/sgmllib3k@1.0.0", + "UID": "a6388df1b2bc901" + }, + "Version": "1.0.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "shellingham@1.5.4", + "Name": "shellingham", + "Identifier": { + "PURL": "pkg:pypi/shellingham@1.5.4", + "UID": "d506ce660d05c75a" + }, + "Version": "1.5.4", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "six@1.17.0", + "Name": "six", + "Identifier": { + "PURL": "pkg:pypi/six@1.17.0", + "UID": "6d52467f91fbb7a1" + }, + "Version": "1.17.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "sniffio@1.3.1", + "Name": "sniffio", + "Identifier": { + "PURL": "pkg:pypi/sniffio@1.3.1", + "UID": "1ce19e35a8ce5758" + }, + "Version": "1.3.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "socksio@1.0.0", + "Name": "socksio", + "Identifier": { + "PURL": "pkg:pypi/socksio@1.0.0", + "UID": "7919f9e91eb10079" + }, + "Version": "1.0.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "sse-starlette@3.4.4", + "Name": "sse-starlette", + "Identifier": { + "PURL": "pkg:pypi/sse-starlette@3.4.4", + "UID": "dd13553280d62c81" + }, + "Version": "3.4.4", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "anyio@4.13.0", + "starlette@1.3.1" + ] + }, + { + "ID": "tabulate@0.10.0", + "Name": "tabulate", + "Identifier": { + "PURL": "pkg:pypi/tabulate@0.10.0", + "UID": "70c5c9cf3c6ed8fc" + }, + "Version": "0.10.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tiktoken@0.13.0", + "Name": "tiktoken", + "Identifier": { + "PURL": "pkg:pypi/tiktoken@0.13.0", + "UID": "3cd41aa3722452a5" + }, + "Version": "0.13.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "regex@2026.5.9", + "requests@2.34.2" + ] + }, + { + "ID": "tld@0.13.2", + "Name": "tld", + "Identifier": { + "PURL": "pkg:pypi/tld@0.13.2", + "UID": "788e2916ab5041aa" + }, + "Version": "0.13.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tldextract@5.3.1", + "Name": "tldextract", + "Identifier": { + "PURL": "pkg:pypi/tldextract@5.3.1", + "UID": "a4231d7e253d955" + }, + "Version": "5.3.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "filelock@3.29.0", + "idna@3.16", + "requests-file@3.0.1", + "requests@2.34.2" + ] + }, + { + "ID": "tokenizers@0.23.1", + "Name": "tokenizers", + "Identifier": { + "PURL": "pkg:pypi/tokenizers@0.23.1", + "UID": "f4ee0223d5f4cdb0" + }, + "Version": "0.23.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "huggingface-hub@1.16.1" + ] + }, + { + "ID": "tomli@2.4.1", + "Name": "tomli", + "Identifier": { + "PURL": "pkg:pypi/tomli@2.4.1", + "UID": "20bcead7b0d200b5" + }, + "Version": "2.4.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tqdm@4.67.3", + "Name": "tqdm", + "Identifier": { + "PURL": "pkg:pypi/tqdm@4.67.3", + "UID": "301669619356c4c0" + }, + "Version": "4.67.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "colorama@0.4.6" + ] + }, + { + "ID": "trafilatura@2.1.0", + "Name": "trafilatura", + "Identifier": { + "PURL": "pkg:pypi/trafilatura@2.1.0", + "UID": "146facb70391d0fd" + }, + "Version": "2.1.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "certifi@2026.5.20", + "charset-normalizer@3.4.7", + "courlan@1.4.0", + "htmldate@1.10.0", + "justext@3.0.2", + "lxml@6.1.1", + "urllib3@2.7.0" + ] + }, + { + "ID": "tree-sitter@0.25.2", + "Name": "tree-sitter", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter@0.25.2", + "UID": "9a6268b2b51b6303" + }, + "Version": "0.25.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-c-sharp@0.23.5", + "Name": "tree-sitter-c-sharp", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-c-sharp@0.23.5", + "UID": "95210b17917f7e7f" + }, + "Version": "0.23.5", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-cpp@0.23.4", + "Name": "tree-sitter-cpp", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-cpp@0.23.4", + "UID": "219b1d75e5d65d2e" + }, + "Version": "0.23.4", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-go@0.25.0", + "Name": "tree-sitter-go", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-go@0.25.0", + "UID": "595881d5dc7a0628" + }, + "Version": "0.25.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-java@0.23.5", + "Name": "tree-sitter-java", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-java@0.23.5", + "UID": "838315b64575b259" + }, + "Version": "0.23.5", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-javascript@0.25.0", + "Name": "tree-sitter-javascript", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-javascript@0.25.0", + "UID": "ae8fce409d8eeecf" + }, + "Version": "0.25.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-lua@0.5.0", + "Name": "tree-sitter-lua", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-lua@0.5.0", + "UID": "c6af3b893a75d12b" + }, + "Version": "0.5.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-php@0.24.1", + "Name": "tree-sitter-php", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-php@0.24.1", + "UID": "d1c6c35197eda7fe" + }, + "Version": "0.24.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-python@0.25.0", + "Name": "tree-sitter-python", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-python@0.25.0", + "UID": "7a7715b1e43a4360" + }, + "Version": "0.25.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-rust@0.24.2", + "Name": "tree-sitter-rust", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-rust@0.24.2", + "UID": "53d892c5390466c4" + }, + "Version": "0.24.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tree-sitter-typescript@0.23.2", + "Name": "tree-sitter-typescript", + "Identifier": { + "PURL": "pkg:pypi/tree-sitter-typescript@0.23.2", + "UID": "3495385b51052032" + }, + "Version": "0.23.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "twisted@26.4.0", + "Name": "twisted", + "Identifier": { + "PURL": "pkg:pypi/twisted@26.4.0", + "UID": "9604f6909ee425f1" + }, + "Version": "26.4.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "attrs@26.1.0", + "automat@25.4.16", + "constantly@23.10.4", + "hyperlink@21.0.0", + "incremental@24.11.0", + "typing-extensions@4.15.0", + "zope-interface@8.5" + ] + }, + { + "ID": "typer@0.25.1", + "Name": "typer", + "Identifier": { + "PURL": "pkg:pypi/typer@0.25.1", + "UID": "2e29dfd7124201ba" + }, + "Version": "0.25.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "annotated-doc@0.0.4", + "click@8.4.1", + "rich@15.0.0", + "shellingham@1.5.4" + ] + }, + { + "ID": "typing-extensions@4.15.0", + "Name": "typing-extensions", + "Identifier": { + "PURL": "pkg:pypi/typing-extensions@4.15.0", + "UID": "88de06ebf9f410bf" + }, + "Version": "4.15.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "typing-inspection@0.4.2", + "Name": "typing-inspection", + "Identifier": { + "PURL": "pkg:pypi/typing-inspection@0.4.2", + "UID": "284668b5aaa0b762" + }, + "Version": "0.4.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "typing-extensions@4.15.0" + ] + }, + { + "ID": "tzdata@2026.2", + "Name": "tzdata", + "Identifier": { + "PURL": "pkg:pypi/tzdata@2026.2", + "UID": "ceecdf3721c83e67" + }, + "Version": "2026.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "tzlocal@5.3.1", + "Name": "tzlocal", + "Identifier": { + "PURL": "pkg:pypi/tzlocal@5.3.1", + "UID": "c4b82592ea529dcb" + }, + "Version": "5.3.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "tzdata@2026.2" + ] + }, + { + "ID": "urllib3@2.7.0", + "Name": "urllib3", + "Identifier": { + "PURL": "pkg:pypi/urllib3@2.7.0", + "UID": "e5d000df273620a4" + }, + "Version": "2.7.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "uvloop@0.22.1", + "Name": "uvloop", + "Identifier": { + "PURL": "pkg:pypi/uvloop@0.22.1", + "UID": "c014f41e768af26b" + }, + "Version": "0.22.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "volcengine@1.0.222", + "Name": "volcengine", + "Identifier": { + "PURL": "pkg:pypi/volcengine@1.0.222", + "UID": "ff9e95b01340c9fb" + }, + "Version": "1.0.222", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "google@3.0.0", + "protobuf@6.33.6", + "pycryptodome@3.23.0", + "pytz@2026.2", + "requests@2.34.2", + "retry@0.9.2", + "six@1.17.0" + ] + }, + { + "ID": "volcengine-python-sdk@5.0.28", + "Name": "volcengine-python-sdk", + "Identifier": { + "PURL": "pkg:pypi/volcengine-python-sdk@5.0.28", + "UID": "550367d680327ee2" + }, + "Version": "5.0.28", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "anyio@4.13.0", + "certifi@2026.5.20", + "cryptography@49.0.0", + "httpx@0.28.1", + "pydantic@2.13.4", + "python-dateutil@2.9.0.post0", + "six@1.17.0", + "urllib3@2.7.0" + ] + }, + { + "ID": "w3lib@2.4.1", + "Name": "w3lib", + "Identifier": { + "PURL": "pkg:pypi/w3lib@2.4.1", + "UID": "bd5ce8fa2da24e38" + }, + "Version": "2.4.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "watchfiles@1.2.0", + "Name": "watchfiles", + "Identifier": { + "PURL": "pkg:pypi/watchfiles@1.2.0", + "UID": "151b6d1df9cd1dc6" + }, + "Version": "1.2.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "anyio@4.13.0" + ] + }, + { + "ID": "websocket-client@1.9.0", + "Name": "websocket-client", + "Identifier": { + "PURL": "pkg:pypi/websocket-client@1.9.0", + "UID": "c1424d85000fd7be" + }, + "Version": "1.9.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "websockets@16.0", + "Name": "websockets", + "Identifier": { + "PURL": "pkg:pypi/websockets@16.0", + "UID": "d430ebe985e5c420" + }, + "Version": "16.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "win32-setctime@1.2.0", + "Name": "win32-setctime", + "Identifier": { + "PURL": "pkg:pypi/win32-setctime@1.2.0", + "UID": "754e6d984a7107db" + }, + "Version": "1.2.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "wrapt@2.2.1", + "Name": "wrapt", + "Identifier": { + "PURL": "pkg:pypi/wrapt@2.2.1", + "UID": "9123195fab1fb9d2" + }, + "Version": "2.2.1", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "xlrd@2.0.2", + "Name": "xlrd", + "Identifier": { + "PURL": "pkg:pypi/xlrd@2.0.2", + "UID": "537a3d0858a38313" + }, + "Version": "2.0.2", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "xlsxwriter@3.2.9", + "Name": "xlsxwriter", + "Identifier": { + "PURL": "pkg:pypi/xlsxwriter@3.2.9", + "UID": "540aa0252a1a5a29" + }, + "Version": "3.2.9", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "xxhash@3.7.0", + "Name": "xxhash", + "Identifier": { + "PURL": "pkg:pypi/xxhash@3.7.0", + "UID": "820b78187c88065a" + }, + "Version": "3.7.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "yarl@1.24.2", + "Name": "yarl", + "Identifier": { + "PURL": "pkg:pypi/yarl@1.24.2", + "UID": "699a7c7261334735" + }, + "Version": "1.24.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "idna@3.16", + "multidict@6.7.1", + "propcache@0.5.2" + ] + }, + { + "ID": "zipp@4.1.0", + "Name": "zipp", + "Identifier": { + "PURL": "pkg:pypi/zipp@4.1.0", + "UID": "f8320f81fe4db0c3" + }, + "Version": "4.1.0", + "Indirect": true, + "Relationship": "indirect" + }, + { + "ID": "zope-interface@8.5", + "Name": "zope-interface", + "Identifier": { + "PURL": "pkg:pypi/zope-interface@8.5", + "UID": "e30e2fa360db3c49" + }, + "Version": "8.5", + "Indirect": true, + "Relationship": "indirect" + } + ] + } + ] +} From 7de8e00ec4faa656be53ba860d2e12ad86d40663 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Tue, 28 Jul 2026 12:28:41 +0800 Subject: [PATCH 15/22] fix(ci): run pinned Trivy container --- .github/workflows/ci-v2-production.yml | 46 ++++++++++++-------- tests/deployment/test_reference_manifests.py | 6 ++- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-v2-production.yml b/.github/workflows/ci-v2-production.yml index 6ea9180..64cb149 100644 --- a/.github/workflows/ci-v2-production.yml +++ b/.github/workflows/ci-v2-production.yml @@ -86,20 +86,32 @@ jobs: push: false load: true tags: openrath:review - - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 - with: - image-ref: openrath:review - severity: CRITICAL,HIGH - exit-code: "1" - ignore-unfixed: true - - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 - with: - image-ref: openrath:review - format: cyclonedx - output: openrath-v2-sbom.cdx.json - - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 - with: - scan-type: fs - scan-ref: . - scanners: secret - exit-code: "1" + - name: Scan image, generate SBOM, and scan repository secrets + env: + TRIVY_IMAGE: aquasec/trivy@sha256:e2b22eac59c02003d8749f5b8d9bd073b62e30fefaef5b7c8371204e0a4b0c08 # v0.67.2 + run: | + mkdir -p "$RUNNER_TEMP/trivy-cache" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image \ + --severity CRITICAL,HIGH \ + --exit-code 1 \ + --ignore-unfixed \ + openrath:review + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + -v "$PWD:/workspace" \ + "$TRIVY_IMAGE" image \ + --format cyclonedx \ + --output /workspace/openrath-v2-sbom.cdx.json \ + openrath:review + docker run --rm \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + -v "$PWD:/workspace:ro" \ + "$TRIVY_IMAGE" fs \ + --scanners secret \ + --exit-code 1 \ + --skip-dirs /workspace/.git \ + /workspace diff --git a/tests/deployment/test_reference_manifests.py b/tests/deployment/test_reference_manifests.py index 0c4b51a..1eb4624 100644 --- a/tests/deployment/test_reference_manifests.py +++ b/tests/deployment/test_reference_manifests.py @@ -34,4 +34,8 @@ def test_production_workflow_pins_actions_and_service_images() -> None: assert "redis:8-alpine@sha256:" in workflow assert "minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:" in workflow assert "pip-audit" in workflow - assert "scanners: secret" in workflow + assert "--scanners secret" in workflow + assert ( + "aquasec/trivy@sha256:" + "e2b22eac59c02003d8749f5b8d9bd073b62e30fefaef5b7c8371204e0a4b0c08" in workflow + ) From 3717423e19002d0a0c26534bf3868a39468eeac7 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Tue, 28 Jul 2026 12:35:19 +0800 Subject: [PATCH 16/22] test: remove flaky sandbox microbenchmarks --- tests/backends/test_opensandbox_async.py | 43 ++++-------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/tests/backends/test_opensandbox_async.py b/tests/backends/test_opensandbox_async.py index c70eef3..6d55ed5 100644 --- a/tests/backends/test_opensandbox_async.py +++ b/tests/backends/test_opensandbox_async.py @@ -6,8 +6,8 @@ per-sandbox exec lock. - Concurrent ``files.write`` to the *same* path serialise behind the per-path fs lock (last-writer-wins is deterministic; no torn payloads). -- Concurrent ``files.write`` to *distinct* paths run in parallel. -- Concurrent reads do not serialise behind any lock. +- Concurrent ``files.write`` to *distinct* paths do not clobber one another. +- Concurrent reads return complete payloads under thread contention. These tests require a reachable opensandbox-server (see ``conftest.py``'s ``opensandbox_real`` marker). There is no ``FakeSandbox`` fallback — the suite @@ -47,8 +47,8 @@ def os_sandbox(): backend.close(sb) -def test_concurrent_distinct_path_writes_run_in_parallel(os_sandbox) -> None: - """Writes to N distinct paths complete in < None: + """Concurrent writes to N distinct paths all commit their own payload.""" backend, sb = os_sandbox n = 8 payloads = {f"distinct_{i}.txt": f"v{i}".encode() for i in range(n)} @@ -57,26 +57,10 @@ def write_one(name: str) -> int: r = sb.dispatch(BackendToolFilesWrite(path=name, data=payloads[name])) return getattr(r, "bytes_written", -1) - # Warm-up one write so we have a per-call baseline. - t0 = time.perf_counter() - write_one(next(iter(payloads.keys()))) - per_call = time.perf_counter() - t0 - - start = time.perf_counter() with ThreadPoolExecutor(max_workers=n) as pool: results = list(pool.map(write_one, payloads.keys())) - elapsed = time.perf_counter() - start assert all(r > 0 for r in results) - # If they serialised, we'd expect ~n × per_call. Parallel should beat - # serial by at least 2×. Generous to avoid CI flake against a real server. - # Below 10 ms the fixed thread-pool and transport setup costs dominate the - # measured operation, so the ratio is not useful evidence of serialization. - if per_call > 0.01: - assert elapsed < per_call * n * 0.7, ( - f"distinct-path writes did not run in parallel: " - f"per-call ≈ {per_call:.2f}s, {n} parallel took {elapsed:.2f}s" - ) for name, want in payloads.items(): r = sb.dispatch(BackendToolFilesRead(path=name, encoding=None)) @@ -138,8 +122,8 @@ def run_one(tag: str) -> tuple[str, int]: assert "BEGIN" in out and "END" in out -def test_concurrent_reads_do_not_serialise(os_sandbox) -> None: - """Reads share no lock — N parallel reads complete much faster than serial.""" +def test_concurrent_reads_return_complete_payloads(os_sandbox) -> None: + """N concurrent reads all return the complete payload.""" backend, sb = os_sandbox # Seed a file to read. @@ -151,22 +135,7 @@ def read_one(_: int) -> bytes: r = sb.dispatch(BackendToolFilesRead(path="readable.txt", encoding=None)) return getattr(r, "data", b"") - # Baseline. - t0 = time.perf_counter() - read_one(0) - per_call = time.perf_counter() - t0 - - start = time.perf_counter() with ThreadPoolExecutor(max_workers=n) as pool: results = list(pool.map(read_one, range(n))) - elapsed = time.perf_counter() - start assert all(r == b"hello" for r in results) - # Generous bound: parallel reads should be at most ~2× a single read. - # Skip the overlap assertion when per-call latency is sub-ms (the - # serial baseline is dominated by fixed overhead, not the lock). - if per_call > 0.01: - assert elapsed < per_call * n * 0.7, ( - f"reads appear serialised: per-call ≈ {per_call:.2f}s, " - f"{n} parallel took {elapsed:.2f}s" - ) From cbba110630cb4954b4d5bad405d88f7d5e4bc9c1 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Tue, 28 Jul 2026 12:38:20 +0800 Subject: [PATCH 17/22] test: assert tool overlap without wall-clock threshold --- tests/session/test_arun_session_loop.py | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/session/test_arun_session_loop.py b/tests/session/test_arun_session_loop.py index 7a1b5ab..423a5d5 100644 --- a/tests/session/test_arun_session_loop.py +++ b/tests/session/test_arun_session_loop.py @@ -181,6 +181,8 @@ def test_arun_session_loop_write_file_via_tool_then_stop(tmp_path: Any) -> None: def test_arun_session_loop_parallel_safe_tools_overlap(tmp_path: Any) -> None: """Three writes on distinct paths run concurrently.""" n = 3 + in_flight = 0 + max_in_flight = 0 paths = [str(tmp_path / f"par_{i}.txt") for i in range(n)] parts = tuple( _tool_call( @@ -193,6 +195,10 @@ def test_arun_session_loop_parallel_safe_tools_overlap(tmp_path: Any) -> None: # Sleeping tool to make parallelism observable. resource_key returns the # path so distinct paths land on distinct queues. + import threading + + counter_lock = threading.Lock() + class _SleepyWrite(FlowToolCall): parallel_safe = True @@ -215,9 +221,17 @@ def parameters(self) -> Mapping[str, Any]: } def __call__(self, session: Session, arguments: Mapping[str, Any]) -> Any: - time.sleep(0.25) - with open(arguments["path"], "w", encoding="utf-8") as fp: - fp.write(str(arguments["content"])) + nonlocal in_flight, max_in_flight + with counter_lock: + in_flight += 1 + max_in_flight = max(max_in_flight, in_flight) + try: + time.sleep(0.25) + with open(arguments["path"], "w", encoding="utf-8") as fp: + fp.write(str(arguments["content"])) + finally: + with counter_lock: + in_flight -= 1 return True executor = _ScriptedAsyncExecutor([_tool_round(*parts, rid="r-par"), _stop("done")]) @@ -225,7 +239,6 @@ def __call__(self, session: Session, arguments: Mapping[str, Any]) -> Any: backend = get("local") with backend.open() as sandbox: user = Session.from_user_message("write three").bind_sandbox(sandbox) - t0 = time.perf_counter() out = runtime().run( _arun_session_loop( user, @@ -235,12 +248,12 @@ def __call__(self, session: Session, arguments: Mapping[str, Any]) -> Any: tools=[_SleepyWrite()], ) ) - elapsed = time.perf_counter() - t0 for p in paths: assert open(p, encoding="utf-8").read().startswith("slot-") - # 3 × 0.25s serial would be 0.75s; parallel must finish well under that. - assert elapsed < 0.5, f"parallel-safe tools did not overlap; elapsed={elapsed:.3f}s" + assert max_in_flight >= 2, ( + f"parallel-safe tools did not overlap; max_in_flight={max_in_flight}" + ) # Transcript order must remain the call order. tool_rows = [r for r in out.chunk_table.rows if r.kind == ChunkKind.TOOL_RESULT] assert [r.payload.get("tool_call_id") for r in tool_rows] == [ From feedcaadb79a349aa60c034618610231d83fb131 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Wed, 29 Jul 2026 21:07:22 +0800 Subject: [PATCH 18/22] release: prepare OpenRath 2.0.0rc1 --- .github/workflows/ci-live-provider.yml | 33 ++++ .github/workflows/ci-test-openviking.yml | 27 ++- .github/workflows/release-v2-rc.yml | 183 +++++++++++++++++++ README.md | 4 +- README_zh.md | 11 ++ deploy/compose/compose.yaml | 8 +- deploy/docs/api-governance-v2.md | 8 +- deploy/docs/known-limitations-v2.md | 27 +++ deploy/docs/openapi-v2.json | 2 +- deploy/docs/operations-v2.md | 6 + deploy/docs/threat-model-v2.md | 66 +++++++ deploy/kubernetes/openrath.yaml | 7 +- docker/Dockerfile | 7 + examples/v2_server_app.py | 19 +- pyproject.toml | 2 +- release/checklists/v2.0.0-rc.md | 26 +++ release/evidence/schema/manifest.schema.json | 33 ++++ release/notes/v2.0.0rc1.md | 51 ++++++ review/v2.0.0/release-approval.md | 28 ++- scripts/export_openapi_v2.py | 2 +- scripts/release/build_evidence.py | 124 +++++++++++++ scripts/release/verify_evidence.py | 72 ++++++++ src/rath/security/__init__.py | 9 +- src/rath/security/audit.py | 72 +++++++- tests/deployment/test_reference_manifests.py | 27 +++ tests/deployment/test_release_version.py | 42 +++++ tests/security/test_secrets_audit.py | 35 ++++ tests/server/test_openapi_contract.py | 4 +- uv.lock | 2 +- 29 files changed, 901 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/ci-live-provider.yml create mode 100644 .github/workflows/release-v2-rc.yml create mode 100644 deploy/docs/known-limitations-v2.md create mode 100644 deploy/docs/threat-model-v2.md create mode 100644 release/checklists/v2.0.0-rc.md create mode 100644 release/evidence/schema/manifest.schema.json create mode 100644 release/notes/v2.0.0rc1.md create mode 100644 scripts/release/build_evidence.py create mode 100644 scripts/release/verify_evidence.py create mode 100644 tests/deployment/test_release_version.py diff --git a/.github/workflows/ci-live-provider.yml b/.github/workflows/ci-live-provider.yml new file mode 100644 index 0000000..f8e9587 --- /dev/null +++ b/.github/workflows/ci-live-provider.yml @@ -0,0 +1,33 @@ +name: Test live provider release gate + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + live-provider: + name: pytest (live provider required) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 + with: + python-version: '3.12' + - name: Require approved provider credentials + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + if [[ -z "${OPENAI_API_KEY}" && -z "${ANTHROPIC_API_KEY}" ]]; then + echo "An approved live provider credential is required." >&2 + exit 1 + fi + - name: Install live provider test dependencies + run: uv sync --dev --frozen + - name: Run live provider lifecycle tests + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: uv run pytest -q -m live_llm diff --git a/.github/workflows/ci-test-openviking.yml b/.github/workflows/ci-test-openviking.yml index a773e64..2f0b3fa 100644 --- a/.github/workflows/ci-test-openviking.yml +++ b/.github/workflows/ci-test-openviking.yml @@ -1,7 +1,13 @@ -name: Test OpenViking +name: Test OpenViking contracts and live service on: workflow_dispatch: + inputs: + require_live: + description: Fail when live OpenViking credentials are unavailable + required: true + type: boolean + default: true push: branches: [main] paths: @@ -32,11 +38,8 @@ permissions: jobs: test-openviking: - name: pytest (openviking) + name: pytest (openviking contracts) runs-on: ubuntu-latest - # OpenViking tests require a running server with reachable embedding + - # VLM providers; allow failure in PRs until the CI environment is stable. - continue-on-error: ${{ github.event_name == 'pull_request' }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5 @@ -62,6 +65,14 @@ jobs: fi - name: Run OpenViking SDK and offline contracts run: uv run pytest -q tests/memory/unit + - name: Require live credentials for release validation + if: >- + github.event_name == 'workflow_dispatch' && + inputs.require_live && + steps.creds.outputs.available != 'true' + run: | + echo "Live OpenViking credentials are required for release validation." >&2 + exit 1 - name: Start OpenViking server if: steps.creds.outputs.available == 'true' env: @@ -76,9 +87,11 @@ jobs: echo "OPEN_VIKING_ROOT_API_KEY=${key}" >> "$GITHUB_ENV" echo "OPEN_VIKING_URL=http://127.0.0.1:1933" >> "$GITHUB_ENV" - name: Skip OpenViking (no repository secrets) - if: steps.creds.outputs.available != 'true' + if: >- + steps.creds.outputs.available != 'true' && + github.event_name != 'workflow_dispatch' run: | - echo "Skipping OpenViking integration tests." + echo "Only offline OpenViking contracts ran for this change." echo "Configure repository secrets OPEN_VIKING_EMBEDDING_API_KEY + OPEN_VIKING_VLM_API_KEY," echo "or OPENAI_API_KEY, to run the live server job." - name: Run OpenViking tests diff --git a/.github/workflows/release-v2-rc.yml b/.github/workflows/release-v2-rc.yml new file mode 100644 index 0000000..b109cc8 --- /dev/null +++ b/.github/workflows/release-v2-rc.yml @@ -0,0 +1,183 @@ +name: Publish v2 release candidate + +on: + push: + tags: + - 'v2.0.0rc*' + workflow_dispatch: + inputs: + tag: + description: Existing v2.0.0rcN tag to publish + required: true + type: string + +permissions: + contents: write + packages: write + id-token: write + attestations: write + +jobs: + publish-rc: + name: Build, attest, and publish RC + runs-on: ubuntu-latest + environment: rc-release + env: + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} + IMAGE: ghcr.io/rath-team/openrath + TRIVY_IMAGE: aquasec/trivy@sha256:e2b22eac59c02003d8749f5b8d9bd073b62e30fefaef5b7c8371204e0a4b0c08 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 + with: + python-version: '3.12' + - name: Validate tag and project version + run: | + version="${RELEASE_TAG#v}" + project_version="$(python - <<'PY' + import re + from pathlib import Path + text = Path("pyproject.toml").read_text(encoding="utf-8") + print(re.search(r'^version = "([^"]+)"$', text, re.MULTILINE).group(1)) + PY + )" + test "$version" = "$project_version" + test -f "release/notes/v${version}.md" + echo "VERSION=$version" >> "$GITHUB_ENV" + echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + - name: Verify source and package gates + run: | + uv lock --check + uv sync --frozen --all-extras --all-groups + uv run ruff format --check src tests example + uv run ruff check src tests scripts examples + uv run mypy --no-incremental src/rath + uv run pytest -q -n auto -m "not live_llm and not opensandbox and not openviking" + uv run python scripts/export_openapi_v2.py --output "$RUNNER_TEMP/openapi-v2.json" + diff -u deploy/docs/openapi-v2.json "$RUNNER_TEMP/openapi-v2.json" + uv build + uvx twine check dist/* + - name: Audit exact production dependencies + run: | + uv export --frozen --no-dev --no-emit-project \ + --extra server --extra postgres --extra s3 --extra redis --extra otel \ + --output-file production-requirements.txt + uvx pip-audit --no-deps --disable-pip \ + --format json --output dependency-audit-production.json \ + -r production-requirements.txt + uv export --frozen --all-extras --all-groups --no-emit-project \ + --output-file all-requirements.txt + uvx pip-audit --no-deps --disable-pip \ + --format json --output dependency-audit-all.json \ + -r all-requirements.txt + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push immutable RC image + id: image + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: docker/Dockerfile + push: true + tags: | + ${{ env.IMAGE }}:${{ env.VERSION }} + ${{ env.IMAGE }}:${{ env.SOURCE_SHA }} + build-args: | + OPENRATH_VERSION=${{ env.VERSION }} + OPENRATH_REVISION=${{ env.SOURCE_SHA }} + - name: Pull, scan, and describe published image + env: + IMAGE_DIGEST: ${{ steps.image.outputs.digest }} + run: | + mkdir -p release/evidence/"$VERSION" + docker pull "$IMAGE@$IMAGE_DIGEST" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD:/workspace" \ + "$TRIVY_IMAGE" image \ + --severity CRITICAL,HIGH \ + --exit-code 1 \ + --ignore-unfixed \ + --format json \ + --output /workspace/release/evidence/"$VERSION"/image-scan.json \ + "$IMAGE@$IMAGE_DIGEST" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD:/workspace" \ + "$TRIVY_IMAGE" image \ + --format cyclonedx \ + --output /workspace/release/evidence/"$VERSION"/sbom.cdx.json \ + "$IMAGE@$IMAGE_DIGEST" + docker run --rm \ + -v "$PWD:/workspace:ro" \ + -v "$PWD/release/evidence/$VERSION:/evidence" \ + "$TRIVY_IMAGE" fs \ + --scanners secret \ + --exit-code 1 \ + --format json \ + --output /evidence/secret-scan.json \ + --skip-dirs /workspace/.git \ + /workspace + cp dependency-audit-production.json release/evidence/"$VERSION"/ + cp dependency-audit-all.json release/evidence/"$VERSION"/ + sed "s#${IMAGE}:${VERSION}#${IMAGE}@${IMAGE_DIGEST}#g" \ + deploy/kubernetes/openrath.yaml \ + > release/evidence/"$VERSION"/openrath-kubernetes.yaml + - name: Build and verify SHA-bound evidence + env: + IMAGE_DIGEST: ${{ steps.image.outputs.digest }} + run: | + uv run python scripts/release/build_evidence.py \ + --tag "$RELEASE_TAG" \ + --image-ref "$IMAGE" \ + --image-digest "$IMAGE_DIGEST" \ + --output release/evidence/"$VERSION"/manifest.json \ + --artifact sbom=release/evidence/"$VERSION"/sbom.cdx.json \ + --artifact image_scan=release/evidence/"$VERSION"/image-scan.json \ + --artifact secret_scan=release/evidence/"$VERSION"/secret-scan.json \ + --artifact dependency_audit_production=release/evidence/"$VERSION"/dependency-audit-production.json \ + --artifact dependency_audit_all=release/evidence/"$VERSION"/dependency-audit-all.json \ + --artifact kubernetes=release/evidence/"$VERSION"/openrath-kubernetes.yaml + uv run python scripts/release/verify_evidence.py \ + release/evidence/"$VERSION"/manifest.json + - name: Attest published image + uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.image.outputs.digest }} + push-to-registry: true + - name: Upload release evidence + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: openrath-${{ env.VERSION }}-release + retention-days: 90 + if-no-files-found: error + path: | + dist/* + release/evidence/${{ env.VERSION }}/* + - name: Create or update GitHub prerelease + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release upload "$RELEASE_TAG" --clobber \ + dist/* release/evidence/"$VERSION"/* + gh release edit "$RELEASE_TAG" \ + --prerelease \ + --title "OpenRath $VERSION" \ + --notes-file "release/notes/v${VERSION}.md" + else + gh release create "$RELEASE_TAG" \ + dist/* release/evidence/"$VERSION"/* \ + --verify-tag \ + --prerelease \ + --title "OpenRath $VERSION" \ + --notes-file "release/notes/v${VERSION}.md" + fi diff --git a/README.md b/README.md index 9a485de..96c39fa 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ Most agent frameworks begin with an agent loop. OpenRath begins with **Session** OpenRath is designed for this: many agents collaborating across many branchable sessions, while still tracing every role, workspace, memory write, and final output. -## v2.0.0 durable runtime (unreleased) +## v2.0.0 durable runtime (release candidate) -The v2 candidate adds explicit `@step` / `@router` execution plans, durable +The `2.0.0rc1` candidate adds explicit `@step` / `@router` execution plans, durable Runs and checkpoints, effect reconciliation, tenant-scoped Agent Server APIs, and governed Provider/Tool/Sandbox/Memory adapters. The HTTP contract is currently **Beta**; v1 JSONL imports are historical and cannot resume an active diff --git a/README_zh.md b/README_zh.md index 3082295..87ea319 100644 --- a/README_zh.md +++ b/README_zh.md @@ -54,6 +54,17 @@ OpenRath 为此而设计:多个 Agent 在多个可分支 Session 上协作,同时仍能追踪每个 role、workspace、memory 写入和最终输出。 +## v2.0.0 durable runtime(候选版本) + +`2.0.0rc1` 新增显式 `@step` / `@router` 执行计划、durable Run 与 +Checkpoint、effect reconciliation、tenant-scoped Agent Server API,以及受 +治理的 Provider/Tool/Sandbox/Memory adapter。HTTP contract 在 RC 阶段仍为 +**Beta**;v1 JSONL 导入仅作为历史记录,不能恢复 active Run。 + +Embedded mode 面向可信本地进程。Agent Server mode 是严格的 durable +profile:token 必须具有显式 action grants,对象访问按 tenant/project +隔离;需要强制 deadline 时应使用 async step 或 isolated executor。 +

多智能体多会话映射

diff --git a/deploy/compose/compose.yaml b/deploy/compose/compose.yaml index 83ee18f..650ab63 100644 --- a/deploy/compose/compose.yaml +++ b/deploy/compose/compose.yaml @@ -5,7 +5,7 @@ services: build: context: ../.. dockerfile: docker/Dockerfile - image: openrath:2.0.0-review + image: openrath:2.0.0rc1 entrypoint: ["openrath-migrate"] environment: OPENRATH_POSTGRES_DSN: postgresql://openrath:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/openrath @@ -24,12 +24,13 @@ services: build: context: ../.. dockerfile: docker/Dockerfile - image: openrath:2.0.0-review + image: openrath:2.0.0rc1 environment: OPENRATH_APP: examples.v2_server_app:app OPENRATH_POSTGRES_DSN: postgresql://openrath:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/openrath OPENRATH_TOKEN: ${OPENRATH_TOKEN:?set OPENRATH_TOKEN} OPENRATH_TENANT_ID: ${OPENRATH_TENANT_ID:-default} + OPENRATH_GRANTS: assistant.read,session.read,session.create,run.read,run.create,run.cancel,run.resume,interrupt.read,interrupt.decide,feedback.create,metrics.read OPENRATH_EMBEDDED_WORKER: "false" ports: - "${OPENRATH_PORT:-8000}:8000" @@ -58,13 +59,14 @@ services: build: context: ../.. dockerfile: docker/Dockerfile - image: openrath:2.0.0-review + image: openrath:2.0.0rc1 entrypoint: ["openrath-worker"] command: ["--app", "examples.v2_server_app:server"] environment: OPENRATH_POSTGRES_DSN: postgresql://openrath:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/openrath OPENRATH_TOKEN: ${OPENRATH_TOKEN:?set OPENRATH_TOKEN} OPENRATH_TENANT_ID: ${OPENRATH_TENANT_ID:-default} + OPENRATH_GRANTS: assistant.read,session.read,session.create,run.read,run.create,run.cancel,run.resume,interrupt.read,interrupt.decide,feedback.create,metrics.read OPENRATH_EMBEDDED_WORKER: "false" depends_on: migrate: diff --git a/deploy/docs/api-governance-v2.md b/deploy/docs/api-governance-v2.md index c3fbdda..1f08f95 100644 --- a/deploy/docs/api-governance-v2.md +++ b/deploy/docs/api-governance-v2.md @@ -1,7 +1,8 @@ # OpenRath v2 API and maintenance policy -This policy is part of the v2.0.0 review candidate and becomes effective only -when the repository owner approves the release. +This policy is part of the v2.0.0 release candidate. The RC publishes it for +compatibility feedback; final Stable promotions and the v1 maintenance window +still require repository-owner approval before GA. ## Stability levels @@ -22,6 +23,9 @@ The Agent Server OpenAPI document currently labels `/v1` operations **Beta**. Stable error codes and persisted fields may be promoted independently only after the RC evidence gate passes. +For `2.0.0rc1`, the Python v1 façade remains supported and the Agent Server +remains Beta. The RC does not begin or end a maintenance window. + ## Action and object authorization Authentication alone grants no access. Tokens carry explicit action grants; diff --git a/deploy/docs/known-limitations-v2.md b/deploy/docs/known-limitations-v2.md new file mode 100644 index 0000000..8052a41 --- /dev/null +++ b/deploy/docs/known-limitations-v2.md @@ -0,0 +1,27 @@ +# OpenRath v2.0.0 release-candidate limitations + +These limitations are part of the `2.0.0rc1` contract and must not be omitted +from release notes. + +- The Agent Server `/v1` HTTP contract is Beta during the RC. Stable error + codes and persisted fields are promoted only after compatibility review. +- v1 JSONL Sessions import as non-resumable history. They do not contain a + durable program counter or an effect outcome. +- Embedded mode trusts the local process. It is not a multi-tenant isolation + boundary. +- A synchronous Python step cannot be preempted safely in-process. The durable + server profile rejects synchronous timeout declarations; use an async or + isolated executor. +- Redis improves wake, cancel, and stream latency but never stores final Run + state. Its loss falls back to bounded PostgreSQL polling. +- Exactly-once behavior is not promised for arbitrary external side effects. + Non-idempotent ambiguous outcomes stop in `NEEDS_REVIEW`. +- The static-token reference application is a deployment example. Operators + must integrate their identity provider, secret manager, TLS ingress, audit + collector, retention, and network policy. +- OpenViking and live provider support remains conditional on the published + compatibility range and successful lifecycle validation with the selected + service. +- Capacity numbers are hardware/workload profiles, not universal SLA values. +- Webhooks, cron triggers, enterprise UI, SAML/SCIM, billing, and cross-region + active-active storage are outside the v2.0.0 scope. diff --git a/deploy/docs/openapi-v2.json b/deploy/docs/openapi-v2.json index 4767f91..de58914 100644 --- a/deploy/docs/openapi-v2.json +++ b/deploy/docs/openapi-v2.json @@ -328,7 +328,7 @@ "info": { "description": "Beta v2 durable Agent Server API.", "title": "OpenRath Agent Server", - "version": "2.0.0-unreleased" + "version": "2.0.0rc1" }, "openapi": "3.1.0", "paths": { diff --git a/deploy/docs/operations-v2.md b/deploy/docs/operations-v2.md index e5c047f..106a8f0 100644 --- a/deploy/docs/operations-v2.md +++ b/deploy/docs/operations-v2.md @@ -23,6 +23,12 @@ allows same-process filesystem/network behavior and is unsuitable for untrusted tenants. Service deployments should supply a fail-closed policy, governed adapter executors, durable effect ledger, and audit sink. +The reference server requires `OPENRATH_GRANTS` with explicit actions and +rejects the wildcard grant. It emits redacted newline-delimited JSON audit +records to stdout. Production operators must configure a collector, retention, +access control, and delivery alert for that stream; an in-memory sink is never +production evidence. + The Kubernetes template is fail closed for egress. PostgreSQL, Redis, S3, and an HTTPS egress gateway must run in a namespace labelled `openrath.io/data-plane=allowed`; DNS is limited to `kube-system`. If the CNI diff --git a/deploy/docs/threat-model-v2.md b/deploy/docs/threat-model-v2.md new file mode 100644 index 0000000..e784766 --- /dev/null +++ b/deploy/docs/threat-model-v2.md @@ -0,0 +1,66 @@ +# OpenRath v2 threat model + +Status: release-candidate baseline. Residual risks remain blocking for GA until +the linked target-environment evidence is attached. + +## Assets and security objectives + +| Asset | Required property | +| --- | --- | +| Provider, Tool, Sandbox, Memory, database, object-store credentials | Confidential; referenced rather than persisted in Run state | +| Tenant and project data | Isolated at every API, store, adapter, and artifact boundary | +| Run, Event, Checkpoint, Interrupt, and Effect records | Durable, ordered, attributable, and protected from stale writers | +| Artifact content | Tenant scoped, size bounded, integrity checked | +| Revision and ExecutionPlan | Immutable identity bound to deployed content | +| Security audit records | Redacted, correlated, append-only at the configured collector | + +## Trust boundaries + +1. HTTP ingress is untrusted until the authentication provider produces a + `SecurityContext`. Request tenant/project fields never override that context. +2. Provider, Tool, MCP, Sandbox, Memory, and recalled content are external and + untrusted. Each adapter call receives a reduced context and policy decision. +3. PostgreSQL is the durable source of truth. Redis is only a wake/cancel/fanout + accelerator and cannot reconstruct Run state. +4. S3-compatible storage is outside the process boundary. Artifact size, + tenant scope, and SHA-256 are checked independently of object metadata. +5. Workers are replaceable and may be stale. Lease expiry and fencing tokens + prevent an old worker from committing after ownership changes. +6. Operators, registry publishers, migration identities, and runtime identities + are separate roles. Runtime identities do not need DDL or release rights. + +## Principal abuse cases and controls + +| Abuse case | Control | Required evidence | +| --- | --- | --- | +| Tenant/project/object bypass | `SecurityContext` authority, action grants, scoped store queries, not-found masking | Cross-scope API/store negative tests | +| Bearer or provider secret leakage | `SecretRef`, redacted repr/log/trace/audit/error paths | Secret canary tests and repository scan | +| SSRF through URL ingestion | Explicit allowed HTTP hosts, redirect rejection, bounded response | Loopback/link-local/private-range and redirect tests | +| Path or symlink escape | Canonical root checks, symlink rejection, bounded file operations | Traversal/symlink tests on supported platforms | +| Prompt or recalled-content privilege escalation | Trust/provenance labels; external text never becomes SYSTEM authority | Trust-preservation tests | +| Duplicate delivery or stale worker commit | CAS, idempotency key, lease and fencing token | Duplicate/stale-token chaos tests | +| Ambiguous non-idempotent effect replay | Durable effect ledger and `NEEDS_REVIEW` | Kill-after-dispatch test | +| Queue, body, page, or SSE exhaustion | Body/page/queue bounds, deadlines, bounded SSE batches and backoff | Resource-exhaustion tests and load report | +| Revision substitution | Canonical plan/revision digest and resume compatibility check | Revision mismatch tests | +| Audit suppression | Production reference app wires a structured audit sink; sink failures propagate | Audit delivery and redaction tests | + +## Residual risks for the RC + +- The Agent Server HTTP surface remains **Beta** in `2.0.0rc1`. +- Live provider and OpenViking lifecycle evidence requires approved external + credentials and is not replaced by offline contracts. +- The reference static-token authenticator is suitable for a bounded + self-hosted example, not a complete enterprise identity provider. +- Container stdout audit durability depends on an operator-configured collector + and retention policy. +- A local SQLite soak or single-host benchmark is not production capacity + evidence. +- Arbitrary third-party non-idempotent side effects cannot be guaranteed + exactly once. + +## GA closure + +GA requires a candidate-SHA-bound report covering live adapters, explicit +tenant/security negatives, image/dependency/secret scans, an eight-hour +target-like soak, worker scaling, backup/restore, and rollout/rollback. Any +accepted exception records an owner, mitigation, expiry, and target version. diff --git a/deploy/kubernetes/openrath.yaml b/deploy/kubernetes/openrath.yaml index c1e4271..7272c64 100644 --- a/deploy/kubernetes/openrath.yaml +++ b/deploy/kubernetes/openrath.yaml @@ -19,6 +19,7 @@ data: OPENRATH_EMBEDDED_WORKER: "false" OPENRATH_DB_SCHEMA: openrath OPENRATH_TENANT_ID: default + OPENRATH_GRANTS: assistant.read,session.read,session.create,run.read,run.create,run.cancel,run.resume,interrupt.read,interrupt.decide,feedback.create,metrics.read --- apiVersion: batch/v1 kind: Job @@ -39,7 +40,7 @@ spec: type: RuntimeDefault containers: - name: migrate - image: ghcr.io/rath-team/openrath:2.0.0-review + image: ghcr.io/rath-team/openrath:2.0.0rc1 # Release automation must replace the review tag with image@sha256. imagePullPolicy: Always command: ["openrath-migrate"] @@ -76,7 +77,7 @@ spec: type: RuntimeDefault containers: - name: openrath - image: ghcr.io/rath-team/openrath:2.0.0-review + image: ghcr.io/rath-team/openrath:2.0.0rc1 # Release automation must replace the review tag with image@sha256. imagePullPolicy: Always envFrom: @@ -159,7 +160,7 @@ spec: type: RuntimeDefault containers: - name: worker - image: ghcr.io/rath-team/openrath:2.0.0-review + image: ghcr.io/rath-team/openrath:2.0.0rc1 # Release automation must replace the review tag with image@sha256. imagePullPolicy: Always command: ["openrath-worker", "--app", "examples.v2_server_app:server"] diff --git a/docker/Dockerfile b/docker/Dockerfile index 72ac635..13623f6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,6 +15,13 @@ RUN pip install uv==0.7.18 \ FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS runtime +ARG OPENRATH_VERSION=0+unknown +ARG OPENRATH_REVISION=unknown +LABEL org.opencontainers.image.title="OpenRath" \ + org.opencontainers.image.source="https://github.com/Rath-Team/OpenRath" \ + org.opencontainers.image.version="${OPENRATH_VERSION}" \ + org.opencontainers.image.revision="${OPENRATH_REVISION}" + ENV PATH="/opt/venv/bin:${PATH}" \ PYTHONPATH="/app" \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/examples/v2_server_app.py b/examples/v2_server_app.py index 1a354cc..026854c 100644 --- a/examples/v2_server_app.py +++ b/examples/v2_server_app.py @@ -9,7 +9,12 @@ from rath.definition import EffectClass, step from rath.flow import Workflow from rath.runtime import LocalRuntime, PostgresEffectLedger, PostgresRunStore -from rath.security import Principal, PrincipalKind, SecurityContext +from rath.security import ( + Principal, + PrincipalKind, + SecurityContext, + StructuredAuditSink, +) from rath.server import AgentServer, StaticTokenAuth from rath.session import Session @@ -37,6 +42,15 @@ def forward(self, session: Session) -> Session: dsn = os.environ["OPENRATH_POSTGRES_DSN"] token = os.environ["OPENRATH_TOKEN"] tenant_id = os.getenv("OPENRATH_TENANT_ID", "default") +grants = frozenset( + grant.strip() + for grant in os.environ["OPENRATH_GRANTS"].split(",") + if grant.strip() +) +if not grants or "*" in grants: + raise RuntimeError( + "OPENRATH_GRANTS must contain explicit action grants and must not use '*'" + ) store = PostgresRunStore( dsn, schema=os.getenv("OPENRATH_DB_SCHEMA", "openrath"), @@ -56,10 +70,11 @@ def forward(self, session: Session) -> Session: token: SecurityContext( principal=Principal(id="reference-user", kind=PrincipalKind.SERVICE), tenant_id=tenant_id, - grants=frozenset({"*"}), + grants=grants, ) } ), + audit_sink=StructuredAuditSink(), embedded_worker=os.getenv("OPENRATH_EMBEDDED_WORKER", "true").lower() == "true", worker_id=os.getenv("HOSTNAME", "standalone-worker"), worker_lease_seconds=float(os.getenv("OPENRATH_WORKER_LEASE_SECONDS", "30")), diff --git a/pyproject.toml b/pyproject.toml index b0b1301..0f845af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "openrath" -version = "1.3.0" +version = "2.0.0rc1" description = "An open-source, torch-like API framework for dynamic multi-agent workflow." readme = "README.md" requires-python = ">=3.10,<3.14" diff --git a/release/checklists/v2.0.0-rc.md b/release/checklists/v2.0.0-rc.md new file mode 100644 index 0000000..056aae4 --- /dev/null +++ b/release/checklists/v2.0.0-rc.md @@ -0,0 +1,26 @@ +# OpenRath v2.0.0 RC checklist + +Candidate: `v2.0.0rc1` + +## RC artifact publication + +- [ ] Package, OpenAPI, image label, tag, and release title report `2.0.0rc1`. +- [ ] Lock, Ruff, mypy, offline tests, integration tests, OpenSandbox, build, + dependency audits, image scan, secret scan, and manifest validation pass. +- [ ] Wheel, sdist, SBOM, scan reports, manifest, and immutable image digest are + attached to the GitHub prerelease. +- [ ] Release notes state that live provider/OpenViking and target-environment + Gate C remain pending. +- [ ] No PyPI publication or shared deployment occurs as part of RC1. + +## GA-blocking acceptance after RC publication + +- [ ] Approved live provider lifecycle. +- [ ] Approved live OpenViking lifecycle. +- [ ] Target-like single-host and split-profile capacity report. +- [ ] One-to-four worker scaling efficiency at least 70%. +- [ ] Eight-hour target-like soak. +- [ ] PostgreSQL/Redis/S3/API/worker fault matrix. +- [ ] Target-cluster backup/restore and rollout/rollback. +- [ ] Final API stability and v1 maintenance-window owner decision. +- [ ] Final `2.0.0` evidence regenerated from the GA SHA. diff --git a/release/evidence/schema/manifest.schema.json b/release/evidence/schema/manifest.schema.json new file mode 100644 index 0000000..48e6056 --- /dev/null +++ b/release/evidence/schema/manifest.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openrath.dev/schemas/rc-evidence-manifest.json", + "title": "OpenRath RC evidence manifest", + "type": "object", + "required": [ + "schema", + "release_stage", + "version", + "tag", + "source_commit", + "source_tree_clean", + "ga_approved", + "artifacts", + "blocking_gates" + ], + "properties": { + "schema": {"const": "openrath.rc-evidence/1"}, + "release_stage": {"const": "rc"}, + "version": {"type": "string", "pattern": "^2\\.0\\.0rc[0-9]+$"}, + "tag": {"type": "string", "pattern": "^v2\\.0\\.0rc[0-9]+$"}, + "source_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "source_tree_clean": {"const": true}, + "ga_approved": {"const": false}, + "artifacts": {"type": "object"}, + "blocking_gates": { + "type": "array", + "minItems": 1, + "items": {"type": "string"} + } + }, + "additionalProperties": true +} diff --git a/release/notes/v2.0.0rc1.md b/release/notes/v2.0.0rc1.md new file mode 100644 index 0000000..8990094 --- /dev/null +++ b/release/notes/v2.0.0rc1.md @@ -0,0 +1,51 @@ +# OpenRath v2.0.0rc1 + +`v2.0.0rc1` is the first installable release candidate for OpenRath's durable +Runtime and Agent Server. It is published for integration, compatibility, and +operator validation. It is **not GA approval** and is not deployed by this +release. + +## Highlights + +- Explicit `@step` / `@router` compilation with canonical plans and revision + identity. +- Durable Run, Event, Checkpoint, Interrupt, lease/fencing, cancellation, + deadline, retry, and effect-reconciliation semantics. +- PostgreSQL production storage, optional Redis signaling, and + S3-compatible artifact storage. +- Tenant/project-scoped Agent Server HTTP/SSE APIs with explicit action grants, + bounded queues/pages/bodies, security headers, and redacted structured audit. +- Governed Provider, Tool/MCP, Sandbox, and Memory adapter boundaries. +- OpenTelemetry integration, datasets/experiments/feedback, migration tooling, + Compose/Kubernetes references, SBOM, and security gates. + +## Stability + +- The v1 Python façade remains supported during the RC. +- The Agent Server `/v1` HTTP surface remains **Beta**. +- v1 JSONL imports are historical and cannot resume active Runs. +- See `deploy/docs/known-limitations-v2.md`, + `deploy/docs/threat-model-v2.md`, and `deploy/docs/migration-v2.md`. + +## RC evidence boundary + +This prerelease includes source/package/container CI and local/available +backend evidence. The following remain blocking before final `v2.0.0` GA: + +1. Approved live LLM/provider lifecycle. +2. Approved live OpenViking lifecycle. +3. Target-like single-host and split-profile capacity results. +4. One-to-four worker scaling efficiency validation. +5. Eight-hour target-like soak. +6. Target-cluster backup/restore, dependency-failure, rollout, and rollback + drills. +7. Final API stability, v1 maintenance-window, and GA owner approval. + +Do not represent this RC as production certification. + +## Distribution + +- Wheel and source distribution are attached to this GitHub prerelease. +- The OCI image is published to GHCR and identified by the digest in the + attached evidence manifest. +- This RC does not publish to PyPI and does not deploy to a shared environment. diff --git a/review/v2.0.0/release-approval.md b/review/v2.0.0/release-approval.md index ea51f16..714a5b6 100644 --- a/review/v2.0.0/release-approval.md +++ b/review/v2.0.0/release-approval.md @@ -1,15 +1,22 @@ -# OpenRath v2.0.0 release approval +# OpenRath v2 release approval -Current decision: **HOLD — owner review required** +Current decision: **v2.0.0rc1 PUBLICATION AUTHORIZED; v2.0.0 GA HOLD** -The implementation may be reviewed, amended, and committed locally. The -following actions remain prohibited until the repository owner gives explicit -approval in a later message: +The repository owner explicitly requested publication of an RC on 2026-07-29. +That authorizes the following RC-only actions: -- changing package metadata to `2.0.0`; -- creating or pushing a `v2.0.0` tag; -- pushing the review branch or image; -- creating a GitHub release; +- changing package metadata to `2.0.0rc1`; +- pushing the `codex/v2-review-remediation` branch; +- creating and pushing the `v2.0.0rc1` tag; +- publishing wheel/sdist as GitHub prerelease assets; +- publishing the RC image to GHCR by immutable digest; +- creating a GitHub prerelease. + +The following remain prohibited until separately approved: + +- changing package metadata to final `2.0.0`; +- creating or pushing the final `v2.0.0` tag; +- publishing the final PyPI/GHCR/GitHub release; - deploying to any shared, staging, or production environment. ## Owner review checklist @@ -30,3 +37,6 @@ approval in a later message: Approval must be explicit. Silence, code review completion, or a passing CI run does not authorize publication. + +RC publication is not evidence that the unchecked GA items passed. The +prerelease must link the unresolved Gate C items and remain marked prerelease. diff --git a/scripts/export_openapi_v2.py b/scripts/export_openapi_v2.py index 52902ce..d338d3c 100644 --- a/scripts/export_openapi_v2.py +++ b/scripts/export_openapi_v2.py @@ -16,7 +16,7 @@ def main() -> None: type=Path, default=Path("deploy/docs/openapi-v2.json"), ) - parser.add_argument("--version", default="2.0.0-unreleased") + parser.add_argument("--version", default="2.0.0rc1") args = parser.parse_args() args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text( diff --git a/scripts/release/build_evidence.py b/scripts/release/build_evidence.py new file mode 100644 index 0000000..bfc7be8 --- /dev/null +++ b/scripts/release/build_evidence.py @@ -0,0 +1,124 @@ +"""Build a SHA-bound OpenRath release-candidate evidence manifest.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +from datetime import datetime, timezone +from pathlib import Path + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _project_version() -> str: + text = Path("pyproject.toml").read_text(encoding="utf-8") + match = re.search(r'^version = "([^"]+)"$', text, flags=re.MULTILINE) + if match is None: + raise RuntimeError("project version is missing from pyproject.toml") + return match.group(1) + + +def _git(*arguments: str) -> str: + return subprocess.check_output( + ["git", *arguments], + text=True, + encoding="utf-8", + ).strip() + + +def _artifact(path: Path) -> dict[str, object]: + if not path.is_file(): + raise FileNotFoundError(path) + return { + "path": path.as_posix(), + "size": path.stat().st_size, + "sha256": _sha256(path), + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--tag", required=True) + parser.add_argument("--image-ref", required=True) + parser.add_argument("--image-digest", required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument( + "--artifact", + action="append", + default=[], + metavar="NAME=PATH", + help="additional report or artifact to hash", + ) + args = parser.parse_args() + + version = _project_version() + if args.tag != f"v{version}": + raise SystemExit(f"tag {args.tag!r} does not match project version {version!r}") + if re.fullmatch(r"sha256:[0-9a-f]{64}", args.image_digest) is None: + raise SystemExit("image digest must be sha256 followed by 64 lowercase hex") + + wheel = next(Path("dist").glob(f"openrath-{version}-*.whl")) + sdist = Path("dist") / f"openrath-{version}.tar.gz" + artifacts: dict[str, object] = { + "wheel": _artifact(wheel), + "sdist": _artifact(sdist), + "openapi": _artifact(Path("deploy/docs/openapi-v2.json")), + "image": { + "reference": args.image_ref, + "digest": args.image_digest, + }, + } + for item in args.artifact: + name, separator, raw_path = item.partition("=") + if not separator or not name or not raw_path: + raise SystemExit(f"invalid --artifact value: {item!r}") + if name in artifacts: + raise SystemExit(f"duplicate artifact name: {name}") + artifacts[name] = _artifact(Path(raw_path)) + + commit = _git("rev-parse", "HEAD") + tracked_status = _git("status", "--porcelain", "--untracked-files=no") + manifest = { + "schema": "openrath.rc-evidence/1", + "generated_at": datetime.now(timezone.utc).isoformat(), + "release_stage": "rc", + "version": version, + "tag": args.tag, + "source_commit": commit, + "source_tree_clean": not tracked_status, + "base_commit": _git("merge-base", "HEAD", "origin/main"), + "ga_approved": False, + "workflow": { + "repository": os.getenv("GITHUB_REPOSITORY"), + "run_id": os.getenv("GITHUB_RUN_ID"), + "run_attempt": os.getenv("GITHUB_RUN_ATTEMPT"), + }, + "artifacts": artifacts, + "blocking_gates": [ + "approved live LLM/provider lifecycle", + "approved live OpenViking lifecycle", + "target-like capacity and one-to-four worker scaling", + "eight-hour target-like soak", + "target-cluster backup/restore and rollout/rollback drills", + "final API stability, v1 maintenance window, and GA owner approval", + ], + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/verify_evidence.py b/scripts/release/verify_evidence.py new file mode 100644 index 0000000..8362499 --- /dev/null +++ b/scripts/release/verify_evidence.py @@ -0,0 +1,72 @@ +"""Verify an OpenRath release-candidate evidence manifest.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import subprocess +from pathlib import Path + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _project_version() -> str: + text = Path("pyproject.toml").read_text(encoding="utf-8") + match = re.search(r'^version = "([^"]+)"$', text, flags=re.MULTILINE) + if match is None: + raise RuntimeError("project version is missing from pyproject.toml") + return match.group(1) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("manifest", type=Path) + args = parser.parse_args() + + manifest = json.loads(args.manifest.read_text(encoding="utf-8")) + version = _project_version() + commit = subprocess.check_output( + ["git", "rev-parse", "HEAD"], + text=True, + encoding="utf-8", + ).strip() + + assert manifest["schema"] == "openrath.rc-evidence/1" + assert manifest["release_stage"] == "rc" + assert manifest["version"] == version + assert manifest["tag"] == f"v{version}" + assert manifest["source_commit"] == commit + assert manifest["source_tree_clean"] is True + assert manifest["ga_approved"] is False + assert manifest["blocking_gates"] + + openapi = json.loads( + Path("deploy/docs/openapi-v2.json").read_text(encoding="utf-8") + ) + assert openapi["info"]["version"] == version + + for name, artifact in manifest["artifacts"].items(): + if name == "image": + assert re.fullmatch(r"sha256:[0-9a-f]{64}", artifact["digest"]) + continue + path = Path(artifact["path"]) + assert path.is_file(), path + assert path.stat().st_size == artifact["size"], path + assert _sha256(path) == artifact["sha256"], path + + print( + f"verified {manifest['tag']} evidence for " + f"{manifest['source_commit']} ({len(manifest['artifacts'])} artifacts)" + ) + + +if __name__ == "__main__": + main() diff --git a/src/rath/security/__init__.py b/src/rath/security/__init__.py index aba3b45..8eca7ba 100644 --- a/src/rath/security/__init__.py +++ b/src/rath/security/__init__.py @@ -1,6 +1,12 @@ """Public security contracts for identity, policy, secrets, and audit.""" -from rath.security.audit import AuditEvent, AuditKind, AuditSink, InMemoryAuditSink +from rath.security.audit import ( + AuditEvent, + AuditKind, + AuditSink, + InMemoryAuditSink, + StructuredAuditSink, +) from rath.security.context import ( Principal, PrincipalKind, @@ -48,5 +54,6 @@ "SecretRef", "SecretResolver", "SecurityContext", + "StructuredAuditSink", "TrustLevel", ] diff --git a/src/rath/security/audit.py b/src/rath/security/audit.py index a6caf99..1cef910 100644 --- a/src/rath/security/audit.py +++ b/src/rath/security/audit.py @@ -2,12 +2,14 @@ from __future__ import annotations +import json +import sys import threading from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum -from typing import TYPE_CHECKING, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Callable, Protocol, runtime_checkable from uuid import UUID, uuid4 from rath._json import JSONValue, freeze_mapping @@ -21,8 +23,28 @@ "AuditKind", "AuditSink", "InMemoryAuditSink", + "StructuredAuditSink", ] +_SENSITIVE_AUDIT_FIELDS = frozenset( + {"api_key", "authorization", "cookie", "password", "secret", "token"} +) + + +def _redact_audit(value: object) -> object: + if isinstance(value, Mapping): + return { + str(key): ( + "" + if any(part in str(key).lower() for part in _SENSITIVE_AUDIT_FIELDS) + else _redact_audit(item) + ) + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return [_redact_audit(item) for item in value] + return value + class AuditKind(str, Enum): AUTHENTICATION = "authentication" @@ -110,3 +132,51 @@ def events(self) -> tuple[AuditEvent, ...]: async def emit(self, event: AuditEvent) -> None: with self._lock: self._events.append(event) + + +class StructuredAuditSink: + """Emit redacted, newline-delimited JSON security audit records. + + The default sink writes and flushes stdout so container log collectors can + retain the security stream independently from diagnostic traces. Sink + failures deliberately propagate: a production reference deployment must + not silently discard an audit record. + """ + + def __init__(self, sink: Callable[[str], None] | None = None) -> None: + self._sink = sink or self._write_stdout + + @staticmethod + def _write_stdout(line: str) -> None: + sys.stdout.write(line + "\n") + sys.stdout.flush() + + async def emit(self, event: AuditEvent) -> None: + record: dict[str, object] = { + "schema": "openrath.security-audit/1", + "id": str(event.id), + "kind": event.kind.value, + "occurred_at": event.occurred_at.isoformat(), + "tenant_id": event.tenant_id, + "principal_id": event.principal_id, + "request_id": str(event.request_id), + "trace_id": event.trace_id, + "action": event.action, + "resource_kind": event.resource_kind, + "resource_id": event.resource_id, + "outcome": event.outcome, + "reason": event.reason, + "policy_id": event.policy_id, + "attributes": dict(event.attributes), + } + safe = _redact_audit(record) + assert isinstance(safe, dict) + self._sink( + json.dumps( + safe, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + default=str, + ) + ) diff --git a/tests/deployment/test_reference_manifests.py b/tests/deployment/test_reference_manifests.py index 1eb4624..e949282 100644 --- a/tests/deployment/test_reference_manifests.py +++ b/tests/deployment/test_reference_manifests.py @@ -23,6 +23,17 @@ def test_kubernetes_template_covers_workloads_and_dns() -> None: assert "protocol: TCP\n port: 53" in manifest assert manifest.count("Release automation must replace") == 3 assert "imagePullPolicy: Always" in manifest + assert "OPENRATH_GRANTS:" in manifest + assert '{"*"}' not in manifest + + +def test_reference_server_enables_audit_and_explicit_grants() -> None: + app = Path("examples/v2_server_app.py").read_text(encoding="utf-8") + compose = Path("deploy/compose/compose.yaml").read_text(encoding="utf-8") + assert "StructuredAuditSink()" in app + assert 'os.environ["OPENRATH_GRANTS"]' in app + assert 'frozenset({"*"})' not in app + assert "OPENRATH_GRANTS:" in compose def test_production_workflow_pins_actions_and_service_images() -> None: @@ -39,3 +50,19 @@ def test_production_workflow_pins_actions_and_service_images() -> None: "aquasec/trivy@sha256:" "e2b22eac59c02003d8749f5b8d9bd073b62e30fefaef5b7c8371204e0a4b0c08" in workflow ) + + +def test_live_release_workflows_fail_closed() -> None: + openviking = Path(".github/workflows/ci-test-openviking.yml").read_text( + encoding="utf-8" + ) + provider = Path(".github/workflows/ci-live-provider.yml").read_text( + encoding="utf-8" + ) + assert "pytest (openviking contracts)" in openviking + assert "continue-on-error:" not in openviking + assert "Require live credentials for release validation" in openviking + assert "exit 1" in openviking + assert "pytest (live provider required)" in provider + assert "An approved live provider credential is required." in provider + assert "uv run pytest -q -m live_llm" in provider diff --git a/tests/deployment/test_release_version.py b/tests/deployment/test_release_version.py new file mode 100644 index 0000000..53f2489 --- /dev/null +++ b/tests/deployment/test_release_version.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path + +RC_VERSION = "2.0.0rc1" + + +def test_release_candidate_version_surfaces_are_consistent() -> None: + pyproject = Path("pyproject.toml").read_text(encoding="utf-8") + match = re.search(r'^version = "([^"]+)"$', pyproject, flags=re.MULTILINE) + assert match is not None + assert match.group(1) == RC_VERSION + + openapi = json.loads( + Path("deploy/docs/openapi-v2.json").read_text(encoding="utf-8") + ) + assert openapi["info"]["version"] == RC_VERSION + + compose = Path("deploy/compose/compose.yaml").read_text(encoding="utf-8") + kubernetes = Path("deploy/kubernetes/openrath.yaml").read_text(encoding="utf-8") + assert compose.count(f"openrath:{RC_VERSION}") == 3 + assert kubernetes.count(f"openrath:{RC_VERSION}") == 3 + + dockerfile = Path("docker/Dockerfile").read_text(encoding="utf-8") + assert 'org.opencontainers.image.version="${OPENRATH_VERSION}"' in dockerfile + assert 'org.opencontainers.image.revision="${OPENRATH_REVISION}"' in dockerfile + + assert Path(f"release/notes/v{RC_VERSION}.md").is_file() + + +def test_release_candidate_workflow_is_digest_and_evidence_bound() -> None: + workflow = Path(".github/workflows/release-v2-rc.yml").read_text(encoding="utf-8") + assert "packages: write" in workflow + assert "attestations: write" in workflow + assert "push: true" in workflow + assert "steps.image.outputs.digest" in workflow + assert "scripts/release/build_evidence.py" in workflow + assert "scripts/release/verify_evidence.py" in workflow + assert "--prerelease" in workflow + assert "PyPI" not in workflow diff --git a/tests/security/test_secrets_audit.py b/tests/security/test_secrets_audit.py index b3a6c1b..32ab1f8 100644 --- a/tests/security/test_secrets_audit.py +++ b/tests/security/test_secrets_audit.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json from uuid import uuid4 from rath.context import RunContext @@ -14,6 +15,7 @@ ResolvedSecret, ResourceRef, SecretRef, + StructuredAuditSink, ) @@ -52,3 +54,36 @@ async def exercise() -> None: assert event.tenant_id == "local" asyncio.run(exercise()) + + +def test_structured_audit_sink_emits_redacted_correlated_json() -> None: + async def exercise() -> None: + context = RunContext.local(revision_id=uuid4()) + event = AuditEvent.for_policy_decision( + kind=AuditKind.POLICY_DECISION, + action=Action("provider.invoke"), + resource=ResourceRef(kind="provider", id="openai-main"), + context=context, + decision=PolicyDecision( + effect=PolicyEffect.ALLOW, + reason="approved", + policy_id="release-policy", + ), + attributes={ + "authorization": "Bearer super-secret-value", + "secret_ref": "env:OPENAI_API_KEY", + }, + ) + lines: list[str] = [] + await StructuredAuditSink(lines.append).emit(event) + + assert len(lines) == 1 + assert "super-secret-value" not in lines[0] + record = json.loads(lines[0]) + assert record["schema"] == "openrath.security-audit/1" + assert record["request_id"] == str(context.request_id) + assert record["trace_id"] == context.trace_context.trace_id + assert record["action"] == "provider.invoke" + assert record["attributes"]["authorization"] == "" + + asyncio.run(exercise()) diff --git a/tests/server/test_openapi_contract.py b/tests/server/test_openapi_contract.py index e9414ad..11d52af 100644 --- a/tests/server/test_openapi_contract.py +++ b/tests/server/test_openapi_contract.py @@ -11,13 +11,13 @@ def test_committed_openapi_matches_generator() -> None: Path("deploy/docs/openapi-v2.json").read_text(encoding="utf-8") ) assert committed == _openapi_document( - "2.0.0-unreleased", + "2.0.0rc1", store_enabled=True, ) def test_openapi_documents_security_actions_and_schemas() -> None: - document = _openapi_document("2.0.0-unreleased", store_enabled=True) + document = _openapi_document("2.0.0rc1", store_enabled=True) paths = document["paths"] assert paths["/v1/runs"]["post"]["x-openrath-action"] == "run.create" assert paths["/metrics"]["get"]["security"] == [{"bearerAuth": []}] diff --git a/uv.lock b/uv.lock index 3f9dd72..a133e5c 100644 --- a/uv.lock +++ b/uv.lock @@ -1994,7 +1994,7 @@ wheels = [ [[package]] name = "openrath" -version = "1.3.0" +version = "2.0.0rc1" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 6956d2424a0c54252bbddfbcb53dfac44600d375 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Thu, 30 Jul 2026 10:00:13 +0800 Subject: [PATCH 19/22] release: add v2 GA evidence and publication gates --- .github/workflows/ci-v2-production.yml | 10 + .github/workflows/release-v2-ga.yml | 366 ++++++++++++++++++ .github/workflows/release-v2-rc.yml | 8 + release/checklists/v2.0.0-ga.md | 43 ++ release/checklists/v2.0.0-rc.md | 23 +- .../evidence/schema/ga-approval.schema.json | 101 +++++ .../schema/ga-gate-report.schema.json | 62 +++ release/evidence/schema/manifest.schema.json | 155 +++++++- release/notes/v2.0.0.md | 47 +++ review/v2.0.0/README.md | 17 +- review/v2.0.0/evidence.json | 14 +- review/v2.0.0/release-approval.md | 16 + scripts/release/build_evidence.py | 230 ++++++++++- scripts/release/create_ga_approval.py | 131 +++++++ scripts/release/verify_evidence.py | 125 +++++- scripts/release/verify_gate_reports.py | 156 ++++++++ scripts/release/verify_pypi_files.py | 119 ++++++ tests/deployment/test_ga_gate_reports.py | 115 ++++++ tests/deployment/test_pypi_recovery.py | 66 ++++ tests/deployment/test_release_evidence.py | 183 +++++++++ tests/deployment/test_release_version.py | 34 ++ 21 files changed, 1976 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/release-v2-ga.yml create mode 100644 release/checklists/v2.0.0-ga.md create mode 100644 release/evidence/schema/ga-approval.schema.json create mode 100644 release/evidence/schema/ga-gate-report.schema.json create mode 100644 release/notes/v2.0.0.md create mode 100644 scripts/release/create_ga_approval.py create mode 100644 scripts/release/verify_gate_reports.py create mode 100644 scripts/release/verify_pypi_files.py create mode 100644 tests/deployment/test_ga_gate_reports.py create mode 100644 tests/deployment/test_pypi_recovery.py create mode 100644 tests/deployment/test_release_evidence.py diff --git a/.github/workflows/ci-v2-production.yml b/.github/workflows/ci-v2-production.yml index 64cb149..8c43e75 100644 --- a/.github/workflows/ci-v2-production.yml +++ b/.github/workflows/ci-v2-production.yml @@ -35,6 +35,8 @@ jobs: OPENRATH_TEST_S3_SECRET_KEY: openrath-test-secret steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 - name: Start S3-compatible object store run: >- @@ -78,6 +80,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: @@ -114,4 +118,10 @@ jobs: --scanners secret \ --exit-code 1 \ --skip-dirs /workspace/.git \ + --skip-dirs /workspace/.venv \ + --skip-dirs /workspace/.mypy_cache \ + --skip-dirs /workspace/.pytest_cache \ + --skip-dirs /workspace/.ruff_cache \ + --skip-dirs /workspace/build \ + --skip-dirs /workspace/dist \ /workspace diff --git a/.github/workflows/release-v2-ga.yml b/.github/workflows/release-v2-ga.yml new file mode 100644 index 0000000..65bd399 --- /dev/null +++ b/.github/workflows/release-v2-ga.yml @@ -0,0 +1,366 @@ +name: Publish v2.0.0 GA + +on: + workflow_dispatch: + inputs: + tag: + description: Existing annotated GA tag + required: true + type: string + evidence_run_id: + description: Successful run containing openrath-v2.0.0-ga-input + required: true + type: string + confirmation: + description: Type "publish v2.0.0" to authorize this run + required: true + type: string + +concurrency: + group: publish-openrath-v2.0.0 + cancel-in-progress: false + +permissions: {} + +jobs: + publish-ga: + name: Verify evidence, publish OCI, and attest + runs-on: ubuntu-latest + environment: + name: ga-release + url: https://github.com/${{ github.repository }}/releases/tag/v2.0.0 + permissions: + actions: read + attestations: write + contents: read + id-token: write + packages: write + outputs: + image_digest: ${{ steps.push_image.outputs.digest }} + env: + RELEASE_TAG: ${{ inputs.tag }} + EVIDENCE_RUN_ID: ${{ inputs.evidence_run_id }} + IMAGE: ghcr.io/rath-team/openrath + TRIVY_IMAGE: aquasec/trivy@sha256:e2b22eac59c02003d8749f5b8d9bd073b62e30fefaef5b7c8371204e0a4b0c08 + VERSION: 2.0.0 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 + with: + python-version: '3.12' + enable-cache: false + - name: Validate dispatch, tag, version, and main ancestry + env: + RELEASE_CONFIRMATION: ${{ inputs.confirmation }} + run: | + test "$GITHUB_REF" = "refs/heads/main" + test "$RELEASE_CONFIRMATION" = "publish v2.0.0" + test "$RELEASE_TAG" = "v2.0.0" + test "$(git cat-file -t "$RELEASE_TAG")" = "tag" + test "$(git rev-parse "$RELEASE_TAG^{}")" = "$(git rev-parse HEAD)" + git fetch origin main + git merge-base --is-ancestor HEAD origin/main + project_version="$(python - <<'PY' + import re + from pathlib import Path + text = Path("pyproject.toml").read_text(encoding="utf-8") + print(re.search(r'^version = "([^"]+)"$', text, re.MULTILINE).group(1)) + PY + )" + test "$project_version" = "$VERSION" + test -f "release/notes/v${VERSION}.md" + ! grep -Eq 'TODO|HOLD|DRAFT' "release/notes/v${VERSION}.md" + test -z "$(git status --porcelain --untracked-files=no)" + echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + - name: Download and verify same-SHA Gate C evidence + env: + GH_TOKEN: ${{ github.token }} + run: | + [[ "$EVIDENCE_RUN_ID" =~ ^[1-9][0-9]*$ ]] + run_json="$(gh run view "$EVIDENCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --json headSha,conclusion)" + test "$(jq -r .conclusion <<<"$run_json")" = "success" + test "$(jq -r .headSha <<<"$run_json")" = "$SOURCE_SHA" + gate_dir="release/evidence/$VERSION/gates" + mkdir -p "$gate_dir" + gh run download "$EVIDENCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name openrath-v2.0.0-ga-input \ + --dir "$gate_dir" + uv run python scripts/release/verify_gate_reports.py \ + "$gate_dir" \ + --source-commit "$SOURCE_SHA" + - name: Record protected-environment approval + env: + GH_TOKEN: ${{ github.token }} + run: | + review_history="$RUNNER_TEMP/environment-approvals.json" + gh api \ + "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/approvals" \ + > "$review_history" + uv run python scripts/release/create_ga_approval.py \ + --version "$VERSION" \ + --source-commit "$SOURCE_SHA" \ + --requested-by "$GITHUB_ACTOR" \ + --repository "$GITHUB_REPOSITORY" \ + --workflow-run-id "$GITHUB_RUN_ID" \ + --review-history "$review_history" \ + --output release/evidence/"$VERSION"/ga-approval.json + - name: Verify source and package gates + run: | + uv lock --check + uv sync --frozen --all-extras --all-groups + uv run ruff format --check src tests example + uv run ruff check src tests scripts examples + uv run mypy --no-incremental src/rath + uv run pytest -q -n auto \ + -m "not live_llm and not opensandbox and not openviking" + uv run python scripts/export_openapi_v2.py \ + --output "$RUNNER_TEMP/openapi-v2.json" + diff -u deploy/docs/openapi-v2.json "$RUNNER_TEMP/openapi-v2.json" + uv build + uvx twine check dist/* + - name: Audit exact dependency sets + run: | + uv export --frozen --no-dev --no-emit-project \ + --extra server --extra postgres --extra s3 --extra redis --extra otel \ + --output-file production-requirements.txt + uvx pip-audit --no-deps --disable-pip \ + --format json \ + --output release/evidence/"$VERSION"/dependency-audit-production.json \ + -r production-requirements.txt + uv export --frozen --all-extras --all-groups --no-emit-project \ + --output-file all-requirements.txt + uvx pip-audit --no-deps --disable-pip \ + --format json \ + --output release/evidence/"$VERSION"/dependency-audit-all.json \ + -r all-requirements.txt + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Build the final image once + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: docker/Dockerfile + load: true + push: false + tags: | + ${{ env.IMAGE }}:${{ env.VERSION }} + ${{ env.IMAGE }}:${{ env.SOURCE_SHA }} + build-args: | + OPENRATH_VERSION=${{ env.VERSION }} + OPENRATH_REVISION=${{ env.SOURCE_SHA }} + - name: Scan the final image and source + run: | + evidence_dir="release/evidence/$VERSION" + mkdir -p "$RUNNER_TEMP/trivy-cache" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + -v "$PWD:/workspace" \ + "$TRIVY_IMAGE" image \ + --severity CRITICAL,HIGH \ + --exit-code 1 \ + --ignore-unfixed \ + --format json \ + --output "/workspace/$evidence_dir/image-scan.json" \ + "$IMAGE:$VERSION" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + -v "$PWD:/workspace" \ + "$TRIVY_IMAGE" image \ + --format cyclonedx \ + --output "/workspace/$evidence_dir/sbom.cdx.json" \ + "$IMAGE:$VERSION" + docker run --rm \ + -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ + -v "$PWD:/workspace:ro" \ + -v "$PWD/$evidence_dir:/evidence" \ + "$TRIVY_IMAGE" fs \ + --scanners secret \ + --exit-code 1 \ + --format json \ + --output /evidence/secret-scan.json \ + --skip-dirs /workspace/.git \ + --skip-dirs /workspace/.venv \ + --skip-dirs /workspace/.mypy_cache \ + --skip-dirs /workspace/.pytest_cache \ + --skip-dirs /workspace/.ruff_cache \ + --skip-dirs /workspace/build \ + --skip-dirs /workspace/dist \ + /workspace + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Push the already-scanned image + id: push_image + run: | + push_output="$(docker push "$IMAGE:$VERSION")" + printf '%s\n' "$push_output" + digest="$(sed -n \ + 's/^.*digest: \(sha256:[0-9a-f]\{64\}\).*$/\1/p' \ + <<<"$push_output" | tail -1)" + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] + docker push "$IMAGE:$SOURCE_SHA" + echo "digest=$digest" >> "$GITHUB_OUTPUT" + - name: Build and verify final SHA-bound evidence + env: + IMAGE_DIGEST: ${{ steps.push_image.outputs.digest }} + run: | + evidence_dir="release/evidence/$VERSION" + sed "s#${IMAGE}:${VERSION}#${IMAGE}@${IMAGE_DIGEST}#g" \ + deploy/kubernetes/openrath.yaml \ + > "$evidence_dir/openrath-kubernetes.yaml" + uv run python scripts/release/build_evidence.py \ + --stage ga \ + --tag "$RELEASE_TAG" \ + --approval "$evidence_dir/ga-approval.json" \ + --image-ref "$IMAGE" \ + --image-digest "$IMAGE_DIGEST" \ + --output "$evidence_dir/manifest.json" \ + --artifact sbom="$evidence_dir/sbom.cdx.json" \ + --artifact image_scan="$evidence_dir/image-scan.json" \ + --artifact secret_scan="$evidence_dir/secret-scan.json" \ + --artifact dependency_audit_production="$evidence_dir/dependency-audit-production.json" \ + --artifact dependency_audit_all="$evidence_dir/dependency-audit-all.json" \ + --artifact kubernetes="$evidence_dir/openrath-kubernetes.yaml" \ + --artifact tests="$evidence_dir/gates/tests.json" \ + --artifact live_adapters="$evidence_dir/gates/live-adapters.json" \ + --artifact performance="$evidence_dir/gates/performance.json" \ + --artifact soak="$evidence_dir/gates/soak.json" \ + --artifact drills="$evidence_dir/gates/drills.json" \ + --artifact compatibility="$evidence_dir/gates/compatibility.json" + uv run python scripts/release/verify_evidence.py \ + "$evidence_dir/manifest.json" + - name: Attest the final OCI digest + uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.push_image.outputs.digest }} + push-to-registry: true + - name: Upload the final release bundle + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: openrath-2.0.0-release + retention-days: 90 + if-no-files-found: error + path: | + dist/* + release/evidence/2.0.0/* + scripts/release/verify_pypi_files.py + + publish-pypi: + name: Publish distributions to PyPI + needs: publish-ga + runs-on: ubuntu-latest + environment: + name: ga-release + url: https://pypi.org/p/openrath + permissions: + id-token: write + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: openrath-2.0.0-release + path: release-bundle + - name: Reject conflicting existing PyPI files + run: | + python release-bundle/scripts/release/verify_pypi_files.py \ + --packages-dir release-bundle/dist \ + --version 2.0.0 + - name: Publish package distributions with attestations + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 + with: + packages-dir: release-bundle/dist + print-hash: true + skip-existing: true + + publish-github: + name: Create the final GitHub Release + needs: publish-pypi + runs-on: ubuntu-latest + permissions: + contents: write + env: + RELEASE_TAG: v2.0.0 + VERSION: 2.0.0 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ env.RELEASE_TAG }} + persist-credentials: false + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: openrath-2.0.0-release + path: release-bundle + - name: Create the immutable final release + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + echo "Release $RELEASE_TAG already exists; refusing to overwrite it." >&2 + exit 1 + fi + gh release create "$RELEASE_TAG" \ + release-bundle/dist/* \ + release-bundle/release/evidence/"$VERSION"/*.json \ + release-bundle/release/evidence/"$VERSION"/*.yaml \ + release-bundle/release/evidence/"$VERSION"/gates/*.json \ + --verify-tag \ + --title "OpenRath $VERSION" \ + --notes-file "release/notes/v${VERSION}.md" + + verify-publication: + name: Verify public GA artifacts + needs: [publish-ga, publish-github] + runs-on: ubuntu-latest + permissions: + contents: read + env: + IMAGE: ghcr.io/rath-team/openrath + IMAGE_DIGEST: ${{ needs.publish-ga.outputs.image_digest }} + VERSION: 2.0.0 + steps: + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 + with: + python-version: '3.12' + enable-cache: false + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: openrath-2.0.0-release + path: release-bundle + - name: Verify published PyPI files match the release bundle + run: | + python release-bundle/scripts/release/verify_pypi_files.py \ + --packages-dir release-bundle/dist \ + --version "$VERSION" \ + --require-complete \ + --attempts 12 \ + --delay-seconds 10 + - name: Verify a fresh PyPI installation + run: | + uv venv "$RUNNER_TEMP/verify" + uv pip install \ + --python "$RUNNER_TEMP/verify/bin/python" \ + --no-cache \ + "openrath==$VERSION" + "$RUNNER_TEMP/verify/bin/python" -c \ + "import importlib.metadata; assert importlib.metadata.version('openrath') == '$VERSION'" + - name: Verify anonymous OCI access and provenance + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir "$RUNNER_TEMP/docker-anonymous" + DOCKER_CONFIG="$RUNNER_TEMP/docker-anonymous" \ + docker manifest inspect "$IMAGE@$IMAGE_DIGEST" >/dev/null + gh attestation verify \ + "oci://$IMAGE@$IMAGE_DIGEST" \ + --repo "$GITHUB_REPOSITORY" + gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" >/dev/null diff --git a/.github/workflows/release-v2-rc.yml b/.github/workflows/release-v2-rc.yml index b109cc8..c3509e7 100644 --- a/.github/workflows/release-v2-rc.yml +++ b/.github/workflows/release-v2-rc.yml @@ -31,9 +31,11 @@ jobs: with: ref: ${{ env.RELEASE_TAG }} fetch-depth: 0 + persist-credentials: false - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 with: python-version: '3.12' + enable-cache: false - name: Validate tag and project version run: | version="${RELEASE_TAG#v}" @@ -124,6 +126,12 @@ jobs: --format json \ --output /evidence/secret-scan.json \ --skip-dirs /workspace/.git \ + --skip-dirs /workspace/.venv \ + --skip-dirs /workspace/.mypy_cache \ + --skip-dirs /workspace/.pytest_cache \ + --skip-dirs /workspace/.ruff_cache \ + --skip-dirs /workspace/build \ + --skip-dirs /workspace/dist \ /workspace cp dependency-audit-production.json release/evidence/"$VERSION"/ cp dependency-audit-all.json release/evidence/"$VERSION"/ diff --git a/release/checklists/v2.0.0-ga.md b/release/checklists/v2.0.0-ga.md new file mode 100644 index 0000000..c4b4038 --- /dev/null +++ b/release/checklists/v2.0.0-ga.md @@ -0,0 +1,43 @@ +# OpenRath v2.0.0 GA checklist + +Candidate: `v2.0.0` + +No item in this checklist is satisfied by the `v2.0.0rc1` publication alone. +Every machine-readable report must identify the exact final source commit. + +## Gate C evidence + +- [ ] Required CI report has zero open P0 findings. +- [ ] Live Provider, OpenSandbox, and OpenViking lifecycles pass without skips. +- [ ] Single-host and split-profile capacity reports pass. +- [ ] One-to-four worker scaling efficiency is at least 70%. +- [ ] Target-like soak runs for at least 28,800 seconds with zero errors and no + unexplained resource growth. +- [ ] PostgreSQL, Redis, S3, API, and worker fault matrix passes. +- [ ] Target-cluster backup/restore and rollout/rollback drills pass. +- [ ] API stability, v1 maintenance window, and migration review pass. +- [ ] The evidence workflow run is successful and uses the final source commit. + +## Release governance + +- [ ] PR review is complete with no unresolved P0/P1 thread. +- [ ] `main` protection and required checks are active. +- [ ] The `ga-release` environment has required reviewers. +- [ ] PyPI Trusted Publishing authorizes + `.github/workflows/release-v2-ga.yml` in environment `ga-release`. +- [ ] The annotated `v2.0.0` tag points to a commit reachable from `main`. +- [ ] The owner explicitly approves merge, version bump, tag, PyPI, GHCR, and + GitHub Release actions. + +## Publication and external verification + +- [ ] Wheel, sdist, OpenAPI, OCI labels, tag, and evidence report `2.0.0`. +- [ ] Production and all-extras dependency audits pass. +- [ ] Image and repository secret scans pass. +- [ ] Final SBOM, immutable image digest, and evidence manifest are attached. +- [ ] OCI provenance attestation verifies by digest. +- [ ] PyPI Trusted Publishing and PyPI attestations succeed. +- [ ] Fresh Python installation from PyPI reports `2.0.0`. +- [ ] Anonymous GHCR pull by digest succeeds. +- [ ] GitHub Release is final, not a prerelease. +- [ ] Post-release smoke and 24h/72h observation owners are active. diff --git a/release/checklists/v2.0.0-rc.md b/release/checklists/v2.0.0-rc.md index 056aae4..3dffa3b 100644 --- a/release/checklists/v2.0.0-rc.md +++ b/release/checklists/v2.0.0-rc.md @@ -4,14 +4,27 @@ Candidate: `v2.0.0rc1` ## RC artifact publication -- [ ] Package, OpenAPI, image label, tag, and release title report `2.0.0rc1`. -- [ ] Lock, Ruff, mypy, offline tests, integration tests, OpenSandbox, build, +- [x] Package, OpenAPI, image label, tag, and release title report `2.0.0rc1`. +- [x] Lock, Ruff, mypy, offline tests, integration tests, OpenSandbox, build, dependency audits, image scan, secret scan, and manifest validation pass. -- [ ] Wheel, sdist, SBOM, scan reports, manifest, and immutable image digest are +- [x] Wheel, sdist, SBOM, scan reports, manifest, and immutable image digest are attached to the GitHub prerelease. -- [ ] Release notes state that live provider/OpenViking and target-environment +- [x] Release notes state that live provider/OpenViking and target-environment Gate C remain pending. -- [ ] No PyPI publication or shared deployment occurs as part of RC1. +- [x] No PyPI publication or shared deployment occurs as part of RC1. + +Publication evidence: + +- Source commit: + `feedcaadb79a349aa60c034618610231d83fb131`. +- Release workflow: + . +- GitHub prerelease: + . +- OCI artifact: + `ghcr.io/rath-team/openrath@sha256:d66a1c7933e410d290528ba5091f73a9f4e94566e1bd9d917f7fd7937c6b9be2`. +- Anonymous manifest retrieval and GitHub attestation verification passed on + 2026-07-30. ## GA-blocking acceptance after RC publication diff --git a/release/evidence/schema/ga-approval.schema.json b/release/evidence/schema/ga-approval.schema.json new file mode 100644 index 0000000..a160b7f --- /dev/null +++ b/release/evidence/schema/ga-approval.schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openrath.dev/schemas/ga-approval-v1.json", + "title": "OpenRath GA approval", + "type": "object", + "required": [ + "schema", + "version", + "source_commit", + "approved", + "approved_at", + "approvers", + "requested_by", + "environment", + "environment_reviews", + "repository", + "workflow_run_id", + "actions" + ], + "properties": { + "schema": { + "const": "openrath.ga-approval/1" + }, + "version": { + "const": "2.0.0" + }, + "source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "approved": { + "const": true + }, + "approved_at": { + "type": "string", + "format": "date-time" + }, + "approvers": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "requested_by": { + "type": "string", + "minLength": 1 + }, + "environment": { + "const": "ga-release" + }, + "environment_reviews": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["review_id", "reviewer", "approved_at"], + "properties": { + "review_id": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "reviewer": { + "type": "string", + "minLength": 1 + }, + "approved_at": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + } + }, + "repository": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "workflow_run_id": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "actions": { + "type": "object", + "required": [ + "pypi", + "ghcr", + "github_release" + ], + "properties": { + "pypi": {"const": true}, + "ghcr": {"const": true}, + "github_release": {"const": true} + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/release/evidence/schema/ga-gate-report.schema.json b/release/evidence/schema/ga-gate-report.schema.json new file mode 100644 index 0000000..51f6f0b --- /dev/null +++ b/release/evidence/schema/ga-gate-report.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openrath.dev/schemas/ga-gate-report-v1.json", + "title": "OpenRath GA gate report", + "type": "object", + "required": [ + "schema", + "gate", + "source_commit", + "result", + "generated_at", + "environment", + "evidence", + "open_risks", + "details" + ], + "properties": { + "schema": { + "const": "openrath.ga-gate-report/1" + }, + "gate": { + "enum": [ + "tests", + "live_adapters", + "performance", + "soak", + "drills", + "compatibility" + ] + }, + "source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "result": { + "const": "passed" + }, + "generated_at": { + "type": "string", + "format": "date-time" + }, + "environment": { + "type": "object", + "minProperties": 1 + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "open_risks": { + "type": "array" + }, + "details": { + "type": "object" + } + }, + "additionalProperties": false +} diff --git a/release/evidence/schema/manifest.schema.json b/release/evidence/schema/manifest.schema.json index 48e6056..45fdb86 100644 --- a/release/evidence/schema/manifest.schema.json +++ b/release/evidence/schema/manifest.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://openrath.dev/schemas/rc-evidence-manifest.json", - "title": "OpenRath RC evidence manifest", + "$id": "https://openrath.dev/schemas/release-evidence-manifest-v2.json", + "title": "OpenRath release evidence manifest v2", "type": "object", "required": [ "schema", @@ -15,19 +15,150 @@ "blocking_gates" ], "properties": { - "schema": {"const": "openrath.rc-evidence/1"}, - "release_stage": {"const": "rc"}, - "version": {"type": "string", "pattern": "^2\\.0\\.0rc[0-9]+$"}, - "tag": {"type": "string", "pattern": "^v2\\.0\\.0rc[0-9]+$"}, - "source_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, - "source_tree_clean": {"const": true}, - "ga_approved": {"const": false}, - "artifacts": {"type": "object"}, + "schema": { + "const": "openrath.release-evidence/2" + }, + "release_stage": { + "enum": ["rc", "ga"] + }, + "version": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "source_tree_clean": { + "const": true + }, + "ga_approved": { + "type": "boolean" + }, + "artifacts": { + "type": "object" + }, "blocking_gates": { "type": "array", - "minItems": 1, - "items": {"type": "string"} + "items": { + "type": "string" + } + }, + "approval": { + "type": "object", + "required": [ + "approvers", + "requested_by", + "approved_at", + "environment", + "workflow_run_id", + "source_commit", + "actions" + ], + "properties": { + "approvers": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "requested_by": { + "type": "string", + "minLength": 1 + }, + "approved_at": { + "type": "string", + "format": "date-time" + }, + "environment": { + "const": "ga-release" + }, + "workflow_run_id": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "actions": { + "type": "object", + "required": [ + "pypi", + "ghcr", + "github_release" + ], + "properties": { + "pypi": {"const": true}, + "ghcr": {"const": true}, + "github_release": {"const": true} + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, + "oneOf": [ + { + "title": "Release candidate", + "properties": { + "release_stage": {"const": "rc"}, + "version": { + "type": "string", + "pattern": "^2\\.0\\.0rc[1-9][0-9]*$" + }, + "tag": { + "type": "string", + "pattern": "^v2\\.0\\.0rc[1-9][0-9]*$" + }, + "ga_approved": {"const": false}, + "blocking_gates": { + "type": "array", + "minItems": 1 + } + }, + "not": { + "required": ["approval"] + } + }, + { + "title": "General availability", + "required": ["approval"], + "properties": { + "release_stage": {"const": "ga"}, + "version": {"const": "2.0.0"}, + "tag": {"const": "v2.0.0"}, + "ga_approved": {"const": true}, + "blocking_gates": { + "type": "array", + "maxItems": 0 + }, + "artifacts": { + "type": "object", + "required": [ + "approval", + "tests", + "live_adapters", + "performance", + "soak", + "drills", + "compatibility", + "sbom", + "image_scan", + "secret_scan", + "dependency_audit_production", + "dependency_audit_all", + "kubernetes" + ] + } + } + } + ], "additionalProperties": true } diff --git a/release/notes/v2.0.0.md b/release/notes/v2.0.0.md new file mode 100644 index 0000000..5847b2b --- /dev/null +++ b/release/notes/v2.0.0.md @@ -0,0 +1,47 @@ +# OpenRath v2.0.0 + +OpenRath 2.0.0 introduces a durable multi-agent Runtime and Agent Server while +preserving the supported Python v1 facade for the documented maintenance +window. + +## Highlights + +- Explicit `@step` and `@router` compilation with canonical plans and immutable + revision identity. +- Durable Runs, Events, Checkpoints, Interrupts, cancellation, deadlines, + retries, lease fencing, and effect reconciliation. +- PostgreSQL production state, optional Redis signaling, and S3-compatible + artifact storage. +- Tenant/project-scoped HTTP and SSE APIs with explicit grants, bounded + resources, security headers, and redacted structured audit events. +- Governed Provider, Tool/MCP, Sandbox, and Memory adapter boundaries. +- OpenTelemetry integration, evaluation resources, migration tooling, and + split API/worker deployment references. + +## Compatibility and migration + +- Existing v1 JSONL Sessions import as non-resumable historical Runs. +- Database migrations are additive during the rollback window. +- Operators should inventory v1 data, back up PostgreSQL and artifacts, run the + migration dry-run, and restore-test backups before switching traffic. +- See `deploy/docs/migration-v2.md`, `deploy/docs/operations-v2.md`, and + `deploy/docs/api-governance-v2.md`. + +## Security and operational boundary + +- Authentication never grants access by itself; deployments must provide + explicit action grants and tenant/project scope. +- Embedded mode trusts the local process and is not a multi-tenant isolation + boundary. +- Exactly-once execution is not promised for arbitrary external side effects; + ambiguous non-idempotent outcomes stop in `NEEDS_REVIEW`. +- Production operators must supply their identity provider, TLS ingress, + secret manager, audit collector, retention controls, and restricted egress. +- See `deploy/docs/threat-model-v2.md` and + `deploy/docs/known-limitations-v2.md`. + +## Distribution and provenance + +The release publishes wheel and source distributions to PyPI, an immutable OCI +image to GHCR, and a GitHub Release containing the SBOM, scans, rendered +Kubernetes manifest, Gate C summaries, and SHA-bound evidence manifest. diff --git a/review/v2.0.0/README.md b/review/v2.0.0/README.md index abfbbfb..479d799 100644 --- a/review/v2.0.0/README.md +++ b/review/v2.0.0/README.md @@ -1,11 +1,18 @@ # OpenRath v2.0.0 review candidate -Status: **implementation complete; release on hold for user review** +Status: **historical pre-RC evidence; superseded for release decisions** -This directory is the local evidence package for the v2.0.0 implementation. -It is not a release. The package metadata intentionally remains `1.3.0`; no -v2 tag, registry push, GitHub release, or production deployment is permitted -until the owner approves [release-approval.md](release-approval.md). +This directory preserves the local implementation-review snapshot created +before `v2.0.0rc1`. Its `1.3.0` package metadata, local image, test totals, and +`publication=false` fields describe that snapshot only. Do not use them as the +current release status. + +The authoritative RC evidence is the +[`v2.0.0rc1` release manifest](https://github.com/Rath-Team/OpenRath/releases/download/v2.0.0rc1/manifest.json), +bound to commit `feedcaadb79a349aa60c034618610231d83fb131` and the immutable +OCI digest recorded in [release-approval.md](release-approval.md). The GA +decision remains on hold until the unchecked owner and target-environment +gates are complete. ## Implemented production surface diff --git a/review/v2.0.0/evidence.json b/review/v2.0.0/evidence.json index dc3abb5..4ec37d0 100644 --- a/review/v2.0.0/evidence.json +++ b/review/v2.0.0/evidence.json @@ -1,8 +1,19 @@ { "schema": "openrath.v2.review-evidence/1", - "release_state": "hold_for_owner_review", + "release_state": "superseded_pre_rc_snapshot", + "original_release_state": "hold_for_owner_review", + "evidence_scope": "local implementation review before v2.0.0rc1", "package_version": "1.3.0", "review_target": "2.0.0", + "superseded_by": { + "candidate": "v2.0.0rc1", + "source_commit": "feedcaadb79a349aa60c034618610231d83fb131", + "release_url": "https://github.com/Rath-Team/OpenRath/releases/tag/v2.0.0rc1", + "manifest_url": "https://github.com/Rath-Team/OpenRath/releases/download/v2.0.0rc1/manifest.json", + "image": "ghcr.io/rath-team/openrath@sha256:d66a1c7933e410d290528ba5091f73a9f4e94566e1bd9d917f7fd7937c6b9be2", + "anonymous_manifest_verified": "2026-07-30", + "attestation_verified": "2026-07-30" + }, "tests": { "passed": 1024, "skipped": 14, @@ -46,6 +57,7 @@ "invalid_resources": 0 }, "publication": { + "scope": "this historical pre-RC evidence bundle", "pushed": false, "tagged": false, "released": false, diff --git a/review/v2.0.0/release-approval.md b/review/v2.0.0/release-approval.md index 714a5b6..be96590 100644 --- a/review/v2.0.0/release-approval.md +++ b/review/v2.0.0/release-approval.md @@ -19,6 +19,22 @@ The following remain prohibited until separately approved: - publishing the final PyPI/GHCR/GitHub release; - deploying to any shared, staging, or production environment. +## RC publication record + +The authorized RC publication completed from source commit +`feedcaadb79a349aa60c034618610231d83fb131`: + +- release workflow: + ; +- GitHub prerelease: + ; +- OCI artifact: + `ghcr.io/rath-team/openrath@sha256:d66a1c7933e410d290528ba5091f73a9f4e94566e1bd9d917f7fd7937c6b9be2`. + +Anonymous OCI manifest retrieval and GitHub attestation verification passed on +2026-07-30. This record closes RC artifact publication only; it does not change +the GA hold or approve a shared deployment. + ## Owner review checklist - [ ] Review public SDK/API compatibility and the stable error model. diff --git a/scripts/release/build_evidence.py b/scripts/release/build_evidence.py index bfc7be8..be5bdf5 100644 --- a/scripts/release/build_evidence.py +++ b/scripts/release/build_evidence.py @@ -1,4 +1,4 @@ -"""Build a SHA-bound OpenRath release-candidate evidence manifest.""" +"""Build a SHA-bound OpenRath RC or GA evidence manifest.""" from __future__ import annotations @@ -11,6 +11,169 @@ from datetime import datetime, timezone from pathlib import Path +RELEASE_SCHEMA = "openrath.release-evidence/2" +RC_BLOCKING_GATES = ( + "approved live LLM/provider lifecycle", + "approved live OpenViking lifecycle", + "target-like capacity and one-to-four worker scaling", + "eight-hour target-like soak", + "target-cluster backup/restore and rollout/rollback drills", + "final API stability, v1 maintenance window, and GA owner approval", +) +GA_RELEASE_ACTIONS = frozenset( + { + "pypi", + "ghcr", + "github_release", + } +) +GA_REQUIRED_ARTIFACTS = frozenset( + { + "approval", + "tests", + "live_adapters", + "performance", + "soak", + "drills", + "compatibility", + "sbom", + "image_scan", + "secret_scan", + "dependency_audit_production", + "dependency_audit_all", + "kubernetes", + } +) + + +def infer_release_stage(version: str) -> str: + """Return the only supported release stage for a v2.0.0 version.""" + if re.fullmatch(r"2\.0\.0rc[1-9][0-9]*", version): + return "rc" + if version == "2.0.0": + return "ga" + raise ValueError(f"unsupported release version: {version!r}") + + +def load_ga_approval( + path: Path, + *, + version: str, + commit: str, + repository: str | None = None, + workflow_run_id: str | None = None, +) -> dict[str, object]: + """Load and validate an explicit owner approval bound to the GA SHA.""" + approval = json.loads(path.read_text(encoding="utf-8")) + if approval.get("schema") != "openrath.ga-approval/1": + raise ValueError("GA approval schema must be openrath.ga-approval/1") + if approval.get("version") != version: + raise ValueError("GA approval version does not match the release") + if approval.get("source_commit") != commit: + raise ValueError("GA approval source_commit does not match HEAD") + if approval.get("approved") is not True: + raise ValueError("GA approval must set approved=true") + if approval.get("environment") != "ga-release": + raise ValueError("GA approval environment must be ga-release") + approval_repository = approval.get("repository") + if ( + not isinstance(approval_repository, str) + or re.fullmatch(r"[^/\s]+/[^/\s]+", approval_repository) is None + ): + raise ValueError("GA approval repository must be owner/name") + if repository is not None and approval.get("repository") != repository: + raise ValueError("GA approval repository does not match the workflow") + approval_run_id = approval.get("workflow_run_id") + if re.fullmatch(r"[1-9][0-9]*", str(approval_run_id or "")) is None: + raise ValueError("GA approval workflow_run_id must be a positive integer") + if ( + workflow_run_id is not None + and approval.get("workflow_run_id") != workflow_run_id + ): + raise ValueError("GA approval workflow_run_id does not match the workflow") + requested_by = approval.get("requested_by") + if not isinstance(requested_by, str) or not requested_by.strip(): + raise ValueError("GA approval requires requested_by") + approvers = approval.get("approvers") + if ( + not isinstance(approvers, list) + or not approvers + or any(not isinstance(item, str) or not item.strip() for item in approvers) + or len(set(approvers)) != len(approvers) + ): + raise ValueError("GA approval requires unique non-empty approvers") + environment_reviews = approval.get("environment_reviews") + if not isinstance(environment_reviews, list) or not environment_reviews: + raise ValueError("GA approval requires environment reviews") + review_approvers: set[str] = set() + review_ids: set[str] = set() + review_times: list[datetime] = [] + for review in environment_reviews: + if not isinstance(review, dict): + raise ValueError("GA approval environment review must be an object") + review_id = str(review.get("review_id", "")) + if re.fullmatch(r"[1-9][0-9]*", review_id) is None: + raise ValueError("GA approval review_id must be a positive integer") + if review_id in review_ids: + raise ValueError("GA approval review ids must be unique") + review_ids.add(review_id) + reviewer = review.get("reviewer") + if not isinstance(reviewer, str) or not reviewer.strip(): + raise ValueError("GA approval environment review requires reviewer") + review_approvers.add(reviewer) + review_approved_at = review.get("approved_at") + if not isinstance(review_approved_at, str): + raise ValueError("GA approval environment review requires approved_at") + try: + parsed_review_time = datetime.fromisoformat( + review_approved_at.replace("Z", "+00:00") + ) + except ValueError as error: + raise ValueError( + "GA approval environment review approved_at must be ISO 8601" + ) from error + if parsed_review_time.tzinfo is None: + raise ValueError( + "GA approval environment review approved_at requires a timezone" + ) + review_times.append(parsed_review_time) + if review_approvers != set(approvers): + raise ValueError("GA approval approvers do not match environment reviews") + approved_at = approval.get("approved_at") + if not isinstance(approved_at, str): + raise ValueError("GA approval requires approved_at") + try: + parsed_approved_at = datetime.fromisoformat(approved_at.replace("Z", "+00:00")) + except ValueError as error: + raise ValueError("GA approval approved_at must be ISO 8601") from error + if parsed_approved_at.tzinfo is None: + raise ValueError("GA approval approved_at must include a timezone") + if parsed_approved_at != max(review_times): + raise ValueError("GA approval approved_at must match the latest review") + actions = approval.get("actions") + if not isinstance(actions, dict): + raise ValueError("GA approval requires release actions") + missing_actions = sorted( + action for action in GA_RELEASE_ACTIONS if actions.get(action) is not True + ) + if missing_actions: + raise ValueError( + "GA approval is missing affirmative actions: " + ", ".join(missing_actions) + ) + unexpected_actions = sorted(set(actions) - GA_RELEASE_ACTIONS) + if unexpected_actions: + raise ValueError( + "GA approval contains unsupported actions: " + ", ".join(unexpected_actions) + ) + return approval + + +def require_ga_artifacts(names: set[str]) -> None: + """Fail unless every GA evidence category is present.""" + missing = sorted(GA_REQUIRED_ARTIFACTS - names) + if missing: + raise ValueError("GA evidence is missing artifacts: " + ", ".join(missing)) + def _sha256(path: Path) -> str: digest = hashlib.sha256() @@ -49,6 +212,8 @@ def _artifact(path: Path) -> dict[str, object]: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--tag", required=True) + parser.add_argument("--stage", choices=("rc", "ga")) + parser.add_argument("--approval", type=Path) parser.add_argument("--image-ref", required=True) parser.add_argument("--image-digest", required=True) parser.add_argument("--output", type=Path, required=True) @@ -64,9 +229,18 @@ def main() -> None: version = _project_version() if args.tag != f"v{version}": raise SystemExit(f"tag {args.tag!r} does not match project version {version!r}") + try: + release_stage = infer_release_stage(version) + except ValueError as error: + raise SystemExit(str(error)) from error + if args.stage is not None and args.stage != release_stage: + raise SystemExit( + f"stage {args.stage!r} does not match release version {version!r}" + ) if re.fullmatch(r"sha256:[0-9a-f]{64}", args.image_digest) is None: raise SystemExit("image digest must be sha256 followed by 64 lowercase hex") + commit = _git("rev-parse", "HEAD") wheel = next(Path("dist").glob(f"openrath-{version}-*.whl")) sdist = Path("dist") / f"openrath-{version}.tar.gz" artifacts: dict[str, object] = { @@ -86,33 +260,63 @@ def main() -> None: raise SystemExit(f"duplicate artifact name: {name}") artifacts[name] = _artifact(Path(raw_path)) - commit = _git("rev-parse", "HEAD") + approval_summary: dict[str, object] | None = None + if release_stage == "ga": + if args.approval is None: + raise SystemExit("GA evidence requires --approval") + try: + approval = load_ga_approval( + args.approval, + version=version, + commit=commit, + repository=os.getenv("GITHUB_REPOSITORY"), + workflow_run_id=os.getenv("GITHUB_RUN_ID"), + ) + except (OSError, ValueError, json.JSONDecodeError) as error: + raise SystemExit(f"invalid GA approval: {error}") from error + if "approval" in artifacts: + raise SystemExit("approval artifact is reserved for --approval") + artifacts["approval"] = _artifact(args.approval) + try: + require_ga_artifacts(set(artifacts)) + except ValueError as error: + raise SystemExit(str(error)) from error + approval_summary = { + "approvers": approval["approvers"], + "requested_by": approval["requested_by"], + "approved_at": approval["approved_at"], + "environment": approval["environment"], + "workflow_run_id": approval["workflow_run_id"], + "source_commit": approval["source_commit"], + "actions": approval["actions"], + } + blocking_gates: list[str] = [] + else: + if args.approval is not None: + raise SystemExit("--approval is only valid for a GA release") + blocking_gates = list(RC_BLOCKING_GATES) + tracked_status = _git("status", "--porcelain", "--untracked-files=no") manifest = { - "schema": "openrath.rc-evidence/1", + "schema": RELEASE_SCHEMA, "generated_at": datetime.now(timezone.utc).isoformat(), - "release_stage": "rc", + "release_stage": release_stage, "version": version, "tag": args.tag, "source_commit": commit, "source_tree_clean": not tracked_status, "base_commit": _git("merge-base", "HEAD", "origin/main"), - "ga_approved": False, + "ga_approved": release_stage == "ga", "workflow": { "repository": os.getenv("GITHUB_REPOSITORY"), "run_id": os.getenv("GITHUB_RUN_ID"), "run_attempt": os.getenv("GITHUB_RUN_ATTEMPT"), }, "artifacts": artifacts, - "blocking_gates": [ - "approved live LLM/provider lifecycle", - "approved live OpenViking lifecycle", - "target-like capacity and one-to-four worker scaling", - "eight-hour target-like soak", - "target-cluster backup/restore and rollout/rollback drills", - "final API stability, v1 maintenance window, and GA owner approval", - ], + "blocking_gates": blocking_gates, } + if approval_summary is not None: + manifest["approval"] = approval_summary args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text( json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n", diff --git a/scripts/release/create_ga_approval.py b/scripts/release/create_ga_approval.py new file mode 100644 index 0000000..93645ad --- /dev/null +++ b/scripts/release/create_ga_approval.py @@ -0,0 +1,131 @@ +"""Create a workflow-bound GA approval record after environment approval.""" + +from __future__ import annotations + +import argparse +import json +import re +from datetime import datetime +from pathlib import Path + +GA_ACTIONS = { + "pypi": True, + "ghcr": True, + "github_release": True, +} +GA_ENVIRONMENT = "ga-release" + + +def _environment_reviews(path: Path) -> list[dict[str, str]]: + """Extract approved GA environment reviews from GitHub's run history.""" + history = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(history, list): + raise ValueError("review history must be a JSON array") + + reviews: list[dict[str, str]] = [] + for review in history: + if not isinstance(review, dict) or review.get("state") != "approved": + continue + environments = review.get("environments") + if not isinstance(environments, list) or not any( + isinstance(environment, dict) and environment.get("name") == GA_ENVIRONMENT + for environment in environments + ): + continue + review_id = review.get("id") + user = review.get("user") + created_at = review.get("created_at") + reviewer = user.get("login") if isinstance(user, dict) else None + if not isinstance(review_id, int) or review_id < 1: + raise ValueError("approved review requires a positive integer id") + if not isinstance(reviewer, str) or not reviewer.strip(): + raise ValueError("approved review requires user.login") + if not isinstance(created_at, str): + raise ValueError("approved review requires created_at") + try: + parsed = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + except ValueError as error: + raise ValueError("approved review created_at must be ISO 8601") from error + if parsed.tzinfo is None: + raise ValueError("approved review created_at must include a timezone") + reviews.append( + { + "review_id": str(review_id), + "reviewer": reviewer, + "approved_at": created_at, + } + ) + + if not reviews: + raise ValueError(f"no approved {GA_ENVIRONMENT} environment review found") + review_ids = [review["review_id"] for review in reviews] + if len(set(review_ids)) != len(review_ids): + raise ValueError("approved environment review ids must be unique") + return sorted(reviews, key=lambda item: (item["approved_at"], item["review_id"])) + + +def build_approval( + *, + version: str, + source_commit: str, + requested_by: str, + repository: str, + workflow_run_id: str, + review_history: Path, +) -> dict[str, object]: + """Build an approval record bound to one protected workflow run.""" + if version != "2.0.0": + raise ValueError("GA approval version must be 2.0.0") + if re.fullmatch(r"[0-9a-f]{40}", source_commit) is None: + raise ValueError("source_commit must be 40 lowercase hexadecimal characters") + if not requested_by.strip(): + raise ValueError("requested_by is required") + if not repository.strip(): + raise ValueError("repository is required") + if re.fullmatch(r"[1-9][0-9]*", workflow_run_id) is None: + raise ValueError("workflow_run_id must be a positive integer") + environment_reviews = _environment_reviews(review_history) + approvers = sorted({review["reviewer"] for review in environment_reviews}) + return { + "schema": "openrath.ga-approval/1", + "version": version, + "source_commit": source_commit, + "approved": True, + "approved_at": environment_reviews[-1]["approved_at"], + "approvers": approvers, + "requested_by": requested_by, + "environment": GA_ENVIRONMENT, + "environment_reviews": environment_reviews, + "repository": repository, + "workflow_run_id": workflow_run_id, + "actions": dict(GA_ACTIONS), + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--source-commit", required=True) + parser.add_argument("--requested-by", required=True) + parser.add_argument("--repository", required=True) + parser.add_argument("--workflow-run-id", required=True) + parser.add_argument("--review-history", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + approval = build_approval( + version=args.version, + source_commit=args.source_commit, + requested_by=args.requested_by, + repository=args.repository, + workflow_run_id=args.workflow_run_id, + review_history=args.review_history, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(approval, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/verify_evidence.py b/scripts/release/verify_evidence.py index 8362499..7ffdcf3 100644 --- a/scripts/release/verify_evidence.py +++ b/scripts/release/verify_evidence.py @@ -1,4 +1,4 @@ -"""Verify an OpenRath release-candidate evidence manifest.""" +"""Verify an OpenRath RC or GA evidence manifest.""" from __future__ import annotations @@ -9,6 +9,98 @@ import subprocess from pathlib import Path +try: + from .build_evidence import load_ga_approval +except ImportError: # pragma: no cover - direct script execution + from build_evidence import load_ga_approval + +LEGACY_RC_SCHEMA = "openrath.rc-evidence/1" +RELEASE_SCHEMA = "openrath.release-evidence/2" +GA_RELEASE_ACTIONS = frozenset( + { + "pypi", + "ghcr", + "github_release", + } +) +GA_REQUIRED_ARTIFACTS = frozenset( + { + "approval", + "tests", + "live_adapters", + "performance", + "soak", + "drills", + "compatibility", + "sbom", + "image_scan", + "secret_scan", + "dependency_audit_production", + "dependency_audit_all", + "kubernetes", + } +) + + +def validate_release_state( + manifest: dict[str, object], + *, + version: str, + commit: str, +) -> None: + """Validate stage, version, approval, and blocker invariants.""" + assert manifest["version"] == version + assert manifest["tag"] == f"v{version}" + assert manifest["source_commit"] == commit + assert manifest["source_tree_clean"] is True + + schema = manifest["schema"] + stage = manifest["release_stage"] + if schema == LEGACY_RC_SCHEMA: + assert stage == "rc" + assert re.fullmatch(r"2\.0\.0rc[1-9][0-9]*", version) + assert manifest["ga_approved"] is False + assert manifest["blocking_gates"], "legacy RC requires blocking gates" + return + + assert schema == RELEASE_SCHEMA + if stage == "rc": + assert re.fullmatch(r"2\.0\.0rc[1-9][0-9]*", version) + assert manifest["ga_approved"] is False + assert manifest["blocking_gates"], "RC requires blocking gates" + assert "approval" not in manifest + return + + assert stage == "ga" + assert version == "2.0.0" + assert manifest["ga_approved"] is True + assert not manifest["blocking_gates"], "GA cannot contain blocking gates" + artifacts = manifest["artifacts"] + assert isinstance(artifacts, dict) + missing_artifacts = sorted(GA_REQUIRED_ARTIFACTS - set(artifacts)) + assert not missing_artifacts, "GA evidence is missing artifacts: " + ", ".join( + missing_artifacts + ) + approval = manifest.get("approval") + assert isinstance(approval, dict), "GA requires approval metadata" + assert approval.get("source_commit") == commit + assert approval.get("environment") == "ga-release" + assert isinstance(approval.get("requested_by"), str) and approval["requested_by"] + approvers = approval.get("approvers") + assert isinstance(approvers, list) and approvers + assert all(isinstance(approver, str) and approver for approver in approvers) + assert len(set(approvers)) == len(approvers) + assert re.fullmatch(r"[1-9][0-9]*", str(approval.get("workflow_run_id", ""))) + assert approval.get("approved_at") + actions = approval.get("actions") + assert isinstance(actions, dict) + missing_actions = sorted( + action for action in GA_RELEASE_ACTIONS if actions.get(action) is not True + ) + assert not missing_actions, "GA approval is missing actions: " + ", ".join( + missing_actions + ) + def _sha256(path: Path) -> str: digest = hashlib.sha256() @@ -39,14 +131,7 @@ def main() -> None: encoding="utf-8", ).strip() - assert manifest["schema"] == "openrath.rc-evidence/1" - assert manifest["release_stage"] == "rc" - assert manifest["version"] == version - assert manifest["tag"] == f"v{version}" - assert manifest["source_commit"] == commit - assert manifest["source_tree_clean"] is True - assert manifest["ga_approved"] is False - assert manifest["blocking_gates"] + validate_release_state(manifest, version=version, commit=commit) openapi = json.loads( Path("deploy/docs/openapi-v2.json").read_text(encoding="utf-8") @@ -62,6 +147,28 @@ def main() -> None: assert path.stat().st_size == artifact["size"], path assert _sha256(path) == artifact["sha256"], path + if manifest["release_stage"] == "ga": + approval_path = Path(manifest["artifacts"]["approval"]["path"]) + workflow = manifest.get("workflow") + assert isinstance(workflow, dict) + workflow_repository = workflow.get("repository") + workflow_run_id = workflow.get("run_id") + assert isinstance(workflow_repository, str) and workflow_repository + assert isinstance(workflow_run_id, str) and workflow_run_id + approval = load_ga_approval( + approval_path, + version=version, + commit=commit, + repository=workflow_repository, + workflow_run_id=workflow_run_id, + ) + assert approval["approvers"] == manifest["approval"]["approvers"] + assert approval["requested_by"] == manifest["approval"]["requested_by"] + assert approval["approved_at"] == manifest["approval"]["approved_at"] + assert approval["environment"] == manifest["approval"]["environment"] + assert approval["workflow_run_id"] == manifest["approval"]["workflow_run_id"] + assert approval["actions"] == manifest["approval"]["actions"] + print( f"verified {manifest['tag']} evidence for " f"{manifest['source_commit']} ({len(manifest['artifacts'])} artifacts)" diff --git a/scripts/release/verify_gate_reports.py b/scripts/release/verify_gate_reports.py new file mode 100644 index 0000000..e47d163 --- /dev/null +++ b/scripts/release/verify_gate_reports.py @@ -0,0 +1,156 @@ +"""Verify target-environment evidence before an OpenRath GA release.""" + +from __future__ import annotations + +import argparse +import json +import re +from datetime import datetime +from pathlib import Path + +GATE_REPORT_FILES = { + "tests": "tests.json", + "live_adapters": "live-adapters.json", + "performance": "performance.json", + "soak": "soak.json", + "drills": "drills.json", + "compatibility": "compatibility.json", +} + + +def _require_passed(details: dict[str, object], *names: str) -> None: + failed = sorted(name for name in names if details.get(name) != "passed") + if failed: + raise ValueError("required checks did not pass: " + ", ".join(failed)) + + +def _validate_common( + report: dict[str, object], + *, + gate: str, + source_commit: str, +) -> dict[str, object]: + if report.get("schema") != "openrath.ga-gate-report/1": + raise ValueError(f"{gate}: unsupported report schema") + if report.get("gate") != gate: + raise ValueError(f"{gate}: gate name does not match the filename") + if report.get("source_commit") != source_commit: + raise ValueError(f"{gate}: source_commit does not match the GA candidate") + if report.get("result") != "passed": + raise ValueError(f"{gate}: result must be passed") + generated_at = report.get("generated_at") + if not isinstance(generated_at, str): + raise ValueError(f"{gate}: generated_at is required") + try: + generated = datetime.fromisoformat(generated_at.replace("Z", "+00:00")) + except ValueError as error: + raise ValueError(f"{gate}: generated_at must be ISO 8601") from error + if generated.tzinfo is None: + raise ValueError(f"{gate}: generated_at must include a timezone") + if not isinstance(report.get("environment"), dict): + raise ValueError(f"{gate}: environment profile is required") + evidence = report.get("evidence") + if not isinstance(evidence, list) or not evidence: + raise ValueError(f"{gate}: at least one evidence reference is required") + if not isinstance(report.get("open_risks"), list): + raise ValueError(f"{gate}: open_risks must be a list") + details = report.get("details") + if not isinstance(details, dict): + raise ValueError(f"{gate}: details must be an object") + return details + + +def _validate_gate(gate: str, details: dict[str, object]) -> None: + if gate == "tests": + _require_passed(details, "required_ci") + if details.get("open_p0") != 0: + raise ValueError("tests: open_p0 must be zero") + return + if gate == "live_adapters": + _require_passed(details, "provider", "opensandbox", "openviking") + return + if gate == "performance": + _require_passed(details, "single_host", "split_profile") + efficiency = details.get("worker_scaling_efficiency") + if ( + isinstance(efficiency, bool) + or not isinstance(efficiency, (int, float)) + or efficiency < 0.70 + ): + raise ValueError( + "performance: worker_scaling_efficiency must be at least 0.70" + ) + return + if gate == "soak": + duration = details.get("duration_seconds") + if isinstance(duration, bool) or not isinstance(duration, (int, float)): + raise ValueError("soak: duration_seconds must be numeric") + if duration < 28800: + raise ValueError("soak: duration_seconds must be at least 28800") + if details.get("errors") != 0: + raise ValueError("soak: errors must be zero") + if details.get("unexplained_resource_growth") is not False: + raise ValueError("soak: unexplained_resource_growth must be false") + return + if gate == "drills": + _require_passed( + details, + "fault_matrix", + "backup_restore", + "rollout_rollback", + ) + return + if gate == "compatibility": + _require_passed( + details, + "api_review", + "v1_maintenance_window", + "migration", + ) + return + raise ValueError(f"unsupported GA gate: {gate}") + + +def verify_directory( + directory: Path, + *, + source_commit: str, +) -> dict[str, Path]: + """Verify all Gate C report files and return their paths by gate name.""" + if re.fullmatch(r"[0-9a-f]{40}", source_commit) is None: + raise ValueError("source_commit must be 40 lowercase hexadecimal characters") + validated: dict[str, Path] = {} + for gate, filename in GATE_REPORT_FILES.items(): + path = directory / filename + if not path.is_file(): + raise ValueError(f"missing GA gate report: {filename}") + try: + report = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as error: + raise ValueError(f"{filename}: invalid JSON") from error + if not isinstance(report, dict): + raise ValueError(f"{filename}: report must be an object") + details = _validate_common( + report, + gate=gate, + source_commit=source_commit, + ) + _validate_gate(gate, details) + validated[gate] = path + return validated + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("directory", type=Path) + parser.add_argument("--source-commit", required=True) + args = parser.parse_args() + reports = verify_directory( + args.directory, + source_commit=args.source_commit, + ) + print(f"verified {len(reports)} GA gate reports for {args.source_commit}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release/verify_pypi_files.py b/scripts/release/verify_pypi_files.py new file mode 100644 index 0000000..fe2954b --- /dev/null +++ b/scripts/release/verify_pypi_files.py @@ -0,0 +1,119 @@ +"""Verify that existing PyPI files are identical to local release artifacts.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import time +import urllib.error +import urllib.request +from pathlib import Path + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def local_distributions(packages_dir: Path, *, version: str) -> dict[str, str]: + """Return the exact wheel and sdist hashes expected for one version.""" + sdist = packages_dir / f"openrath-{version}.tar.gz" + wheels = sorted(packages_dir.glob(f"openrath-{version}-*.whl")) + if not sdist.is_file() or len(wheels) != 1: + raise ValueError("expected exactly one OpenRath wheel and one sdist") + paths = [sdist, wheels[0]] + return {path.name: _sha256(path) for path in paths} + + +def verify_remote_files( + local: dict[str, str], + payload: dict[str, object] | None, + *, + version: str, + require_complete: bool, +) -> bool: + """Reject conflicting files and return whether PyPI is complete.""" + if payload is None: + if require_complete: + raise ValueError(f"OpenRath {version} is not visible on PyPI") + return False + + info = payload.get("info") + if not isinstance(info, dict) or info.get("version") != version: + raise ValueError("PyPI response version does not match the release") + urls = payload.get("urls") + if not isinstance(urls, list) or not urls: + raise ValueError("PyPI response contains no distribution files") + + remote: dict[str, str] = {} + for item in urls: + if not isinstance(item, dict): + raise ValueError("PyPI distribution entry must be an object") + filename = item.get("filename") + digests = item.get("digests") + sha256 = digests.get("sha256") if isinstance(digests, dict) else None + if not isinstance(filename, str) or not isinstance(sha256, str): + raise ValueError("PyPI distribution entry is missing its SHA-256") + remote[filename] = sha256 + + unexpected = sorted(set(remote) - set(local)) + if unexpected: + raise ValueError("PyPI contains unexpected files: " + ", ".join(unexpected)) + conflicts = sorted( + filename for filename, digest in remote.items() if local.get(filename) != digest + ) + if conflicts: + raise ValueError("PyPI file hash mismatch: " + ", ".join(conflicts)) + + missing = sorted(set(local) - set(remote)) + if require_complete and missing: + raise ValueError("PyPI is missing release files: " + ", ".join(missing)) + return not missing + + +def _pypi_payload(*, version: str) -> dict[str, object] | None: + url = f"https://pypi.org/pypi/openrath/{version}/json" + try: + with urllib.request.urlopen(url, timeout=20) as response: + return json.load(response) + except urllib.error.HTTPError as error: + if error.code == 404: + return None + raise + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--packages-dir", type=Path, required=True) + parser.add_argument("--version", required=True) + parser.add_argument("--require-complete", action="store_true") + parser.add_argument("--attempts", type=int, default=1) + parser.add_argument("--delay-seconds", type=float, default=5) + args = parser.parse_args() + if args.attempts < 1: + raise SystemExit("--attempts must be positive") + + local = local_distributions(args.packages_dir, version=args.version) + last_error: ValueError | None = None + for attempt in range(1, args.attempts + 1): + try: + complete = verify_remote_files( + local, + _pypi_payload(version=args.version), + version=args.version, + require_complete=args.require_complete, + ) + except ValueError as error: + last_error = error + if attempt == args.attempts: + raise SystemExit(str(error)) from error + else: + state = "complete and identical" if complete else "absent or partial" + print(f"PyPI OpenRath {args.version}: {state}") + return + time.sleep(args.delay_seconds) + raise SystemExit(str(last_error)) diff --git a/tests/deployment/test_ga_gate_reports.py b/tests/deployment/test_ga_gate_reports.py new file mode 100644 index 0000000..120d194 --- /dev/null +++ b/tests/deployment/test_ga_gate_reports.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from scripts.release import verify_gate_reports + +SOURCE_COMMIT = "b" * 40 + + +def _reports() -> dict[str, dict[str, object]]: + common = { + "schema": "openrath.ga-gate-report/1", + "source_commit": SOURCE_COMMIT, + "result": "passed", + "generated_at": "2026-07-30T12:00:00+00:00", + "environment": {"profile": "target-like"}, + "evidence": ["artifact://report"], + "open_risks": [], + } + return { + "tests": { + **common, + "gate": "tests", + "details": {"required_ci": "passed", "open_p0": 0}, + }, + "live_adapters": { + **common, + "gate": "live_adapters", + "details": { + "provider": "passed", + "opensandbox": "passed", + "openviking": "passed", + }, + }, + "performance": { + **common, + "gate": "performance", + "details": { + "single_host": "passed", + "split_profile": "passed", + "worker_scaling_efficiency": 0.72, + }, + }, + "soak": { + **common, + "gate": "soak", + "details": { + "duration_seconds": 28800, + "errors": 0, + "unexplained_resource_growth": False, + }, + }, + "drills": { + **common, + "gate": "drills", + "details": { + "fault_matrix": "passed", + "backup_restore": "passed", + "rollout_rollback": "passed", + }, + }, + "compatibility": { + **common, + "gate": "compatibility", + "details": { + "api_review": "passed", + "v1_maintenance_window": "passed", + "migration": "passed", + }, + }, + } + + +def _write_reports(directory: Path, reports: dict[str, dict[str, object]]) -> None: + for gate, report in reports.items(): + filename = verify_gate_reports.GATE_REPORT_FILES[gate] + (directory / filename).write_text(json.dumps(report), encoding="utf-8") + + +def test_complete_ga_gate_report_set_passes(tmp_path: Path) -> None: + _write_reports(tmp_path, _reports()) + validated = verify_gate_reports.verify_directory( + tmp_path, + source_commit=SOURCE_COMMIT, + ) + assert set(validated) == set(verify_gate_reports.GATE_REPORT_FILES) + + +def test_soak_shorter_than_eight_hours_is_rejected(tmp_path: Path) -> None: + reports = _reports() + soak_details = reports["soak"]["details"] + assert isinstance(soak_details, dict) + soak_details["duration_seconds"] = 28799 + _write_reports(tmp_path, reports) + + with pytest.raises(ValueError, match="28800"): + verify_gate_reports.verify_directory( + tmp_path, + source_commit=SOURCE_COMMIT, + ) + + +def test_report_from_a_different_source_commit_is_rejected(tmp_path: Path) -> None: + reports = _reports() + reports["performance"]["source_commit"] = "c" * 40 + _write_reports(tmp_path, reports) + + with pytest.raises(ValueError, match="source_commit"): + verify_gate_reports.verify_directory( + tmp_path, + source_commit=SOURCE_COMMIT, + ) diff --git a/tests/deployment/test_pypi_recovery.py b/tests/deployment/test_pypi_recovery.py new file mode 100644 index 0000000..bf500ea --- /dev/null +++ b/tests/deployment/test_pypi_recovery.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from scripts.release.verify_pypi_files import ( + local_distributions, + verify_remote_files, +) + + +def _payload(files: dict[str, str]) -> dict[str, object]: + return { + "info": {"version": "2.0.0"}, + "urls": [ + {"filename": filename, "digests": {"sha256": digest}} + for filename, digest in files.items() + ], + } + + +def test_local_distributions_requires_one_wheel_and_sdist(tmp_path: Path) -> None: + (tmp_path / "openrath-2.0.0.tar.gz").write_bytes(b"sdist") + (tmp_path / "openrath-2.0.0-py3-none-any.whl").write_bytes(b"wheel") + + files = local_distributions(tmp_path, version="2.0.0") + + assert set(files) == { + "openrath-2.0.0.tar.gz", + "openrath-2.0.0-py3-none-any.whl", + } + + +def test_pypi_recovery_accepts_absent_partial_or_complete_identical_files() -> None: + local = {"openrath-2.0.0.tar.gz": "a" * 64, "openrath-2.0.0.whl": "b" * 64} + + assert not verify_remote_files(local, None, version="2.0.0", require_complete=False) + assert not verify_remote_files( + local, + _payload({"openrath-2.0.0.tar.gz": "a" * 64}), + version="2.0.0", + require_complete=False, + ) + assert verify_remote_files( + local, _payload(local), version="2.0.0", require_complete=True + ) + + +def test_pypi_recovery_rejects_conflicting_or_unexpected_files() -> None: + local = {"openrath-2.0.0.tar.gz": "a" * 64} + + with pytest.raises(ValueError, match="hash mismatch"): + verify_remote_files( + local, + _payload({"openrath-2.0.0.tar.gz": "b" * 64}), + version="2.0.0", + require_complete=False, + ) + with pytest.raises(ValueError, match="unexpected files"): + verify_remote_files( + local, + _payload({**local, "openrath-2.0.0.exe": "c" * 64}), + version="2.0.0", + require_complete=False, + ) diff --git a/tests/deployment/test_release_evidence.py b/tests/deployment/test_release_evidence.py new file mode 100644 index 0000000..c063a82 --- /dev/null +++ b/tests/deployment/test_release_evidence.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from scripts.release import build_evidence, create_ga_approval, verify_evidence + +SOURCE_COMMIT = "a" * 40 + + +def _approval() -> dict[str, object]: + return { + "schema": "openrath.ga-approval/1", + "version": "2.0.0", + "source_commit": SOURCE_COMMIT, + "approved": True, + "approved_at": "2026-07-30T12:00:00+00:00", + "approvers": ["release-owner"], + "requested_by": "release-requester", + "environment": "ga-release", + "environment_reviews": [ + { + "review_id": "42", + "reviewer": "release-owner", + "approved_at": "2026-07-30T12:00:00+00:00", + } + ], + "repository": "Rath-Team/OpenRath", + "workflow_run_id": "12345", + "actions": { + "pypi": True, + "ghcr": True, + "github_release": True, + }, + } + + +def test_release_stage_is_inferred_from_exact_supported_versions() -> None: + assert build_evidence.infer_release_stage("2.0.0rc1") == "rc" + assert build_evidence.infer_release_stage("2.0.0rc12") == "rc" + assert build_evidence.infer_release_stage("2.0.0") == "ga" + + with pytest.raises(ValueError, match="unsupported release version"): + build_evidence.infer_release_stage("2.0.1") + + +def test_ga_approval_is_bound_to_version_commit_and_all_release_actions( + tmp_path: Path, +) -> None: + approval_path = tmp_path / "approval.json" + approval_path.write_text(json.dumps(_approval()), encoding="utf-8") + + approval = build_evidence.load_ga_approval( + approval_path, + version="2.0.0", + commit=SOURCE_COMMIT, + repository="Rath-Team/OpenRath", + workflow_run_id="12345", + ) + assert approval["approvers"] == ["release-owner"] + + missing_action = _approval() + del missing_action["actions"]["pypi"] # type: ignore[index] + approval_path.write_text(json.dumps(missing_action), encoding="utf-8") + with pytest.raises(ValueError, match="pypi"): + build_evidence.load_ga_approval( + approval_path, + version="2.0.0", + commit=SOURCE_COMMIT, + ) + + unsupported_action = _approval() + unsupported_action["actions"]["tag"] = True # type: ignore[index] + approval_path.write_text(json.dumps(unsupported_action), encoding="utf-8") + with pytest.raises(ValueError, match="unsupported actions"): + build_evidence.load_ga_approval( + approval_path, + version="2.0.0", + commit=SOURCE_COMMIT, + ) + + +def test_ga_approval_uses_actual_protected_environment_reviews( + tmp_path: Path, +) -> None: + history_path = tmp_path / "reviews.json" + history_path.write_text( + json.dumps( + [ + { + "id": 42, + "state": "approved", + "user": {"login": "release-owner"}, + "created_at": "2026-07-30T12:00:00Z", + "environments": [{"name": "ga-release"}], + }, + { + "id": 43, + "state": "approved", + "user": {"login": "other-owner"}, + "created_at": "2026-07-30T12:01:00Z", + "environments": [{"name": "staging"}], + }, + ] + ), + encoding="utf-8", + ) + + approval = create_ga_approval.build_approval( + version="2.0.0", + source_commit=SOURCE_COMMIT, + requested_by="release-requester", + repository="Rath-Team/OpenRath", + workflow_run_id="12345", + review_history=history_path, + ) + + assert approval["approvers"] == ["release-owner"] + assert approval["requested_by"] == "release-requester" + assert approval["approved_at"] == "2026-07-30T12:00:00Z" + assert approval["environment_reviews"] == [ + { + "review_id": "42", + "reviewer": "release-owner", + "approved_at": "2026-07-30T12:00:00Z", + } + ] + + +def test_ga_release_requires_every_evidence_category() -> None: + complete = set(build_evidence.GA_REQUIRED_ARTIFACTS) + build_evidence.require_ga_artifacts(complete) + + incomplete = complete - {"soak"} + with pytest.raises(ValueError, match="soak"): + build_evidence.require_ga_artifacts(incomplete) + + +def test_verifier_accepts_legacy_rc_and_rejects_ga_with_blockers() -> None: + legacy_rc = { + "schema": "openrath.rc-evidence/1", + "release_stage": "rc", + "version": "2.0.0rc1", + "tag": "v2.0.0rc1", + "source_commit": SOURCE_COMMIT, + "source_tree_clean": True, + "ga_approved": False, + "blocking_gates": ["target-like soak"], + "artifacts": {}, + } + verify_evidence.validate_release_state( + legacy_rc, + version="2.0.0rc1", + commit=SOURCE_COMMIT, + ) + + ga = { + **legacy_rc, + "schema": "openrath.release-evidence/2", + "release_stage": "ga", + "version": "2.0.0", + "tag": "v2.0.0", + "ga_approved": True, + "blocking_gates": ["not actually ready"], + "approval": _approval(), + "artifacts": {name: {} for name in build_evidence.GA_REQUIRED_ARTIFACTS}, + } + with pytest.raises(AssertionError, match="blocking gates"): + verify_evidence.validate_release_state( + ga, + version="2.0.0", + commit=SOURCE_COMMIT, + ) + + +def test_release_manifest_schema_has_separate_rc_and_ga_contracts() -> None: + schema = json.loads( + Path("release/evidence/schema/manifest.schema.json").read_text(encoding="utf-8") + ) + assert schema["$id"].endswith("/release-evidence-manifest-v2.json") + assert len(schema["oneOf"]) == 2 diff --git a/tests/deployment/test_release_version.py b/tests/deployment/test_release_version.py index 53f2489..27178d5 100644 --- a/tests/deployment/test_release_version.py +++ b/tests/deployment/test_release_version.py @@ -40,3 +40,37 @@ def test_release_candidate_workflow_is_digest_and_evidence_bound() -> None: assert "scripts/release/verify_evidence.py" in workflow assert "--prerelease" in workflow assert "PyPI" not in workflow + + +def test_ga_workflow_is_protected_evidence_bound_and_uses_trusted_publishing() -> None: + workflow = Path(".github/workflows/release-v2-ga.yml").read_text(encoding="utf-8") + assert "workflow_dispatch:" in workflow + assert "evidence_run_id:" in workflow + assert "confirmation:" in workflow + assert "name: ga-release" in workflow + assert "scripts/release/verify_gate_reports.py" in workflow + assert "--stage ga" in workflow + assert "--approval" in workflow + assert "openrath-v2.0.0-ga-input" in workflow + assert "actions: read" in workflow + assert "packages: write" in workflow + assert "attestations: write" in workflow + assert "id-token: write" in workflow + assert ( + "pypa/gh-action-pypi-publish" + "@dc37677b2e1c63e2034f94d8a5b11f265b73ba33" in workflow + ) + assert "scripts/release/verify_pypi_files.py" in workflow + assert "skip-existing: true" in workflow + assert "--require-complete" in workflow + assert "gh release create" in workflow + assert "--prerelease" not in workflow + + +def test_ga_release_documents_are_present_and_not_marked_as_drafts() -> None: + notes = Path("release/notes/v2.0.0.md").read_text(encoding="utf-8") + checklist = Path("release/checklists/v2.0.0-ga.md").read_text(encoding="utf-8") + assert "OpenRath v2.0.0" in notes + assert "Gate C" in checklist + for marker in ("TODO", "HOLD", "DRAFT"): + assert marker not in notes From cc81e24d67bc9e7bde8baa2a9a7c50a6bcec94e3 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Thu, 30 Jul 2026 12:20:06 +0800 Subject: [PATCH 20/22] release: support manual token-based PyPI publication --- .github/workflows/release-v2-ga-finalize.yml | 203 +++++++++++++++++++ .github/workflows/release-v2-ga.yml | 149 ++------------ release/checklists/v2.0.0-ga.md | 9 +- release/manual-pypi-v2.0.0.md | 52 +++++ scripts/release/publish_pypi_manual.py | 110 ++++++++++ scripts/release/verify_evidence.py | 10 +- scripts/release/verify_release_assets.py | 55 +++++ tests/deployment/test_manual_pypi_publish.py | 32 +++ tests/deployment/test_release_assets.py | 42 ++++ tests/deployment/test_release_version.py | 57 ++++-- 10 files changed, 560 insertions(+), 159 deletions(-) create mode 100644 .github/workflows/release-v2-ga-finalize.yml create mode 100644 release/manual-pypi-v2.0.0.md create mode 100644 scripts/release/publish_pypi_manual.py create mode 100644 scripts/release/verify_release_assets.py create mode 100644 tests/deployment/test_manual_pypi_publish.py create mode 100644 tests/deployment/test_release_assets.py diff --git a/.github/workflows/release-v2-ga-finalize.yml b/.github/workflows/release-v2-ga-finalize.yml new file mode 100644 index 0000000..85cbc97 --- /dev/null +++ b/.github/workflows/release-v2-ga-finalize.yml @@ -0,0 +1,203 @@ +name: Finalize v2.0.0 GA + +on: + workflow_dispatch: + inputs: + tag: + description: Existing annotated GA tag + required: true + type: string + preparation_run_id: + description: Successful Prepare v2.0.0 GA bundle run + required: true + type: string + confirmation: + description: Type "finalize v2.0.0" after the manual PyPI upload + required: true + type: string + +concurrency: + group: finalize-openrath-v2.0.0 + cancel-in-progress: false + +permissions: {} + +jobs: + finalize-ga: + name: Verify PyPI and publish final GA surfaces + runs-on: ubuntu-latest + environment: + name: ga-release + url: https://github.com/${{ github.repository }}/releases/tag/v2.0.0 + permissions: + actions: read # Read and download the protected preparation artifact. + attestations: read # Verify the source-SHA OCI image attestation. + contents: write # Create or verify the immutable GitHub Release. + packages: write # Promote the verified image digest to the 2.0.0 tag. + env: + IMAGE: ghcr.io/rath-team/openrath + PREPARATION_RUN_ID: ${{ inputs.preparation_run_id }} + RELEASE_TAG: ${{ inputs.tag }} + VERSION: 2.0.0 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 + with: + python-version: '3.12' + enable-cache: false + - name: Validate dispatch, tag, version, and main ancestry + env: + RELEASE_CONFIRMATION: ${{ inputs.confirmation }} + run: | + test "$GITHUB_REF" = "refs/heads/main" + test "$RELEASE_CONFIRMATION" = "finalize v2.0.0" + test "$RELEASE_TAG" = "v2.0.0" + test "$(git cat-file -t "$RELEASE_TAG")" = "tag" + test "$(git rev-parse "$RELEASE_TAG^{}")" = "$(git rev-parse HEAD)" + git fetch origin main + git merge-base --is-ancestor HEAD origin/main + project_version="$(python - <<'PY' + import re + from pathlib import Path + text = Path("pyproject.toml").read_text(encoding="utf-8") + print(re.search(r'^version = "([^"]+)"$', text, re.MULTILINE).group(1)) + PY + )" + test "$project_version" = "$VERSION" + test -f "release/notes/v${VERSION}.md" + ! grep -Eq 'TODO|HOLD|DRAFT' "release/notes/v${VERSION}.md" + test -z "$(git status --porcelain --untracked-files=no)" + echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + - name: Download and verify the protected candidate bundle + env: + GH_TOKEN: ${{ github.token }} + run: | + [[ "$PREPARATION_RUN_ID" =~ ^[1-9][0-9]*$ ]] + run_json="$(gh run view "$PREPARATION_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --json headSha,conclusion,event,workflowName)" + test "$(jq -r .conclusion <<<"$run_json")" = "success" + test "$(jq -r .event <<<"$run_json")" = "workflow_dispatch" + test "$(jq -r .headSha <<<"$run_json")" = "$SOURCE_SHA" + test "$(jq -r .workflowName <<<"$run_json")" = \ + "Prepare v2.0.0 GA bundle" + gh run download "$PREPARATION_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name openrath-2.0.0-ga-candidate \ + --dir release-bundle + python scripts/release/verify_evidence.py \ + release-bundle/release/evidence/"$VERSION"/manifest.json \ + --artifact-root release-bundle + python scripts/release/verify_pypi_files.py \ + --packages-dir release-bundle/dist \ + --version "$VERSION" \ + --require-complete \ + --attempts 12 \ + --delay-seconds 10 + image_digest="$(jq -r '.artifacts.image.digest' \ + release-bundle/release/evidence/"$VERSION"/manifest.json)" + [[ "$image_digest" =~ ^sha256:[0-9a-f]{64}$ ]] + echo "IMAGE_DIGEST=$image_digest" >> "$GITHUB_ENV" + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Promote the attested candidate digest + env: + GH_TOKEN: ${{ github.token }} + run: | + candidate_digest="$(docker buildx imagetools inspect \ + "$IMAGE:$SOURCE_SHA" | + sed -n 's/^Digest:[[:space:]]*//p' | head -1)" + test "$candidate_digest" = "$IMAGE_DIGEST" + gh attestation verify \ + "oci://$IMAGE@$IMAGE_DIGEST" \ + --repo "$GITHUB_REPOSITORY" + + if docker buildx imagetools inspect "$IMAGE:$VERSION" \ + >"$RUNNER_TEMP/existing-image.txt" 2>/dev/null; then + existing_digest="$(sed -n \ + 's/^Digest:[[:space:]]*//p' \ + "$RUNNER_TEMP/existing-image.txt" | head -1)" + test "$existing_digest" = "$IMAGE_DIGEST" + else + docker buildx imagetools create \ + --prefer-index=false \ + --tag "$IMAGE:$VERSION" \ + "$IMAGE@$IMAGE_DIGEST" + fi + + promoted_digest="$(docker buildx imagetools inspect \ + "$IMAGE:$VERSION" | + sed -n 's/^Digest:[[:space:]]*//p' | head -1)" + test "$promoted_digest" = "$IMAGE_DIGEST" + - name: Create or verify the immutable GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + shopt -s nullglob + assets=( + release-bundle/dist/* + release-bundle/release/evidence/"$VERSION"/*.json + release-bundle/release/evidence/"$VERSION"/*.yaml + release-bundle/release/evidence/"$VERSION"/gates/*.json + ) + test "${#assets[@]}" -gt 2 + if gh release view "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + release_json="$(gh release view "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --json isDraft,isPrerelease,tagName)" + test "$(jq -r .isDraft <<<"$release_json")" = "false" + test "$(jq -r .isPrerelease <<<"$release_json")" = "false" + test "$(jq -r .tagName <<<"$release_json")" = "$RELEASE_TAG" + mkdir "$RUNNER_TEMP/existing-release" + gh release download "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --dir "$RUNNER_TEMP/existing-release" + python scripts/release/verify_release_assets.py \ + "$RUNNER_TEMP/existing-release" \ + "${assets[@]}" + else + gh release create "$RELEASE_TAG" \ + "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "OpenRath $VERSION" \ + --notes-file "release/notes/v${VERSION}.md" + fi + - name: Verify public GA artifacts + env: + GH_TOKEN: ${{ github.token }} + run: | + python scripts/release/verify_pypi_files.py \ + --packages-dir release-bundle/dist \ + --version "$VERSION" \ + --require-complete + uv venv "$RUNNER_TEMP/verify" + uv pip install \ + --python "$RUNNER_TEMP/verify/bin/python" \ + --no-cache \ + "openrath==$VERSION" + "$RUNNER_TEMP/verify/bin/python" -c \ + "import importlib.metadata; assert importlib.metadata.version('openrath') == '$VERSION'" + + docker logout ghcr.io + mkdir "$RUNNER_TEMP/docker-anonymous" + DOCKER_CONFIG="$RUNNER_TEMP/docker-anonymous" \ + docker manifest inspect "$IMAGE@$IMAGE_DIGEST" >/dev/null + public_digest="$(DOCKER_CONFIG="$RUNNER_TEMP/docker-anonymous" \ + docker buildx imagetools inspect "$IMAGE:$VERSION" | + sed -n 's/^Digest:[[:space:]]*//p' | head -1)" + test "$public_digest" = "$IMAGE_DIGEST" + gh attestation verify \ + "oci://$IMAGE@$IMAGE_DIGEST" \ + --repo "$GITHUB_REPOSITORY" + gh release view "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" >/dev/null diff --git a/.github/workflows/release-v2-ga.yml b/.github/workflows/release-v2-ga.yml index 65bd399..6b1ab75 100644 --- a/.github/workflows/release-v2-ga.yml +++ b/.github/workflows/release-v2-ga.yml @@ -1,4 +1,4 @@ -name: Publish v2.0.0 GA +name: Prepare v2.0.0 GA bundle on: workflow_dispatch: @@ -12,31 +12,29 @@ on: required: true type: string confirmation: - description: Type "publish v2.0.0" to authorize this run + description: Type "prepare v2.0.0" to authorize this run required: true type: string concurrency: - group: publish-openrath-v2.0.0 + group: prepare-openrath-v2.0.0 cancel-in-progress: false permissions: {} jobs: - publish-ga: - name: Verify evidence, publish OCI, and attest + prepare-ga: + name: Verify evidence and prepare immutable artifacts runs-on: ubuntu-latest environment: name: ga-release - url: https://github.com/${{ github.repository }}/releases/tag/v2.0.0 + url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} permissions: - actions: read - attestations: write + actions: read # Read the Gate C result and download its evidence artifact. + attestations: write # Sign the immutable source-SHA OCI image. contents: read - id-token: write - packages: write - outputs: - image_digest: ${{ steps.push_image.outputs.digest }} + id-token: write # Request GitHub's OIDC identity for keyless attestation. + packages: write # Push only the source-SHA image candidate to GHCR. env: RELEASE_TAG: ${{ inputs.tag }} EVIDENCE_RUN_ID: ${{ inputs.evidence_run_id }} @@ -58,7 +56,7 @@ jobs: RELEASE_CONFIRMATION: ${{ inputs.confirmation }} run: | test "$GITHUB_REF" = "refs/heads/main" - test "$RELEASE_CONFIRMATION" = "publish v2.0.0" + test "$RELEASE_CONFIRMATION" = "prepare v2.0.0" test "$RELEASE_TAG" = "v2.0.0" test "$(git cat-file -t "$RELEASE_TAG")" = "tag" test "$(git rev-parse "$RELEASE_TAG^{}")" = "$(git rev-parse HEAD)" @@ -148,9 +146,7 @@ jobs: file: docker/Dockerfile load: true push: false - tags: | - ${{ env.IMAGE }}:${{ env.VERSION }} - ${{ env.IMAGE }}:${{ env.SOURCE_SHA }} + tags: ${{ env.IMAGE }}:${{ env.SOURCE_SHA }} build-args: | OPENRATH_VERSION=${{ env.VERSION }} OPENRATH_REVISION=${{ env.SOURCE_SHA }} @@ -168,7 +164,7 @@ jobs: --ignore-unfixed \ --format json \ --output "/workspace/$evidence_dir/image-scan.json" \ - "$IMAGE:$VERSION" + "$IMAGE:$SOURCE_SHA" docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ @@ -176,7 +172,7 @@ jobs: "$TRIVY_IMAGE" image \ --format cyclonedx \ --output "/workspace/$evidence_dir/sbom.cdx.json" \ - "$IMAGE:$VERSION" + "$IMAGE:$SOURCE_SHA" docker run --rm \ -v "$RUNNER_TEMP/trivy-cache:/root/.cache/trivy" \ -v "$PWD:/workspace:ro" \ @@ -199,16 +195,15 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - - name: Push the already-scanned image + - name: Push the immutable candidate image id: push_image run: | - push_output="$(docker push "$IMAGE:$VERSION")" + push_output="$(docker push "$IMAGE:$SOURCE_SHA")" printf '%s\n' "$push_output" digest="$(sed -n \ 's/^.*digest: \(sha256:[0-9a-f]\{64\}\).*$/\1/p' \ <<<"$push_output" | tail -1)" [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] - docker push "$IMAGE:$SOURCE_SHA" echo "digest=$digest" >> "$GITHUB_OUTPUT" - name: Build and verify final SHA-bound evidence env: @@ -248,119 +243,9 @@ jobs: - name: Upload the final release bundle uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: openrath-2.0.0-release + name: openrath-2.0.0-ga-candidate retention-days: 90 if-no-files-found: error path: | dist/* release/evidence/2.0.0/* - scripts/release/verify_pypi_files.py - - publish-pypi: - name: Publish distributions to PyPI - needs: publish-ga - runs-on: ubuntu-latest - environment: - name: ga-release - url: https://pypi.org/p/openrath - permissions: - id-token: write - steps: - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: openrath-2.0.0-release - path: release-bundle - - name: Reject conflicting existing PyPI files - run: | - python release-bundle/scripts/release/verify_pypi_files.py \ - --packages-dir release-bundle/dist \ - --version 2.0.0 - - name: Publish package distributions with attestations - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 - with: - packages-dir: release-bundle/dist - print-hash: true - skip-existing: true - - publish-github: - name: Create the final GitHub Release - needs: publish-pypi - runs-on: ubuntu-latest - permissions: - contents: write - env: - RELEASE_TAG: v2.0.0 - VERSION: 2.0.0 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: - ref: ${{ env.RELEASE_TAG }} - persist-credentials: false - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: openrath-2.0.0-release - path: release-bundle - - name: Create the immutable final release - env: - GH_TOKEN: ${{ github.token }} - run: | - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - echo "Release $RELEASE_TAG already exists; refusing to overwrite it." >&2 - exit 1 - fi - gh release create "$RELEASE_TAG" \ - release-bundle/dist/* \ - release-bundle/release/evidence/"$VERSION"/*.json \ - release-bundle/release/evidence/"$VERSION"/*.yaml \ - release-bundle/release/evidence/"$VERSION"/gates/*.json \ - --verify-tag \ - --title "OpenRath $VERSION" \ - --notes-file "release/notes/v${VERSION}.md" - - verify-publication: - name: Verify public GA artifacts - needs: [publish-ga, publish-github] - runs-on: ubuntu-latest - permissions: - contents: read - env: - IMAGE: ghcr.io/rath-team/openrath - IMAGE_DIGEST: ${{ needs.publish-ga.outputs.image_digest }} - VERSION: 2.0.0 - steps: - - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 - with: - python-version: '3.12' - enable-cache: false - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: openrath-2.0.0-release - path: release-bundle - - name: Verify published PyPI files match the release bundle - run: | - python release-bundle/scripts/release/verify_pypi_files.py \ - --packages-dir release-bundle/dist \ - --version "$VERSION" \ - --require-complete \ - --attempts 12 \ - --delay-seconds 10 - - name: Verify a fresh PyPI installation - run: | - uv venv "$RUNNER_TEMP/verify" - uv pip install \ - --python "$RUNNER_TEMP/verify/bin/python" \ - --no-cache \ - "openrath==$VERSION" - "$RUNNER_TEMP/verify/bin/python" -c \ - "import importlib.metadata; assert importlib.metadata.version('openrath') == '$VERSION'" - - name: Verify anonymous OCI access and provenance - env: - GH_TOKEN: ${{ github.token }} - run: | - mkdir "$RUNNER_TEMP/docker-anonymous" - DOCKER_CONFIG="$RUNNER_TEMP/docker-anonymous" \ - docker manifest inspect "$IMAGE@$IMAGE_DIGEST" >/dev/null - gh attestation verify \ - "oci://$IMAGE@$IMAGE_DIGEST" \ - --repo "$GITHUB_REPOSITORY" - gh release view "v$VERSION" --repo "$GITHUB_REPOSITORY" >/dev/null diff --git a/release/checklists/v2.0.0-ga.md b/release/checklists/v2.0.0-ga.md index c4b4038..311fb5b 100644 --- a/release/checklists/v2.0.0-ga.md +++ b/release/checklists/v2.0.0-ga.md @@ -23,8 +23,8 @@ Every machine-readable report must identify the exact final source commit. - [ ] PR review is complete with no unresolved P0/P1 thread. - [ ] `main` protection and required checks are active. - [ ] The `ga-release` environment has required reviewers. -- [ ] PyPI Trusted Publishing authorizes - `.github/workflows/release-v2-ga.yml` in environment `ga-release`. +- [ ] A PyPI token scoped only to `openrath` is held outside GitHub and the + repository for the interactive manual upload. - [ ] The annotated `v2.0.0` tag points to a commit reachable from `main`. - [ ] The owner explicitly approves merge, version bump, tag, PyPI, GHCR, and GitHub Release actions. @@ -36,7 +36,10 @@ Every machine-readable report must identify the exact final source commit. - [ ] Image and repository secret scans pass. - [ ] Final SBOM, immutable image digest, and evidence manifest are attached. - [ ] OCI provenance attestation verifies by digest. -- [ ] PyPI Trusted Publishing and PyPI attestations succeed. +- [ ] The protected preparation artifact is uploaded to PyPI without rebuilding. +- [ ] PyPI wheel and sdist SHA-256 values match the preparation artifact. +- [ ] The protected finalization workflow promotes the attested OCI digest and + creates the final GitHub Release. - [ ] Fresh Python installation from PyPI reports `2.0.0`. - [ ] Anonymous GHCR pull by digest succeeds. - [ ] GitHub Release is final, not a prerelease. diff --git a/release/manual-pypi-v2.0.0.md b/release/manual-pypi-v2.0.0.md new file mode 100644 index 0000000..79dce7e --- /dev/null +++ b/release/manual-pypi-v2.0.0.md @@ -0,0 +1,52 @@ +# Manual PyPI publication for OpenRath 2.0.0 + +This procedure uploads only the immutable distributions produced by the +protected `Prepare v2.0.0 GA bundle` workflow. Never rebuild the wheel or sdist +locally and never place a PyPI token in this repository, a command-line +argument, an environment variable, or a GitHub secret. + +## One-time PyPI setup + +1. Sign in to PyPI with an owner of the `openrath` project. +2. Open . +3. Create an API token scoped only to the existing `openrath` project. +4. Copy it to a password manager. The value starts with `pypi-`. + +## Prepare the local handoff + +After the GA preparation workflow succeeds, check out the exact annotated tag +and download its artifact: + +```powershell +git fetch origin --tags +git switch --detach v2.0.0 +gh run download ` + --repo Rath-Team/OpenRath ` + --name openrath-2.0.0-ga-candidate ` + --dir release-bundle +``` + +The helper verifies the SHA-bound manifest, every artifact hash, the current +PyPI state, and Twine metadata before it offers to upload: + +```powershell +uv run python scripts/release/publish_pypi_manual.py ` + --bundle-dir release-bundle +``` + +Type the exact confirmation displayed by the helper. Twine then prompts for +the API token without echoing it. Use the complete value including its +`pypi-` prefix. In Windows Terminal, paste with `Ctrl+Shift+V` if `Ctrl+V` +does not work inside the hidden prompt. The helper performs a second hash +comparison against PyPI after the upload. + +Do not use `dist/` from another checkout and do not run `uv build` during this +handoff. + +## Finalize the release + +Dispatch `Finalize v2.0.0 GA` from `main` with the same annotated tag and the +preparation workflow run ID. Its protected job refuses to continue unless +PyPI exposes exactly the wheel and sdist from the candidate bundle. It then +promotes the already-attested OCI digest to `2.0.0`, creates or verifies the +GitHub Release, and performs fresh public installation and provenance checks. diff --git a/scripts/release/publish_pypi_manual.py b/scripts/release/publish_pypi_manual.py new file mode 100644 index 0000000..03533a8 --- /dev/null +++ b/scripts/release/publish_pypi_manual.py @@ -0,0 +1,110 @@ +"""Publish the verified OpenRath GA bundle with an interactive PyPI token.""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + +CONFIRMATION = "publish openrath 2.0.0 to pypi" +TWINE_VERSION = "6.2.0" + + +def _run(*arguments: str, env: dict[str, str] | None = None) -> None: + subprocess.run(arguments, check=True, env=env) + + +def twine_environment(source: dict[str, str]) -> dict[str, str]: + """Return an environment that cannot reuse stored Twine credentials.""" + clean = dict(source) + for name in ( + "TWINE_PASSWORD", + "TWINE_USERNAME", + "TWINE_REPOSITORY", + "TWINE_REPOSITORY_URL", + ): + clean.pop(name, None) + clean["PYTHON_KEYRING_BACKEND"] = "keyring.backends.null.Keyring" + return clean + + +def twine_upload_command(distributions: list[str]) -> list[str]: + """Build the interactive upload command without embedding a token.""" + return [ + "uvx", + "--from", + f"twine=={TWINE_VERSION}", + "twine", + "upload", + "--repository-url", + "https://upload.pypi.org/legacy/", + "--username", + "__token__", + "--skip-existing", + *distributions, + ] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--bundle-dir", type=Path, required=True) + args = parser.parse_args() + + bundle = args.bundle_dir.resolve() + repository_root = Path(__file__).resolve().parents[2] + manifest = bundle / "release/evidence/2.0.0/manifest.json" + packages_dir = bundle / "dist" + wheel = next(packages_dir.glob("openrath-2.0.0-*.whl"), None) + sdist = packages_dir / "openrath-2.0.0.tar.gz" + if wheel is None or not sdist.is_file() or not manifest.is_file(): + raise SystemExit("bundle must contain the 2.0.0 wheel, sdist, and manifest") + + _run( + sys.executable, + str(repository_root / "scripts/release/verify_evidence.py"), + str(manifest), + "--artifact-root", + str(bundle), + ) + _run( + sys.executable, + str(repository_root / "scripts/release/verify_pypi_files.py"), + "--packages-dir", + str(packages_dir), + "--version", + "2.0.0", + ) + distributions = [str(sdist), str(wheel)] + _run( + "uvx", + "--from", + f"twine=={TWINE_VERSION}", + "twine", + "check", + *distributions, + ) + + print("The token will be requested by Twine and will not be stored by this script.") + if input(f'Type "{CONFIRMATION}" to continue: ') != CONFIRMATION: + raise SystemExit("publication cancelled") + + _run(*twine_upload_command(distributions), env=twine_environment(os.environ)) + _run( + sys.executable, + str(repository_root / "scripts/release/verify_pypi_files.py"), + "--packages-dir", + str(packages_dir), + "--version", + "2.0.0", + "--require-complete", + "--attempts", + "12", + "--delay-seconds", + "10", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/verify_evidence.py b/scripts/release/verify_evidence.py index 7ffdcf3..16674a5 100644 --- a/scripts/release/verify_evidence.py +++ b/scripts/release/verify_evidence.py @@ -121,6 +121,12 @@ def _project_version() -> str: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("manifest", type=Path) + parser.add_argument( + "--artifact-root", + type=Path, + default=Path("."), + help="root directory used to resolve artifact paths from the manifest", + ) args = parser.parse_args() manifest = json.loads(args.manifest.read_text(encoding="utf-8")) @@ -142,13 +148,13 @@ def main() -> None: if name == "image": assert re.fullmatch(r"sha256:[0-9a-f]{64}", artifact["digest"]) continue - path = Path(artifact["path"]) + path = args.artifact_root / artifact["path"] assert path.is_file(), path assert path.stat().st_size == artifact["size"], path assert _sha256(path) == artifact["sha256"], path if manifest["release_stage"] == "ga": - approval_path = Path(manifest["artifacts"]["approval"]["path"]) + approval_path = args.artifact_root / manifest["artifacts"]["approval"]["path"] workflow = manifest.get("workflow") assert isinstance(workflow, dict) workflow_repository = workflow.get("repository") diff --git a/scripts/release/verify_release_assets.py b/scripts/release/verify_release_assets.py new file mode 100644 index 0000000..071d9b9 --- /dev/null +++ b/scripts/release/verify_release_assets.py @@ -0,0 +1,55 @@ +"""Verify an existing GitHub Release has the exact expected assets.""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def verify_assets(actual_directory: Path, expected_paths: list[Path]) -> None: + """Reject duplicate names, missing assets, extra assets, or hash mismatches.""" + expected: dict[str, Path] = {} + for path in expected_paths: + if not path.is_file(): + raise ValueError(f"expected release asset is missing: {path}") + if path.name in expected: + raise ValueError(f"duplicate expected release asset name: {path.name}") + expected[path.name] = path + + actual = {path.name: path for path in actual_directory.iterdir() if path.is_file()} + missing = sorted(set(expected) - set(actual)) + extra = sorted(set(actual) - set(expected)) + if missing: + raise ValueError("GitHub Release is missing assets: " + ", ".join(missing)) + if extra: + raise ValueError("GitHub Release has unexpected assets: " + ", ".join(extra)) + mismatched = sorted( + name for name in expected if _sha256(expected[name]) != _sha256(actual[name]) + ) + if mismatched: + raise ValueError("GitHub Release asset hash mismatch: " + ", ".join(mismatched)) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("actual_directory", type=Path) + parser.add_argument("expected", type=Path, nargs="+") + args = parser.parse_args() + try: + verify_assets(args.actual_directory, args.expected) + except (OSError, ValueError) as error: + raise SystemExit(str(error)) from error + print(f"verified {len(args.expected)} existing GitHub Release assets") + + +if __name__ == "__main__": + main() diff --git a/tests/deployment/test_manual_pypi_publish.py b/tests/deployment/test_manual_pypi_publish.py new file mode 100644 index 0000000..2c51870 --- /dev/null +++ b/tests/deployment/test_manual_pypi_publish.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from scripts.release.publish_pypi_manual import ( + twine_environment, + twine_upload_command, +) + + +def test_manual_upload_command_prompts_without_exposing_a_token() -> None: + command = twine_upload_command(["openrath.whl", "openrath.tar.gz"]) + + assert command[:4] == ["uvx", "--from", "twine==6.2.0", "twine"] + assert "__token__" in command + assert "--password" not in command + assert "--skip-existing" in command + assert not any(value.startswith("pypi-") for value in command) + + +def test_manual_upload_environment_removes_credential_overrides() -> None: + environment = twine_environment( + { + "PATH": "kept", + "TWINE_PASSWORD": "secret", + "TWINE_USERNAME": "wrong", + "TWINE_REPOSITORY": "wrong", + "TWINE_REPOSITORY_URL": "wrong", + } + ) + + assert environment["PATH"] == "kept" + assert environment["PYTHON_KEYRING_BACKEND"] == "keyring.backends.null.Keyring" + assert not any(name.startswith("TWINE_") for name in environment) diff --git a/tests/deployment/test_release_assets.py b/tests/deployment/test_release_assets.py new file mode 100644 index 0000000..57dd897 --- /dev/null +++ b/tests/deployment/test_release_assets.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from scripts.release.verify_release_assets import verify_assets + + +def test_release_assets_require_exact_names_and_hashes(tmp_path: Path) -> None: + expected_dir = tmp_path / "expected" + actual_dir = tmp_path / "actual" + expected_dir.mkdir() + actual_dir.mkdir() + expected = expected_dir / "openrath.whl" + actual = actual_dir / "openrath.whl" + expected.write_bytes(b"same") + actual.write_bytes(b"same") + + verify_assets(actual_dir, [expected]) + + actual.write_bytes(b"different") + with pytest.raises(ValueError, match="hash mismatch"): + verify_assets(actual_dir, [expected]) + + +def test_release_assets_reject_extra_and_duplicate_names(tmp_path: Path) -> None: + actual_dir = tmp_path / "actual" + actual_dir.mkdir() + first = tmp_path / "one" / "asset.json" + second = tmp_path / "two" / "asset.json" + first.parent.mkdir() + second.parent.mkdir() + first.write_text("one", encoding="utf-8") + second.write_text("two", encoding="utf-8") + + with pytest.raises(ValueError, match="duplicate expected"): + verify_assets(actual_dir, [first, second]) + + (actual_dir / "extra.json").write_text("extra", encoding="utf-8") + with pytest.raises(ValueError, match="missing assets"): + verify_assets(actual_dir, [first]) diff --git a/tests/deployment/test_release_version.py b/tests/deployment/test_release_version.py index 27178d5..2b10f8d 100644 --- a/tests/deployment/test_release_version.py +++ b/tests/deployment/test_release_version.py @@ -42,29 +42,41 @@ def test_release_candidate_workflow_is_digest_and_evidence_bound() -> None: assert "PyPI" not in workflow -def test_ga_workflow_is_protected_evidence_bound_and_uses_trusted_publishing() -> None: - workflow = Path(".github/workflows/release-v2-ga.yml").read_text(encoding="utf-8") - assert "workflow_dispatch:" in workflow - assert "evidence_run_id:" in workflow - assert "confirmation:" in workflow - assert "name: ga-release" in workflow - assert "scripts/release/verify_gate_reports.py" in workflow - assert "--stage ga" in workflow - assert "--approval" in workflow - assert "openrath-v2.0.0-ga-input" in workflow - assert "actions: read" in workflow - assert "packages: write" in workflow - assert "attestations: write" in workflow - assert "id-token: write" in workflow - assert ( - "pypa/gh-action-pypi-publish" - "@dc37677b2e1c63e2034f94d8a5b11f265b73ba33" in workflow +def test_ga_workflows_separate_preparation_manual_pypi_and_finalization() -> None: + prepare = Path(".github/workflows/release-v2-ga.yml").read_text(encoding="utf-8") + finalize = Path(".github/workflows/release-v2-ga-finalize.yml").read_text( + encoding="utf-8" ) - assert "scripts/release/verify_pypi_files.py" in workflow - assert "skip-existing: true" in workflow - assert "--require-complete" in workflow - assert "gh release create" in workflow - assert "--prerelease" not in workflow + workflows = prepare + finalize + + assert "workflow_dispatch:" in prepare + assert "evidence_run_id:" in prepare + assert "name: ga-release" in prepare + assert "scripts/release/verify_gate_reports.py" in prepare + assert "--stage ga" in prepare + assert "--approval" in prepare + assert "openrath-v2.0.0-ga-input" in prepare + assert "openrath-2.0.0-ga-candidate" in workflows + assert "attestations: write" in prepare + assert "pypa/gh-action-pypi-publish" not in workflows + assert "TWINE_PASSWORD" not in workflows + assert "id-token: write" in prepare + assert "id-token: write" not in finalize + + assert "preparation_run_id:" in finalize + assert "workflowName" in finalize + assert "--artifact-root release-bundle" in finalize + assert "scripts/release/verify_pypi_files.py" in finalize + assert "--require-complete" in finalize + assert "imagetools create" in finalize + assert "gh release create" in finalize + assert "--prerelease" not in workflows + + manual = Path("scripts/release/publish_pypi_manual.py").read_text(encoding="utf-8") + assert '"__token__"' in manual + assert '"--password"' not in manual + assert "twine=={TWINE_VERSION}" in manual + assert Path("release/manual-pypi-v2.0.0.md").is_file() def test_ga_release_documents_are_present_and_not_marked_as_drafts() -> None: @@ -72,5 +84,6 @@ def test_ga_release_documents_are_present_and_not_marked_as_drafts() -> None: checklist = Path("release/checklists/v2.0.0-ga.md").read_text(encoding="utf-8") assert "OpenRath v2.0.0" in notes assert "Gate C" in checklist + assert "Trusted Publishing" not in checklist for marker in ("TODO", "HOLD", "DRAFT"): assert marker not in notes From 660bc23ee524cd4b81837c8fb4d6d1e539bf3964 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Thu, 30 Jul 2026 13:50:12 +0800 Subject: [PATCH 21/22] docs: design the v2 Gate C evidence boundary --- .../docs/plans/2026-07-30-ga-gate-c-design.md | 60 ++++++++++ .../2026-07-30-ga-gate-c-implementation.md | 103 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 deploy/docs/plans/2026-07-30-ga-gate-c-design.md create mode 100644 deploy/docs/plans/2026-07-30-ga-gate-c-implementation.md diff --git a/deploy/docs/plans/2026-07-30-ga-gate-c-design.md b/deploy/docs/plans/2026-07-30-ga-gate-c-design.md new file mode 100644 index 0000000..dc6e314 --- /dev/null +++ b/deploy/docs/plans/2026-07-30-ga-gate-c-design.md @@ -0,0 +1,60 @@ +# OpenRath v2.0.0 Gate C Design + +## Objective and trust boundary + +Gate C must turn target-environment observations into a reproducible, +same-commit artifact without treating local tests, handwritten status, or a +short rehearsal as production evidence. The release preparation workflow may +consume only an artifact produced by the dedicated `Collect v2.0.0 Gate C +evidence` workflow. That workflow must run from `main`, on the exact candidate +commit, in a protected `ga-evidence` environment, and on a runner labelled +`openrath-ga`. + +The target runner owns infrastructure access. GitHub-hosted release jobs do not +receive Kubernetes, provider, database, or object-store credentials. Operators +place a bundle below a configured runner-local evidence root. The bundle name +is a restricted identifier, not an arbitrary path. The collector copies the +bundle to an isolated temporary directory, validates every report and referenced +file, and uploads it as `openrath-v2.0.0-ga-input`. + +The GA preparation workflow independently verifies the source SHA, workflow +name, event type, branch, report semantics, and referenced hashes before it +builds any public candidate. + +## Reports and target tooling + +Six reports use `openrath.ga-gate-report/1`: tests, live adapters, performance, +soak, drills, and compatibility. Every report identifies the exact source +commit, a timezone-aware generation time, a target-like environment profile, +structured details, open risks, and at least one evidence file. Evidence +entries are relative paths with byte size and SHA-256; absolute paths, +traversal, symlinks, missing files, and hash mismatches fail closed. + +`record_gate.py` records tests, live-adapter, drill, and compatibility outcomes +from immutable logs produced by the approved operator commands. +`load_v2.py` executes bounded authenticated HTTP lifecycle load against a +deployed OpenRath API without printing credentials. Separate single-host and +one/two/four-worker samples are combined by `build_performance_report.py`, +which calculates four-worker scaling efficiency. + +The existing local SQLite soak remains a review tool. Target soak evidence is +recorded from an eight-hour run plus resource snapshots and must explicitly +state zero errors and no unexplained growth. Drill recording never injects a +fault itself: destructive PostgreSQL, Redis, S3, API, worker, backup/restore, +and rollout/rollback operations remain operator-controlled and require +target-specific runbooks. + +## Recovery and verification + +Collector reruns are immutable at the report level: all files are copied and +hashed before upload, and a changed bundle produces a different artifact. A +failed or cancelled run cannot authorize preparation. Release preparation +checks the collector workflow identity instead of accepting an artifact from +any successful workflow. + +Unit tests cover path containment, symlink rejection, evidence hashing, gate +semantics, performance calculations, target/rehearsal separation, and workflow +identity checks. Actionlint and zizmor validate the workflows. Short local +HTTP tests validate the load client, while real Gate C execution remains +blocked until the approved secrets, target cluster, and protected runner are +available. diff --git a/deploy/docs/plans/2026-07-30-ga-gate-c-implementation.md b/deploy/docs/plans/2026-07-30-ga-gate-c-implementation.md new file mode 100644 index 0000000..1a612c5 --- /dev/null +++ b/deploy/docs/plans/2026-07-30-ga-gate-c-implementation.md @@ -0,0 +1,103 @@ +# OpenRath v2.0.0 Gate C Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build a fail-closed target-evidence toolchain and dedicated workflow +that produces the exact same-SHA Gate C artifact required by the GA preparation +workflow. + +**Architecture:** Target-side tools record immutable evidence files and six +strict gate reports. A protected self-hosted collector validates and uploads +the bundle; the GA preparation workflow accepts only that workflow identity +and exact candidate SHA. + +**Tech Stack:** Python 3.12, httpx, pytest, GitHub Actions, pinned GitHub +actions, actionlint, zizmor. + +--- + +### Task 1: Harden the Gate C report contract + +**Files:** +- Modify: `scripts/release/verify_gate_reports.py` +- Create: `scripts/release/record_gate.py` +- Modify: `release/evidence/schema/ga-gate-report.schema.json` +- Modify: `tests/deployment/test_ga_gate_reports.py` + +**Steps:** +1. Add failing tests for target-like environment requirements, evidence file + hashes, traversal, symlinks, and missing evidence. +2. Run `uv run pytest tests/deployment/test_ga_gate_reports.py -q`. +3. Implement strict evidence validation and the reusable report recorder. +4. Run the targeted test again and confirm it passes. + +### Task 2: Add bounded target load and performance reporting + +**Files:** +- Create: `scripts/release/load_v2.py` +- Create: `scripts/release/build_performance_report.py` +- Create: `tests/deployment/test_release_load.py` + +**Steps:** +1. Add tests for authentication redaction, lifecycle completion, sample + validation, profile coverage, and scaling efficiency. +2. Implement an httpx lifecycle load client that writes raw JSON samples. +3. Implement a combiner requiring single-host plus split one/two/four-worker + samples and zero errors. +4. Run `uv run pytest tests/deployment/test_release_load.py -q`. + +### Task 3: Add target soak and drill recording + +**Files:** +- Create: `scripts/release/build_soak_report.py` +- Create: `scripts/release/build_drill_report.py` +- Create: `deploy/docs/drills-v2.md` +- Create: `tests/deployment/test_release_operations_evidence.py` + +**Steps:** +1. Add failing tests for the 28,800-second minimum, zero-error requirement, + resource-growth decision, complete fault matrix, backup/restore, and + rollout/rollback. +2. Implement report builders that bind raw logs and resource snapshots without + executing destructive operations. +3. Document exact operator evidence and stop conditions. +4. Run the targeted operations-evidence tests. + +### Task 4: Add the protected collector workflow + +**Files:** +- Create: `.github/workflows/collect-v2-ga-evidence.yml` +- Modify: `.github/workflows/release-v2-ga.yml` +- Modify: `tests/deployment/test_release_version.py` +- Modify: `release/checklists/v2.0.0-ga.md` + +**Steps:** +1. Add workflow contract tests requiring `main`, the exact SHA, the protected + environment, fixed self-hosted labels, safe bundle identifiers, and the + exact artifact name. +2. Implement the collector with pinned actions and minimal permissions. +3. Restrict GA preparation to a successful workflow-dispatch run named + `Collect v2.0.0 Gate C evidence` on `main`. +4. Run workflow contract tests, actionlint, and zizmor. + +### Task 5: Verify and publish the PR update + +**Files:** +- Modify: PR #51 description + +**Steps:** +1. Run Ruff, MyPy, targeted tests, and the complete non-external suite. +2. Run actionlint, zizmor, YAML parsing, and `git diff --check`. +3. Commit the exact Gate C files and push the existing PR branch. +4. Update PR #51 with the new trust boundary and validation results. +5. Monitor all PR checks to completion. + +### External handoff + +1. Configure approved Provider/OpenViking secrets without sharing values. +2. Register a Linux self-hosted runner labelled `openrath-ga`. +3. Configure the protected `ga-evidence` environment and its evidence-root + variable. +4. Provision the target cluster and run the documented 1/2/4-worker, eight-hour + soak, and recovery drills on the final SHA. +5. Dispatch the collector and provide its run ID to GA preparation. From b6713431ea0c2e8bb1880b9ccca71c2f39c33d47 Mon Sep 17 00:00:00 2001 From: Tokisakix <2116884726@qq.com> Date: Thu, 30 Jul 2026 13:50:35 +0800 Subject: [PATCH 22/22] release: add protected Gate C evidence collection --- .github/actionlint.yaml | 3 + .github/workflows/collect-v2-ga-evidence.yml | 95 +++++++ .github/workflows/release-v2-ga.yml | 16 +- deploy/docs/drills-v2.md | 113 ++++++++ release/checklists/v2.0.0-ga.md | 5 +- .../schema/ga-gate-report.schema.json | 39 ++- scripts/release/build_drill_report.py | 143 ++++++++++ scripts/release/build_performance_report.py | 164 +++++++++++ scripts/release/build_soak_report.py | 148 ++++++++++ scripts/release/load_v2.py | 259 ++++++++++++++++++ scripts/release/record_gate.py | 145 ++++++++++ scripts/release/verify_gate_bundle.py | 89 ++++++ scripts/release/verify_gate_reports.py | 90 +++++- tests/deployment/test_ga_gate_reports.py | 128 ++++++++- tests/deployment/test_gate_bundle_security.py | 69 +++++ tests/deployment/test_release_load.py | 179 ++++++++++++ .../test_release_operations_evidence.py | 229 ++++++++++++++++ tests/deployment/test_release_version.py | 26 ++ 18 files changed, 1924 insertions(+), 16 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/collect-v2-ga-evidence.yml create mode 100644 deploy/docs/drills-v2.md create mode 100644 scripts/release/build_drill_report.py create mode 100644 scripts/release/build_performance_report.py create mode 100644 scripts/release/build_soak_report.py create mode 100644 scripts/release/load_v2.py create mode 100644 scripts/release/record_gate.py create mode 100644 scripts/release/verify_gate_bundle.py create mode 100644 tests/deployment/test_gate_bundle_security.py create mode 100644 tests/deployment/test_release_load.py create mode 100644 tests/deployment/test_release_operations_evidence.py diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..cc0c02c --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - openrath-ga diff --git a/.github/workflows/collect-v2-ga-evidence.yml b/.github/workflows/collect-v2-ga-evidence.yml new file mode 100644 index 0000000..f1fc5e3 --- /dev/null +++ b/.github/workflows/collect-v2-ga-evidence.yml @@ -0,0 +1,95 @@ +name: Collect v2.0.0 Gate C evidence + +on: + workflow_dispatch: + inputs: + bundle_id: + description: Runner-local bundle directory name below the configured evidence root + required: true + type: string + confirmation: + description: Type "collect v2.0.0 gate c" + required: true + type: string + +concurrency: + group: collect-openrath-v2.0.0-${{ github.sha }} + cancel-in-progress: false + +permissions: {} + +jobs: + collect: + name: Validate and collect target evidence + runs-on: [self-hosted, linux, openrath-ga] + timeout-minutes: 15 + environment: + name: ga-evidence + url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + permissions: + contents: read # Check out the exact candidate source for its verifier. + env: + BUNDLE_ID: ${{ inputs.bundle_id }} + EVIDENCE_ROOT: ${{ vars.OPENRATH_GA_EVIDENCE_ROOT }} + RELEASE_CONFIRMATION: ${{ inputs.confirmation }} + VERSION: 2.0.0 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - name: Validate candidate and protected target bundle + shell: bash + run: | + set -euo pipefail + test "$GITHUB_REF" = "refs/heads/main" + test "$RELEASE_CONFIRMATION" = "collect v2.0.0 gate c" + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + git fetch origin main + test "$(git rev-parse origin/main)" = "$GITHUB_SHA" + project_version="$(python3 - <<'PY' + import re + from pathlib import Path + text = Path("pyproject.toml").read_text(encoding="utf-8") + print(re.search(r'^version = "([^"]+)"$', text, re.MULTILINE).group(1)) + PY + )" + test "$project_version" = "$VERSION" + [[ "$BUNDLE_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] + test -n "$EVIDENCE_ROOT" + [[ "$EVIDENCE_ROOT" = /* ]] + + root_real="$(realpath "$EVIDENCE_ROOT")" + source_real="$(realpath "$root_real/$BUNDLE_ID")" + case "$source_real" in + "$root_real"/*) ;; + *) + echo "Evidence bundle escapes the configured root." >&2 + exit 1 + ;; + esac + test -d "$source_real" + if find "$source_real" -type l -print -quit | grep -q .; then + echo "Evidence bundles must not contain symbolic links." >&2 + exit 1 + fi + bundle_kib="$(du -sk "$source_real" | awk '{print $1}')" + test "$bundle_kib" -le 2097152 + + destination="$RUNNER_TEMP/gates" + mkdir "$destination" + cp -a --no-preserve=ownership "$source_real/." "$destination/" + python3 scripts/release/verify_gate_bundle.py "$destination" + python3 scripts/release/verify_gate_reports.py \ + "$destination" \ + --source-commit "$GITHUB_SHA" \ + --artifact-root "$destination" + - name: Upload immutable Gate C input + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: openrath-v2.0.0-ga-input + path: ${{ runner.temp }}/gates + retention-days: 90 + if-no-files-found: error + compression-level: 6 diff --git a/.github/workflows/release-v2-ga.yml b/.github/workflows/release-v2-ga.yml index 6b1ab75..3decda9 100644 --- a/.github/workflows/release-v2-ga.yml +++ b/.github/workflows/release-v2-ga.yml @@ -79,11 +79,16 @@ jobs: GH_TOKEN: ${{ github.token }} run: | [[ "$EVIDENCE_RUN_ID" =~ ^[1-9][0-9]*$ ]] - run_json="$(gh run view "$EVIDENCE_RUN_ID" \ - --repo "$GITHUB_REPOSITORY" \ - --json headSha,conclusion)" + run_json="$(gh api \ + "repos/$GITHUB_REPOSITORY/actions/runs/$EVIDENCE_RUN_ID")" test "$(jq -r .conclusion <<<"$run_json")" = "success" - test "$(jq -r .headSha <<<"$run_json")" = "$SOURCE_SHA" + test "$(jq -r .event <<<"$run_json")" = "workflow_dispatch" + test "$(jq -r .head_branch <<<"$run_json")" = "main" + test "$(jq -r .head_sha <<<"$run_json")" = "$SOURCE_SHA" + test "$(jq -r .name <<<"$run_json")" = \ + "Collect v2.0.0 Gate C evidence" + test "$(jq -r .path <<<"$run_json")" = \ + ".github/workflows/collect-v2-ga-evidence.yml" gate_dir="release/evidence/$VERSION/gates" mkdir -p "$gate_dir" gh run download "$EVIDENCE_RUN_ID" \ @@ -92,7 +97,8 @@ jobs: --dir "$gate_dir" uv run python scripts/release/verify_gate_reports.py \ "$gate_dir" \ - --source-commit "$SOURCE_SHA" + --source-commit "$SOURCE_SHA" \ + --artifact-root "$gate_dir" - name: Record protected-environment approval env: GH_TOKEN: ${{ github.token }} diff --git a/deploy/docs/drills-v2.md b/deploy/docs/drills-v2.md new file mode 100644 index 0000000..63a4cd6 --- /dev/null +++ b/deploy/docs/drills-v2.md @@ -0,0 +1,113 @@ +# OpenRath v2 target validation and recovery drills + +These procedures produce Gate C evidence for one immutable source commit. Run +them only in an approved, isolated target-like environment with an operations +owner, a rollback decision maker, tested backups, and a documented maintenance +window. A Docker Desktop rehearsal does not satisfy this gate. + +## Evidence directory + +Create one runner-local directory below the configured +`OPENRATH_GA_EVIDENCE_ROOT`. Its leaf name is the bundle ID used when +dispatching `Collect v2.0.0 Gate C evidence`. + +```text +// + tests.json + live-adapters.json + performance.json + soak.json + drills.json + compatibility.json + evidence/ + ... +``` + +Every command and target observation must be written below `evidence/`. +Never place credentials, bearer headers, kubeconfig content, database +passwords, or provider responses containing user data in these files. The +collector accepts only UTF-8 JSON/log/text/XML/CSV files and rejects common +credential patterns before upload. + +## Live adapters + +Capture logs for the required live Provider, OpenSandbox, and OpenViking +lifecycle tests. Skips, `continue-on-error`, or a successful offline-only run +are failures. Record the gate with `record_gate.py`; the details object must +set `provider`, `opensandbox`, and `openviking` to `passed`. + +## Capacity and scaling + +Deploy the same image digest and source commit for all samples. Run +`load_v2.py` for a minimum of five minutes for: + +1. single-host, one embedded worker; +2. split profile, one worker replica; +3. split profile, two worker replicas; +4. split profile, four worker replicas. + +The authentication token is read from `OPENRATH_TOKEN`; it is never a command +argument or report field. Combine the four raw samples with +`build_performance_report.py`. Four-worker throughput must be at least 70% of +linear scaling from the one-worker split baseline. + +## Eight-hour soak + +Run `load_v2.py` against the split target for at least 28,800 seconds without +`--max-runs`. Capture resource snapshots before warm-up and after completion. +Each snapshot uses: + +```json +{ + "schema": "openrath.v2.resource-snapshot/1", + "source_commit": "<40 hex>", + "phase": "before", + "captured_at": "", + "components": { + "api": {"memory_bytes": 0, "restarts": 0}, + "worker": {"memory_bytes": 0, "restarts": 0}, + "postgres": {"connections": 0, "storage_bytes": 0} + } +} +``` + +An operations owner compares time-series telemetry, queue age, database +connections/storage, pod restarts, memory, and error logs. The signed-off +assessment must use `openrath.v2.resource-assessment/1`, identify the same +commit and assessor, explain the observed delta, and set +`unexplained_resource_growth` to `false`. Build `soak.json` with +`build_soak_report.py`. + +## Fault and recovery matrix + +Stop immediately if data integrity is uncertain, the backup cannot be read, a +non-idempotent effect is replayed automatically, or rollback cannot complete. +Record start/end timestamps, operator, recovery time, observed state, and data +loss for every drill: + +1. terminate an active worker and verify lease expiry, fencing, and requeue; +2. terminate an API replica and verify readiness, reconnect, and SSE resume; +3. interrupt PostgreSQL and verify readiness becomes 503 with no false success; +4. interrupt Redis and verify durable state remains in PostgreSQL; +5. restart S3/object storage and verify hashes and bounded retry semantics; +6. restore PostgreSQL and artifacts into an isolated namespace, run + `openrath-migrate --check`, and verify RPO 0 / RTO at most 60 minutes; +7. roll from the previous supported release to the candidate, roll the + application back while retaining additive schema, then roll forward again. + +Put the structured results in `openrath.v2.drill-results/1` and bind the raw +operator logs with `build_drill_report.py`. The builder records evidence only; +it never injects faults. + +## Collection + +Verify locally on the target runner: + +```bash +uv run python scripts/release/verify_gate_reports.py \ + "$OPENRATH_GA_EVIDENCE_ROOT/" \ + --source-commit "$(git rev-parse HEAD)" +``` + +Then dispatch the protected collector from `main`. Do not move, edit, or reuse +the bundle after collection. diff --git a/release/checklists/v2.0.0-ga.md b/release/checklists/v2.0.0-ga.md index 311fb5b..df63c81 100644 --- a/release/checklists/v2.0.0-ga.md +++ b/release/checklists/v2.0.0-ga.md @@ -16,7 +16,10 @@ Every machine-readable report must identify the exact final source commit. - [ ] PostgreSQL, Redis, S3, API, and worker fault matrix passes. - [ ] Target-cluster backup/restore and rollout/rollback drills pass. - [ ] API stability, v1 maintenance window, and migration review pass. -- [ ] The evidence workflow run is successful and uses the final source commit. +- [ ] The protected `Collect v2.0.0 Gate C evidence` workflow succeeds on the + final `main` source commit using the approved `openrath-ga` target runner. +- [ ] Every report evidence file is contained in that workflow artifact and + matches its recorded size and SHA-256. ## Release governance diff --git a/release/evidence/schema/ga-gate-report.schema.json b/release/evidence/schema/ga-gate-report.schema.json index 51f6f0b..b4e1b41 100644 --- a/release/evidence/schema/ga-gate-report.schema.json +++ b/release/evidence/schema/ga-gate-report.schema.json @@ -41,14 +41,47 @@ }, "environment": { "type": "object", - "minProperties": 1 + "required": [ + "profile", + "target_like" + ], + "properties": { + "profile": { + "type": "string", + "minLength": 1 + }, + "target_like": { + "const": true + } + }, + "additionalProperties": true }, "evidence": { "type": "array", "minItems": 1, "items": { - "type": "string", - "minLength": 1 + "type": "object", + "required": [ + "path", + "sha256", + "size" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\\\:]+$" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "size": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false } }, "open_risks": { diff --git a/scripts/release/build_drill_report.py b/scripts/release/build_drill_report.py new file mode 100644 index 0000000..a330877 --- /dev/null +++ b/scripts/release/build_drill_report.py @@ -0,0 +1,143 @@ +"""Build the OpenRath GA drill gate from operator-controlled target exercises.""" + +from __future__ import annotations + +import argparse +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +try: + from .record_gate import build_report as build_gate_report +except ImportError: # pragma: no cover - direct script execution + from record_gate import build_report as build_gate_report + +FAULT_DRILLS = frozenset( + { + "postgresql_failure", + "redis_failure", + "s3_failure", + "api_failure", + "worker_failure", + } +) +REQUIRED_DRILLS = FAULT_DRILLS | {"backup_restore", "rollout_rollback"} + + +def _object(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"drill results must be a JSON object: {path}") + return value + + +def _timestamp(value: object, *, field: str, drill: str) -> datetime: + if not isinstance(value, str) or not value: + raise ValueError(f"drill {drill} requires {field}") + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as error: + raise ValueError(f"drill {drill} has invalid {field}") from error + if parsed.tzinfo is None: + raise ValueError(f"drill {drill} {field} requires a timezone") + return parsed + + +def _validate_drill(name: str, value: object) -> None: + if not isinstance(value, dict): + raise ValueError(f"drill result must be an object: {name}") + if value.get("status") != "passed": + raise ValueError(f"drill did not pass: {name}") + for field in ("operator", "observed"): + if not isinstance(value.get(field), str) or not value[field]: + raise ValueError(f"drill {name} requires {field}") + started = _timestamp(value.get("started_at"), field="started_at", drill=name) + completed = _timestamp(value.get("completed_at"), field="completed_at", drill=name) + if completed < started: + raise ValueError(f"drill {name} completion precedes its start") + recovery = value.get("recovery_seconds") + if ( + isinstance(recovery, bool) + or not isinstance(recovery, (int, float)) + or recovery < 0 + ): + raise ValueError(f"drill {name} requires non-negative recovery_seconds") + if value.get("data_loss_records") != 0: + raise ValueError(f"drill {name} reports data loss") + if name == "backup_restore" and recovery > 3600: + raise ValueError("backup_restore recovery_seconds must not exceed 3600") + + +def build_report( + *, + results_path: Path, + evidence_paths: list[Path], + evidence_root: Path, + generated_at: str | None = None, +) -> dict[str, Any]: + """Validate the complete target drill matrix and bind its logs.""" + results = _object(results_path) + if results.get("schema") != "openrath.v2.drill-results/1": + raise ValueError("unsupported drill results schema") + source_commit = results.get("source_commit") + if ( + not isinstance(source_commit, str) + or re.fullmatch(r"[0-9a-f]{40}", source_commit) is None + ): + raise ValueError("drill results require a valid source_commit") + profile = results.get("environment_profile") + if not isinstance(profile, str) or not profile: + raise ValueError("drill results require an environment_profile") + drills = results.get("drills") + if not isinstance(drills, dict): + raise ValueError("drill results require a drills object") + missing = sorted(REQUIRED_DRILLS - set(drills)) + if missing: + raise ValueError("missing drills: " + ", ".join(missing)) + for name in sorted(REQUIRED_DRILLS): + _validate_drill(name, drills[name]) + + return build_gate_report( + gate="drills", + source_commit=source_commit, + environment={"profile": profile, "target_like": True}, + details={ + "fault_matrix": "passed", + "backup_restore": "passed", + "rollout_rollback": "passed", + }, + evidence_root=evidence_root, + evidence_files=[results_path, *evidence_paths], + open_risks=[], + generated_at=generated_at + or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--results", type=Path, required=True) + parser.add_argument("--evidence-root", type=Path, required=True) + parser.add_argument("--evidence-file", type=Path, action="append", default=[]) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + report = build_report( + results_path=args.results, + evidence_paths=args.evidence_file, + evidence_root=args.evidence_root, + ) + except (OSError, ValueError, json.JSONDecodeError) as error: + raise SystemExit(str(error)) from error + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(f"recorded drills Gate C report at {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release/build_performance_report.py b/scripts/release/build_performance_report.py new file mode 100644 index 0000000..e932431 --- /dev/null +++ b/scripts/release/build_performance_report.py @@ -0,0 +1,164 @@ +"""Combine target load samples into the OpenRath GA performance gate report.""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +try: + from .record_gate import build_report as build_gate_report +except ImportError: # pragma: no cover - direct script execution + from record_gate import build_report as build_gate_report + +REQUIRED_PROFILES = { + ("single_host", 1), + ("split", 1), + ("split", 2), + ("split", 4), +} + + +def _load_sample(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"load sample must be a JSON object: {path}") + if value.get("schema") != "openrath.v2.load-sample/1": + raise ValueError(f"unsupported load sample schema: {path}") + return value + + +def build_report( + *, + sample_paths: list[Path], + evidence_root: Path, + environment_profile: str, + minimum_sample_seconds: float = 300, + generated_at: str | None = None, +) -> dict[str, Any]: + """Build a passing report from single-host and 1/2/4-worker split samples.""" + if len(sample_paths) != 4: + raise ValueError("exactly four load samples are required") + samples: dict[tuple[str, int], dict[str, Any]] = {} + commits: set[str] = set() + for path in sample_paths: + sample = _load_sample(path) + profile = sample.get("profile") + replicas = sample.get("worker_replicas") + if ( + not isinstance(profile, str) + or isinstance(replicas, bool) + or not isinstance(replicas, int) + ): + raise ValueError(f"invalid profile or worker count: {path}") + key = (profile, replicas) + if key in samples: + raise ValueError(f"duplicate load profile: {profile}/{replicas}") + samples[key] = sample + commit = sample.get("source_commit") + if not isinstance(commit, str): + raise ValueError(f"load sample source_commit is missing: {path}") + commits.add(commit) + if sample.get("target_like") is not True: + raise ValueError(f"load sample is not target-like: {path}") + if sample.get("errors") != 0: + raise ValueError("performance samples must contain zero errors") + completed = sample.get("completed_runs") + if ( + isinstance(completed, bool) + or not isinstance(completed, int) + or completed < 1 + ): + raise ValueError("performance samples require at least one completed run") + duration = sample.get("duration_seconds") + if ( + isinstance(duration, bool) + or not isinstance(duration, (int, float)) + or duration < minimum_sample_seconds + ): + raise ValueError( + f"performance sample must run at least {minimum_sample_seconds} seconds" + ) + throughput = sample.get("throughput_runs_per_second") + if ( + isinstance(throughput, bool) + or not isinstance(throughput, (int, float)) + or throughput <= 0 + ): + raise ValueError(f"load sample throughput is missing: {path}") + if set(samples) != REQUIRED_PROFILES: + raise ValueError("samples must cover single-host and split 1/2/4 workers") + if len(commits) != 1: + raise ValueError("all performance samples must use the same source commit") + + split_one = float(samples[("split", 1)]["throughput_runs_per_second"]) + split_two = float(samples[("split", 2)]["throughput_runs_per_second"]) + split_four = float(samples[("split", 4)]["throughput_runs_per_second"]) + if split_one <= 0: + raise ValueError("one-worker throughput must be positive") + two_efficiency = split_two / (split_one * 2) + four_efficiency = split_four / (split_one * 4) + if four_efficiency < 0.70: + raise ValueError("four-worker scaling efficiency must be at least 0.70") + + details: dict[str, object] = { + "single_host": "passed", + "split_profile": "passed", + "worker_scaling_efficiency": four_efficiency, + "two_worker_scaling_efficiency": two_efficiency, + "throughput_runs_per_second": { + f"{profile}_{replicas}": sample["throughput_runs_per_second"] + for (profile, replicas), sample in sorted(samples.items()) + }, + } + source_commit = commits.pop() + return build_gate_report( + gate="performance", + source_commit=source_commit, + environment={"profile": environment_profile, "target_like": True}, + details=details, + evidence_root=evidence_root, + evidence_files=sample_paths, + open_risks=[], + generated_at=generated_at + or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--single-host", type=Path, required=True) + parser.add_argument("--split-one", type=Path, required=True) + parser.add_argument("--split-two", type=Path, required=True) + parser.add_argument("--split-four", type=Path, required=True) + parser.add_argument("--evidence-root", type=Path, required=True) + parser.add_argument("--environment-profile", required=True) + parser.add_argument("--minimum-sample-seconds", type=float, default=300) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + report = build_report( + sample_paths=[ + args.single_host, + args.split_one, + args.split_two, + args.split_four, + ], + evidence_root=args.evidence_root, + environment_profile=args.environment_profile, + minimum_sample_seconds=args.minimum_sample_seconds, + ) + except (OSError, ValueError, json.JSONDecodeError) as error: + raise SystemExit(str(error)) from error + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(f"recorded performance Gate C report at {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release/build_soak_report.py b/scripts/release/build_soak_report.py new file mode 100644 index 0000000..7c8eead --- /dev/null +++ b/scripts/release/build_soak_report.py @@ -0,0 +1,148 @@ +"""Build the OpenRath GA soak gate from target load and resource evidence.""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +try: + from .record_gate import build_report as build_gate_report +except ImportError: # pragma: no cover - direct script execution + from record_gate import build_report as build_gate_report + + +def _object(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"evidence must be a JSON object: {path}") + return value + + +def _same_commit(source_commit: object, *values: dict[str, Any]) -> str: + if not isinstance(source_commit, str): + raise ValueError("soak sample source_commit is missing") + if any(value.get("source_commit") != source_commit for value in values): + raise ValueError("all soak evidence must use the same source commit") + return source_commit + + +def build_report( + *, + sample_path: Path, + before_snapshot_path: Path, + after_snapshot_path: Path, + assessment_path: Path, + evidence_root: Path, + environment_profile: str, + generated_at: str | None = None, +) -> dict[str, Any]: + """Require an eight-hour error-free target run and an explained resource delta.""" + sample = _object(sample_path) + before = _object(before_snapshot_path) + after = _object(after_snapshot_path) + assessment = _object(assessment_path) + if sample.get("schema") != "openrath.v2.load-sample/1": + raise ValueError("unsupported soak load sample schema") + if sample.get("target_like") is not True or sample.get("profile") != "split": + raise ValueError("soak sample must use the target-like split profile") + source_commit = _same_commit( + sample.get("source_commit"), + before, + after, + assessment, + ) + snapshot_components: list[set[str]] = [] + for snapshot, phase in ((before, "before"), (after, "after")): + if snapshot.get("schema") != "openrath.v2.resource-snapshot/1": + raise ValueError(f"unsupported {phase} resource snapshot schema") + if snapshot.get("phase") != phase: + raise ValueError(f"resource snapshot phase must be {phase}") + components = snapshot.get("components") + if not isinstance(components, dict) or not components: + raise ValueError(f"{phase} resource snapshot requires components") + if any(not isinstance(value, dict) for value in components.values()): + raise ValueError(f"{phase} resource snapshot components must be objects") + snapshot_components.append(set(components)) + if snapshot_components[0] != snapshot_components[1]: + raise ValueError("resource snapshots must contain the same components") + if assessment.get("schema") != "openrath.v2.resource-assessment/1": + raise ValueError("unsupported resource assessment schema") + if ( + not isinstance(assessment.get("assessor"), str) + or not assessment["assessor"] + or not isinstance(assessment.get("rationale"), str) + or not assessment["rationale"] + ): + raise ValueError("resource assessment requires an assessor and rationale") + if assessment.get("unexplained_resource_growth") is not False: + raise ValueError("soak evidence contains unexplained resource growth") + duration = sample.get("duration_seconds") + if ( + isinstance(duration, bool) + or not isinstance(duration, (int, float)) + or duration < 28800 + ): + raise ValueError("soak duration_seconds must be at least 28800") + if sample.get("errors") != 0: + raise ValueError("soak sample must contain zero errors") + + details: dict[str, object] = { + "duration_seconds": duration, + "errors": 0, + "unexplained_resource_growth": False, + "completed_runs": sample.get("completed_runs"), + "resource_assessor": assessment["assessor"], + "resource_rationale": assessment["rationale"], + } + return build_gate_report( + gate="soak", + source_commit=source_commit, + environment={"profile": environment_profile, "target_like": True}, + details=details, + evidence_root=evidence_root, + evidence_files=[ + sample_path, + before_snapshot_path, + after_snapshot_path, + assessment_path, + ], + open_risks=[], + generated_at=generated_at + or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--sample", type=Path, required=True) + parser.add_argument("--before-snapshot", type=Path, required=True) + parser.add_argument("--after-snapshot", type=Path, required=True) + parser.add_argument("--assessment", type=Path, required=True) + parser.add_argument("--evidence-root", type=Path, required=True) + parser.add_argument("--environment-profile", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + report = build_report( + sample_path=args.sample, + before_snapshot_path=args.before_snapshot, + after_snapshot_path=args.after_snapshot, + assessment_path=args.assessment, + evidence_root=args.evidence_root, + environment_profile=args.environment_profile, + ) + except (OSError, ValueError, json.JSONDecodeError) as error: + raise SystemExit(str(error)) from error + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(f"recorded soak Gate C report at {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release/load_v2.py b/scripts/release/load_v2.py new file mode 100644 index 0000000..3c1da5a --- /dev/null +++ b/scripts/release/load_v2.py @@ -0,0 +1,259 @@ +"""Run bounded authenticated lifecycle load against a deployed OpenRath API.""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import re +import threading +import time +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit +from uuid import uuid4 + +import httpx + + +@dataclass(frozen=True) +class LoadConfig: + """Inputs for one immutable target load sample.""" + + base_url: str + token: str + source_commit: str + profile: str + worker_replicas: int + concurrency: int + duration_seconds: float + max_runs: int | None + poll_interval_seconds: float + request_timeout_seconds: float + run_timeout_seconds: float + target_like: bool + + +def _origin(value: str) -> str: + parsed = urlsplit(value) + if ( + parsed.scheme not in {"http", "https"} + or not parsed.hostname + or parsed.username + or parsed.password + or parsed.query + or parsed.fragment + ): + raise ValueError("base URL must be an HTTP(S) origin without credentials") + port = f":{parsed.port}" if parsed.port is not None else "" + return f"{parsed.scheme}://{parsed.hostname}{port}" + + +def _percentile(values: list[float], percentile: float) -> float: + if not values: + return 0.0 + ordered = sorted(values) + index = max(0, math.ceil(percentile * len(ordered)) - 1) + return ordered[index] + + +def _lifecycle( + client: httpx.Client, + *, + poll_interval_seconds: float, + run_timeout_seconds: float, +) -> tuple[bool, str | None, float]: + started = time.perf_counter() + try: + session = client.post("/v1/sessions") + session.raise_for_status() + session_id = session.json()["id"] + run = client.post( + "/v1/runs", + headers={"Idempotency-Key": f"ga-load-{uuid4()}"}, + json={ + "assistant_id": "echo", + "session_id": session_id, + "state": {"gate_c": True}, + }, + ) + run.raise_for_status() + run_id = run.json()["id"] + deadline = time.monotonic() + run_timeout_seconds + while True: + response = client.get(f"/v1/runs/{run_id}") + response.raise_for_status() + status = response.json().get("status") + if status == "succeeded": + return True, None, time.perf_counter() - started + if status in {"failed", "cancelled", "needs_review"}: + return False, f"run_{status}", time.perf_counter() - started + if time.monotonic() >= deadline: + return False, "run_timeout", time.perf_counter() - started + time.sleep(poll_interval_seconds) + except httpx.HTTPStatusError as error: + return ( + False, + f"http_{error.response.status_code}", + time.perf_counter() - started, + ) + except httpx.TimeoutException: + return False, "request_timeout", time.perf_counter() - started + except (httpx.HTTPError, KeyError, TypeError, ValueError) as error: + return False, type(error).__name__, time.perf_counter() - started + + +def _validate(config: LoadConfig) -> str: + if re.fullmatch(r"[0-9a-f]{40}", config.source_commit) is None: + raise ValueError("source_commit must be 40 lowercase hexadecimal characters") + if config.profile not in {"single_host", "split"}: + raise ValueError("profile must be single_host or split") + if config.worker_replicas < 1 or config.concurrency < 1: + raise ValueError("worker replicas and concurrency must be positive") + if config.duration_seconds <= 0: + raise ValueError("duration_seconds must be positive") + if config.max_runs is not None and config.max_runs < 1: + raise ValueError("max_runs must be positive") + if config.poll_interval_seconds < 0: + raise ValueError("poll_interval_seconds must be non-negative") + if config.request_timeout_seconds <= 0 or config.run_timeout_seconds <= 0: + raise ValueError("request and run timeouts must be positive") + if not config.token: + raise ValueError("an authentication token is required") + origin = _origin(config.base_url) + if config.target_like and not origin.startswith("https://"): + raise ValueError("target-like load requires an HTTPS base URL") + return origin + + +def run_sample( + config: LoadConfig, + *, + transport: httpx.BaseTransport | None = None, +) -> dict[str, Any]: + """Execute lifecycles until the duration or maximum run count is reached.""" + target_origin = _validate(config) + started = time.perf_counter() + deadline = started + config.duration_seconds + lock = threading.Lock() + reserved = 0 + completed = 0 + errors: dict[str, int] = {} + latencies: list[float] = [] + + def worker() -> None: + nonlocal reserved, completed + with httpx.Client( + base_url=target_origin, + headers={"Authorization": f"Bearer {config.token}"}, + timeout=config.request_timeout_seconds, + transport=transport, + trust_env=False, + ) as client: + while True: + with lock: + if time.perf_counter() >= deadline or ( + config.max_runs is not None and reserved >= config.max_runs + ): + return + reserved += 1 + success, error, latency = _lifecycle( + client, + poll_interval_seconds=config.poll_interval_seconds, + run_timeout_seconds=config.run_timeout_seconds, + ) + with lock: + latencies.append(latency) + if success: + completed += 1 + else: + key = error or "unknown" + errors[key] = errors.get(key, 0) + 1 + + with ThreadPoolExecutor(max_workers=config.concurrency) as executor: + futures = [executor.submit(worker) for _ in range(config.concurrency)] + for future in futures: + future.result() + duration = time.perf_counter() - started + return { + "schema": "openrath.v2.load-sample/1", + "source_commit": config.source_commit, + "profile": config.profile, + "target_like": config.target_like, + "worker_replicas": config.worker_replicas, + "concurrency": config.concurrency, + "duration_seconds": duration, + "attempted_runs": reserved, + "completed_runs": completed, + "errors": reserved - completed, + "error_kinds": dict(sorted(errors.items())), + "throughput_runs_per_second": completed / duration if duration else 0, + "latency_seconds": { + "p50": _percentile(latencies, 0.50), + "p95": _percentile(latencies, 0.95), + "p99": _percentile(latencies, 0.99), + }, + "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "target_origin": target_origin, + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--base-url", required=True) + parser.add_argument("--token-env", default="OPENRATH_TOKEN") + parser.add_argument("--source-commit", required=True) + parser.add_argument("--profile", choices=["single_host", "split"], required=True) + parser.add_argument("--worker-replicas", type=int, required=True) + parser.add_argument("--concurrency", type=int, default=16) + parser.add_argument("--duration-seconds", type=float, default=300) + parser.add_argument("--max-runs", type=int) + parser.add_argument("--poll-interval-seconds", type=float, default=0.2) + parser.add_argument("--request-timeout-seconds", type=float, default=20) + parser.add_argument("--run-timeout-seconds", type=float, default=120) + parser.add_argument("--target-like", action="store_true") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + token = os.environ.get(args.token_env, "") + if not token: + raise SystemExit( + f"required token environment variable is absent: {args.token_env}" + ) + try: + report = run_sample( + LoadConfig( + base_url=args.base_url, + token=token, + source_commit=args.source_commit, + profile=args.profile, + worker_replicas=args.worker_replicas, + concurrency=args.concurrency, + duration_seconds=args.duration_seconds, + max_runs=args.max_runs, + poll_interval_seconds=args.poll_interval_seconds, + request_timeout_seconds=args.request_timeout_seconds, + run_timeout_seconds=args.run_timeout_seconds, + target_like=args.target_like, + ) + ) + except ValueError as error: + raise SystemExit(str(error)) from error + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print( + f"completed {report['completed_runs']}/{report['attempted_runs']} " + f"lifecycles; report: {args.output}" + ) + if report["errors"]: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/record_gate.py b/scripts/release/record_gate.py new file mode 100644 index 0000000..b022f54 --- /dev/null +++ b/scripts/release/record_gate.py @@ -0,0 +1,145 @@ +"""Create one hash-bound OpenRath GA Gate C report.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +GATES = frozenset( + { + "tests", + "live_adapters", + "performance", + "soak", + "drills", + "compatibility", + } +) +CONFIRMATION = "record v2.0.0 gate c" + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _contained_file(root: Path, path: Path) -> tuple[Path, str]: + resolved_root = root.resolve(strict=True) + candidate = path if path.is_absolute() else resolved_root / path + relative = candidate.resolve(strict=True).relative_to(resolved_root) + cursor = resolved_root + for part in relative.parts: + cursor /= part + if cursor.is_symlink(): + raise ValueError(f"evidence path must not contain a symlink: {path}") + if not candidate.is_file(): + raise ValueError(f"evidence path is not a file: {path}") + return candidate, relative.as_posix() + + +def build_report( + *, + gate: str, + source_commit: str, + environment: dict[str, object], + details: dict[str, object], + evidence_root: Path, + evidence_files: list[Path], + open_risks: list[object], + generated_at: str | None = None, +) -> dict[str, Any]: + """Build a report that binds every evidence file by relative path and hash.""" + if gate not in GATES: + raise ValueError(f"unsupported GA gate: {gate}") + if re.fullmatch(r"[0-9a-f]{40}", source_commit) is None: + raise ValueError("source_commit must be 40 lowercase hexadecimal characters") + if not evidence_files: + raise ValueError("at least one evidence file is required") + entries: list[dict[str, object]] = [] + seen: set[str] = set() + for item in evidence_files: + path, relative = _contained_file(evidence_root, item) + if relative in seen: + raise ValueError(f"duplicate evidence path: {relative}") + seen.add(relative) + entries.append( + { + "path": relative, + "sha256": _sha256(path), + "size": path.stat().st_size, + } + ) + return { + "schema": "openrath.ga-gate-report/1", + "gate": gate, + "source_commit": source_commit, + "result": "passed", + "generated_at": generated_at + or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "environment": environment, + "evidence": entries, + "open_risks": open_risks, + "details": details, + } + + +def _object(path: Path, *, name: str) -> dict[str, object]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"{name} must contain a JSON object") + return value + + +def _array(path: Path, *, name: str) -> list[object]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, list): + raise ValueError(f"{name} must contain a JSON array") + return value + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--gate", choices=sorted(GATES), required=True) + parser.add_argument("--source-commit", required=True) + parser.add_argument("--profile", required=True) + parser.add_argument("--details", type=Path, required=True) + parser.add_argument("--evidence-root", type=Path, required=True) + parser.add_argument("--evidence-file", type=Path, action="append", required=True) + parser.add_argument("--open-risks", type=Path) + parser.add_argument("--target-like", action="store_true") + parser.add_argument("--confirmation", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.confirmation != CONFIRMATION: + raise SystemExit(f'--confirmation must be exactly "{CONFIRMATION}"') + report = build_report( + gate=args.gate, + source_commit=args.source_commit, + environment={"profile": args.profile, "target_like": args.target_like}, + details=_object(args.details, name="details"), + evidence_root=args.evidence_root, + evidence_files=args.evidence_file, + open_risks=( + _array(args.open_risks, name="open risks") + if args.open_risks is not None + else [] + ), + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(f"recorded {args.gate} Gate C report at {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release/verify_gate_bundle.py b/scripts/release/verify_gate_bundle.py new file mode 100644 index 0000000..f49a158 --- /dev/null +++ b/scripts/release/verify_gate_bundle.py @@ -0,0 +1,89 @@ +"""Reject unsafe files and likely credentials before Gate C artifact upload.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +ALLOWED_SUFFIXES = frozenset({".csv", ".json", ".log", ".txt", ".xml"}) +SENSITIVE_NAMES = frozenset( + { + ".env", + ".pypirc", + "credentials", + "credentials.json", + "kubeconfig", + } +) +MAX_FILE_BYTES = 50 * 1024 * 1024 +MAX_BUNDLE_BYTES = 2 * 1024 * 1024 * 1024 +SECRET_PATTERNS = ( + re.compile(r"Authorization\s*:\s*Bearer\s+\S+", re.IGNORECASE), + re.compile(r"pypi-[A-Za-z0-9_-]{20,}"), + re.compile(r"github_pat_[A-Za-z0-9_]{20,}"), + re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"), + re.compile(r"sk-[A-Za-z0-9_-]{20,}"), + re.compile(r"AKIA[0-9A-Z]{16}"), + re.compile(r"-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----"), + re.compile( + r"""["']?(?:api[_-]?key|access[_-]?token|secret[_-]?key|password)""" + r"""["']?\s*[:=]\s*["'][^"'\r\n]{8,}["']""", + re.IGNORECASE, + ), +) + + +def verify_bundle(root: Path) -> dict[str, int]: + """Return bundle size metadata after validating every file.""" + resolved_root = root.resolve(strict=True) + if root.is_symlink() or not resolved_root.is_dir(): + raise ValueError("Gate C bundle root must be a real directory") + files = 0 + total = 0 + for path in resolved_root.rglob("*"): + if path.is_symlink(): + raise ValueError(f"Gate C bundle contains a symbolic link: {path}") + if not path.is_file(): + continue + if path.name.lower() in SENSITIVE_NAMES: + raise ValueError( + f"Gate C bundle contains a sensitive filename: {path.name}" + ) + if path.suffix.lower() not in ALLOWED_SUFFIXES: + raise ValueError(f"Gate C bundle contains an unsupported extension: {path}") + size = path.stat().st_size + if size > MAX_FILE_BYTES: + raise ValueError(f"Gate C evidence file exceeds 50 MiB: {path}") + total += size + if total > MAX_BUNDLE_BYTES: + raise ValueError("Gate C bundle exceeds 2 GiB") + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError as error: + raise ValueError(f"Gate C evidence must be UTF-8 text: {path}") from error + for pattern in SECRET_PATTERNS: + if pattern.search(text): + raise ValueError(f"Gate C evidence contains a likely secret: {path}") + files += 1 + if files == 0: + raise ValueError("Gate C bundle contains no evidence files") + return {"files": files, "bytes": total} + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("bundle", type=Path) + args = parser.parse_args() + try: + summary = verify_bundle(args.bundle) + except (OSError, ValueError) as error: + raise SystemExit(str(error)) from error + print( + f"verified Gate C bundle safety: " + f"{summary['files']} files, {summary['bytes']} bytes" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/verify_gate_reports.py b/scripts/release/verify_gate_reports.py index e47d163..0cc7c8f 100644 --- a/scripts/release/verify_gate_reports.py +++ b/scripts/release/verify_gate_reports.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import hashlib import json import re from datetime import datetime @@ -24,11 +25,77 @@ def _require_passed(details: dict[str, object], *names: str) -> None: raise ValueError("required checks did not pass: " + ", ".join(failed)) +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _validate_evidence( + evidence: object, + *, + gate: str, + artifact_root: Path, +) -> None: + if not isinstance(evidence, list) or not evidence: + raise ValueError(f"{gate}: at least one evidence reference is required") + root = artifact_root.resolve(strict=True) + seen: set[str] = set() + for item in evidence: + if not isinstance(item, dict): + raise ValueError(f"{gate}: evidence entry must be an object") + relative = item.get("path") + expected_hash = item.get("sha256") + expected_size = item.get("size") + if ( + not isinstance(relative, str) + or not relative + or "\\" in relative + or ":" in relative + or Path(relative).is_absolute() + or ".." in Path(relative).parts + ): + raise ValueError(f"{gate}: evidence path must be a safe relative path") + if relative in seen: + raise ValueError(f"{gate}: duplicate evidence path: {relative}") + seen.add(relative) + if re.fullmatch(r"[0-9a-f]{64}", str(expected_hash or "")) is None: + raise ValueError(f"{gate}: evidence hash must be a SHA-256") + if ( + isinstance(expected_size, bool) + or not isinstance(expected_size, int) + or expected_size < 0 + ): + raise ValueError(f"{gate}: evidence size must be a non-negative integer") + candidate = root / relative + cursor = root + for part in Path(relative).parts: + cursor /= part + if cursor.is_symlink(): + raise ValueError(f"{gate}: evidence path must not contain a symlink") + try: + resolved = candidate.resolve(strict=True) + resolved.relative_to(root) + except (FileNotFoundError, ValueError) as error: + raise ValueError( + f"{gate}: evidence file is missing or outside the artifact root" + ) from error + if not resolved.is_file(): + raise ValueError(f"{gate}: evidence path is not a file: {relative}") + if resolved.stat().st_size != expected_size: + raise ValueError(f"{gate}: evidence size mismatch: {relative}") + if _sha256(resolved) != expected_hash: + raise ValueError(f"{gate}: evidence hash mismatch: {relative}") + + def _validate_common( report: dict[str, object], *, gate: str, source_commit: str, + artifact_root: Path, ) -> dict[str, object]: if report.get("schema") != "openrath.ga-gate-report/1": raise ValueError(f"{gate}: unsupported report schema") @@ -47,11 +114,21 @@ def _validate_common( raise ValueError(f"{gate}: generated_at must be ISO 8601") from error if generated.tzinfo is None: raise ValueError(f"{gate}: generated_at must include a timezone") - if not isinstance(report.get("environment"), dict): + environment = report.get("environment") + if not isinstance(environment, dict): raise ValueError(f"{gate}: environment profile is required") - evidence = report.get("evidence") - if not isinstance(evidence, list) or not evidence: - raise ValueError(f"{gate}: at least one evidence reference is required") + if ( + not isinstance(environment.get("profile"), str) + or not environment["profile"] + ): + raise ValueError(f"{gate}: environment profile name is required") + if environment.get("target_like") is not True: + raise ValueError(f"{gate}: environment target_like must be true") + _validate_evidence( + report.get("evidence"), + gate=gate, + artifact_root=artifact_root, + ) if not isinstance(report.get("open_risks"), list): raise ValueError(f"{gate}: open_risks must be a list") details = report.get("details") @@ -115,10 +192,12 @@ def verify_directory( directory: Path, *, source_commit: str, + artifact_root: Path | None = None, ) -> dict[str, Path]: """Verify all Gate C report files and return their paths by gate name.""" if re.fullmatch(r"[0-9a-f]{40}", source_commit) is None: raise ValueError("source_commit must be 40 lowercase hexadecimal characters") + root = artifact_root if artifact_root is not None else directory validated: dict[str, Path] = {} for gate, filename in GATE_REPORT_FILES.items(): path = directory / filename @@ -134,6 +213,7 @@ def verify_directory( report, gate=gate, source_commit=source_commit, + artifact_root=root, ) _validate_gate(gate, details) validated[gate] = path @@ -144,10 +224,12 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("directory", type=Path) parser.add_argument("--source-commit", required=True) + parser.add_argument("--artifact-root", type=Path) args = parser.parse_args() reports = verify_directory( args.directory, source_commit=args.source_commit, + artifact_root=args.artifact_root, ) print(f"verified {len(reports)} GA gate reports for {args.source_commit}") diff --git a/tests/deployment/test_ga_gate_reports.py b/tests/deployment/test_ga_gate_reports.py index 120d194..e644265 100644 --- a/tests/deployment/test_ga_gate_reports.py +++ b/tests/deployment/test_ga_gate_reports.py @@ -1,11 +1,13 @@ from __future__ import annotations +import hashlib import json from pathlib import Path import pytest +from jsonschema import Draft202012Validator -from scripts.release import verify_gate_reports +from scripts.release import record_gate, verify_gate_reports SOURCE_COMMIT = "b" * 40 @@ -16,8 +18,8 @@ def _reports() -> dict[str, dict[str, object]]: "source_commit": SOURCE_COMMIT, "result": "passed", "generated_at": "2026-07-30T12:00:00+00:00", - "environment": {"profile": "target-like"}, - "evidence": ["artifact://report"], + "environment": {"profile": "staging-us-east", "target_like": True}, + "evidence": [], "open_risks": [], } return { @@ -75,7 +77,18 @@ def _reports() -> dict[str, dict[str, object]]: def _write_reports(directory: Path, reports: dict[str, dict[str, object]]) -> None: + evidence_directory = directory / "evidence" + evidence_directory.mkdir() for gate, report in reports.items(): + evidence = evidence_directory / f"{gate}.log" + evidence.write_text(f"{gate} passed\n", encoding="utf-8") + report["evidence"] = [ + { + "path": evidence.relative_to(directory).as_posix(), + "sha256": hashlib.sha256(evidence.read_bytes()).hexdigest(), + "size": evidence.stat().st_size, + } + ] filename = verify_gate_reports.GATE_REPORT_FILES[gate] (directory / filename).write_text(json.dumps(report), encoding="utf-8") @@ -89,6 +102,19 @@ def test_complete_ga_gate_report_set_passes(tmp_path: Path) -> None: assert set(validated) == set(verify_gate_reports.GATE_REPORT_FILES) +def test_ga_gate_reports_match_the_committed_json_schema(tmp_path: Path) -> None: + reports = _reports() + _write_reports(tmp_path, reports) + schema = json.loads( + Path("release/evidence/schema/ga-gate-report.schema.json").read_text( + encoding="utf-8" + ) + ) + validator = Draft202012Validator(schema) + for report in reports.values(): + validator.validate(report) + + def test_soak_shorter_than_eight_hours_is_rejected(tmp_path: Path) -> None: reports = _reports() soak_details = reports["soak"]["details"] @@ -113,3 +139,99 @@ def test_report_from_a_different_source_commit_is_rejected(tmp_path: Path) -> No tmp_path, source_commit=SOURCE_COMMIT, ) + + +def test_rehearsal_environment_is_rejected(tmp_path: Path) -> None: + reports = _reports() + reports["tests"]["environment"] = { + "profile": "docker-desktop", + "target_like": False, + } + _write_reports(tmp_path, reports) + + with pytest.raises(ValueError, match="target_like"): + verify_gate_reports.verify_directory( + tmp_path, + source_commit=SOURCE_COMMIT, + ) + + +@pytest.mark.parametrize( + ("mutation", "message"), + [ + ({"path": "../outside.log"}, "relative"), + ({"sha256": "0" * 64}, "hash"), + ({"size": 999}, "size"), + ], +) +def test_evidence_files_are_contained_and_hash_bound( + tmp_path: Path, + mutation: dict[str, object], + message: str, +) -> None: + reports = _reports() + _write_reports(tmp_path, reports) + report_path = tmp_path / "tests.json" + report = json.loads(report_path.read_text(encoding="utf-8")) + report["evidence"][0].update(mutation) + report_path.write_text(json.dumps(report), encoding="utf-8") + + with pytest.raises(ValueError, match=message): + verify_gate_reports.verify_directory( + tmp_path, + source_commit=SOURCE_COMMIT, + ) + + +def test_symlink_evidence_is_rejected(tmp_path: Path) -> None: + reports = _reports() + _write_reports(tmp_path, reports) + target = tmp_path / "evidence/tests.log" + link = tmp_path / "evidence/tests-link.log" + try: + link.symlink_to(target) + except OSError: + pytest.skip("symlink creation is unavailable") + report_path = tmp_path / "tests.json" + report = json.loads(report_path.read_text(encoding="utf-8")) + report["evidence"][0]["path"] = "evidence/tests-link.log" + report_path.write_text(json.dumps(report), encoding="utf-8") + + with pytest.raises(ValueError, match="symlink"): + verify_gate_reports.verify_directory( + tmp_path, + source_commit=SOURCE_COMMIT, + ) + + +def test_report_recorder_hashes_evidence_without_storing_absolute_paths( + tmp_path: Path, +) -> None: + evidence_root = tmp_path / "bundle" + evidence_root.mkdir() + log = evidence_root / "logs/live.txt" + log.parent.mkdir() + log.write_text("provider lifecycle passed\n", encoding="utf-8") + + report = record_gate.build_report( + gate="live_adapters", + source_commit=SOURCE_COMMIT, + environment={"profile": "staging-us-east", "target_like": True}, + details={ + "provider": "passed", + "opensandbox": "passed", + "openviking": "passed", + }, + evidence_root=evidence_root, + evidence_files=[log], + open_risks=[], + generated_at="2026-07-30T12:00:00+00:00", + ) + + assert report["evidence"] == [ + { + "path": "logs/live.txt", + "sha256": hashlib.sha256(log.read_bytes()).hexdigest(), + "size": log.stat().st_size, + } + ] diff --git a/tests/deployment/test_gate_bundle_security.py b/tests/deployment/test_gate_bundle_security.py new file mode 100644 index 0000000..a10daf0 --- /dev/null +++ b/tests/deployment/test_gate_bundle_security.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from scripts.release.verify_gate_bundle import verify_bundle + + +def test_gate_bundle_allows_only_small_utf8_evidence_files(tmp_path: Path) -> None: + report = tmp_path / "tests.json" + report.write_text('{"result":"passed"}', encoding="utf-8") + evidence = tmp_path / "evidence" + evidence.mkdir() + log = evidence / "tests.log" + log.write_text("1083 tests passed\n", encoding="utf-8") + + summary = verify_bundle(tmp_path) + + assert summary == { + "files": 2, + "bytes": report.stat().st_size + log.stat().st_size, + } + + +@pytest.mark.parametrize( + "content", + [ + "Authorization: Bearer secret-value-that-must-not-leak", + "pypi-abcdefghijklmnopqrstuvwxyz0123456789", + "github_pat_abcdefghijklmnopqrstuvwxyz0123456789", + "-----BEGIN PRIVATE KEY-----", + '"api_key": "secret-value-that-must-not-leak"', + ], +) +def test_gate_bundle_rejects_high_confidence_secret_patterns( + tmp_path: Path, + content: str, +) -> None: + (tmp_path / "evidence.log").write_text(content, encoding="utf-8") + + with pytest.raises(ValueError, match="secret"): + verify_bundle(tmp_path) + + +def test_gate_bundle_rejects_unsupported_binary_and_sensitive_names( + tmp_path: Path, +) -> None: + (tmp_path / "capture.zip").write_bytes(b"archive") + with pytest.raises(ValueError, match="extension"): + verify_bundle(tmp_path) + + (tmp_path / "capture.zip").unlink() + (tmp_path / ".env").write_text("SAFE=value", encoding="utf-8") + with pytest.raises(ValueError, match="sensitive"): + verify_bundle(tmp_path) + + +def test_gate_bundle_rejects_symbolic_links(tmp_path: Path) -> None: + target = tmp_path / "target.log" + target.write_text("safe", encoding="utf-8") + link = tmp_path / "link.log" + try: + link.symlink_to(target) + except OSError: + pytest.skip("symlink creation is unavailable") + + with pytest.raises(ValueError, match="symbolic"): + verify_bundle(tmp_path) diff --git a/tests/deployment/test_release_load.py b/tests/deployment/test_release_load.py new file mode 100644 index 0000000..26e879f --- /dev/null +++ b/tests/deployment/test_release_load.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import json +import threading +from pathlib import Path +from typing import Any + +import httpx +import pytest + +from scripts.release import build_performance_report, load_v2 + +SOURCE_COMMIT = "d" * 40 + + +def test_load_sample_completes_lifecycles_without_recording_token() -> None: + lock = threading.Lock() + sequence = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal sequence + assert request.headers["authorization"] == "Bearer super-secret" + with lock: + sequence += 1 + current = sequence + if request.method == "POST" and request.url.path == "/v1/sessions": + return httpx.Response(201, json={"id": f"session-{current}"}) + if request.method == "POST" and request.url.path == "/v1/runs": + return httpx.Response(201, json={"id": f"run-{current}"}) + if request.method == "GET" and request.url.path.startswith("/v1/runs/"): + return httpx.Response(200, json={"status": "succeeded"}) + raise AssertionError(f"unexpected request: {request.method} {request.url.path}") + + report = load_v2.run_sample( + load_v2.LoadConfig( + base_url="https://target.example", + token="super-secret", + source_commit=SOURCE_COMMIT, + profile="split", + worker_replicas=4, + concurrency=2, + duration_seconds=60, + max_runs=4, + poll_interval_seconds=0, + request_timeout_seconds=2, + run_timeout_seconds=2, + target_like=True, + ), + transport=httpx.MockTransport(handler), + ) + + assert report["completed_runs"] == 4 + assert report["errors"] == 0 + assert report["worker_replicas"] == 4 + assert report["throughput_runs_per_second"] > 0 + assert "super-secret" not in json.dumps(report) + + +def test_target_like_load_requires_https() -> None: + with pytest.raises(ValueError, match="HTTPS"): + load_v2.run_sample( + load_v2.LoadConfig( + base_url="http://target.example", + token="super-secret", + source_commit=SOURCE_COMMIT, + profile="split", + worker_replicas=1, + concurrency=1, + duration_seconds=1, + max_runs=1, + poll_interval_seconds=0, + request_timeout_seconds=1, + run_timeout_seconds=1, + target_like=True, + ) + ) + + +def _sample( + *, + profile: str, + replicas: int, + throughput: float, + errors: int = 0, +) -> dict[str, Any]: + return { + "schema": "openrath.v2.load-sample/1", + "source_commit": SOURCE_COMMIT, + "profile": profile, + "target_like": True, + "worker_replicas": replicas, + "duration_seconds": 300.0, + "attempted_runs": 100, + "completed_runs": 100 - errors, + "errors": errors, + "throughput_runs_per_second": throughput, + "latency_seconds": {"p50": 0.1, "p95": 0.2, "p99": 0.3}, + "generated_at": "2026-07-30T12:00:00Z", + "target_origin": "https://target.example", + } + + +def _write_samples(root: Path, samples: list[dict[str, Any]]) -> list[Path]: + paths: list[Path] = [] + for index, sample in enumerate(samples): + path = root / f"sample-{index}.json" + path.write_text(json.dumps(sample), encoding="utf-8") + paths.append(path) + return paths + + +def test_performance_report_requires_single_and_split_scaling_samples( + tmp_path: Path, +) -> None: + paths = _write_samples( + tmp_path, + [ + _sample(profile="single_host", replicas=1, throughput=8), + _sample(profile="split", replicas=1, throughput=10), + _sample(profile="split", replicas=2, throughput=18), + _sample(profile="split", replicas=4, throughput=30), + ], + ) + + report = build_performance_report.build_report( + sample_paths=paths, + evidence_root=tmp_path, + environment_profile="staging-us-east", + generated_at="2026-07-30T13:00:00Z", + ) + + details = report["details"] + assert details["single_host"] == "passed" + assert details["split_profile"] == "passed" + assert details["worker_scaling_efficiency"] == pytest.approx(0.75) + assert report["source_commit"] == SOURCE_COMMIT + assert len(report["evidence"]) == 4 + + +def test_performance_report_rejects_errors_and_insufficient_scaling( + tmp_path: Path, +) -> None: + samples = [ + _sample(profile="single_host", replicas=1, throughput=8), + _sample(profile="split", replicas=1, throughput=10), + _sample(profile="split", replicas=2, throughput=18), + _sample(profile="split", replicas=4, throughput=20), + ] + paths = _write_samples(tmp_path, samples) + with pytest.raises(ValueError, match="scaling efficiency"): + build_performance_report.build_report( + sample_paths=paths, + evidence_root=tmp_path, + environment_profile="staging-us-east", + ) + + samples[-1] = _sample( + profile="split", + replicas=4, + throughput=32, + errors=1, + ) + paths[-1].write_text(json.dumps(samples[-1]), encoding="utf-8") + with pytest.raises(ValueError, match="zero errors"): + build_performance_report.build_report( + sample_paths=paths, + evidence_root=tmp_path, + environment_profile="staging-us-east", + ) + + samples[-1] = _sample(profile="split", replicas=4, throughput=32) + samples[-1]["completed_runs"] = 0 + paths[-1].write_text(json.dumps(samples[-1]), encoding="utf-8") + with pytest.raises(ValueError, match="completed run"): + build_performance_report.build_report( + sample_paths=paths, + evidence_root=tmp_path, + environment_profile="staging-us-east", + ) diff --git a/tests/deployment/test_release_operations_evidence.py b/tests/deployment/test_release_operations_evidence.py new file mode 100644 index 0000000..dcf7ddf --- /dev/null +++ b/tests/deployment/test_release_operations_evidence.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +from scripts.release import build_drill_report, build_soak_report + +SOURCE_COMMIT = "e" * 40 + + +def _write(path: Path, value: object) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value), encoding="utf-8") + return path + + +def _soak_sample(*, duration: float = 28800, errors: int = 0) -> dict[str, Any]: + return { + "schema": "openrath.v2.load-sample/1", + "source_commit": SOURCE_COMMIT, + "profile": "split", + "target_like": True, + "worker_replicas": 4, + "duration_seconds": duration, + "attempted_runs": 1000, + "completed_runs": 1000 - errors, + "errors": errors, + "throughput_runs_per_second": 3.0, + "latency_seconds": {"p50": 0.2, "p95": 0.5, "p99": 0.8}, + "generated_at": "2026-07-30T12:00:00Z", + "target_origin": "https://target.example", + } + + +def _snapshot(phase: str, memory: int) -> dict[str, Any]: + return { + "schema": "openrath.v2.resource-snapshot/1", + "source_commit": SOURCE_COMMIT, + "phase": phase, + "captured_at": "2026-07-30T12:00:00Z", + "components": { + "api": {"memory_bytes": memory, "restarts": 0}, + "worker": {"memory_bytes": memory, "restarts": 0}, + }, + } + + +def _assessment(*, unexplained: bool = False) -> dict[str, Any]: + return { + "schema": "openrath.v2.resource-assessment/1", + "source_commit": SOURCE_COMMIT, + "assessor": "operations-owner", + "unexplained_resource_growth": unexplained, + "rationale": "Resource use stabilized after warm-up.", + } + + +def test_soak_report_binds_eight_hour_run_and_resource_assessment( + tmp_path: Path, +) -> None: + sample = _write(tmp_path / "raw/soak.json", _soak_sample()) + before = _write(tmp_path / "raw/before.json", _snapshot("before", 100)) + after = _write(tmp_path / "raw/after.json", _snapshot("after", 110)) + assessment = _write(tmp_path / "raw/assessment.json", _assessment()) + + report = build_soak_report.build_report( + sample_path=sample, + before_snapshot_path=before, + after_snapshot_path=after, + assessment_path=assessment, + evidence_root=tmp_path, + environment_profile="staging-us-east", + generated_at="2026-07-30T21:00:00Z", + ) + + assert report["details"]["duration_seconds"] == 28800 + assert report["details"]["errors"] == 0 + assert report["details"]["unexplained_resource_growth"] is False + assert len(report["evidence"]) == 4 + + +@pytest.mark.parametrize( + ("duration", "errors", "unexplained", "message"), + [ + (28799, 0, False, "28800"), + (28800, 1, False, "zero errors"), + (28800, 0, True, "unexplained"), + ], +) +def test_soak_report_rejects_incomplete_acceptance( + tmp_path: Path, + duration: float, + errors: int, + unexplained: bool, + message: str, +) -> None: + sample = _write( + tmp_path / "raw/soak.json", _soak_sample(duration=duration, errors=errors) + ) + before = _write(tmp_path / "raw/before.json", _snapshot("before", 100)) + after = _write(tmp_path / "raw/after.json", _snapshot("after", 110)) + assessment = _write( + tmp_path / "raw/assessment.json", + _assessment(unexplained=unexplained), + ) + with pytest.raises(ValueError, match=message): + build_soak_report.build_report( + sample_path=sample, + before_snapshot_path=before, + after_snapshot_path=after, + assessment_path=assessment, + evidence_root=tmp_path, + environment_profile="staging-us-east", + ) + + +def test_soak_report_requires_comparable_resource_components(tmp_path: Path) -> None: + sample = _write(tmp_path / "raw/soak.json", _soak_sample()) + before = _write(tmp_path / "raw/before.json", _snapshot("before", 100)) + after_value = _snapshot("after", 110) + del after_value["components"]["worker"] + after = _write(tmp_path / "raw/after.json", after_value) + assessment = _write(tmp_path / "raw/assessment.json", _assessment()) + + with pytest.raises(ValueError, match="same components"): + build_soak_report.build_report( + sample_path=sample, + before_snapshot_path=before, + after_snapshot_path=after, + assessment_path=assessment, + evidence_root=tmp_path, + environment_profile="staging-us-east", + ) + + +def _drill_results() -> dict[str, Any]: + required = ( + "postgresql_failure", + "redis_failure", + "s3_failure", + "api_failure", + "worker_failure", + "backup_restore", + "rollout_rollback", + ) + return { + "schema": "openrath.v2.drill-results/1", + "source_commit": SOURCE_COMMIT, + "environment_profile": "staging-us-east", + "drills": { + name: { + "status": "passed", + "operator": "operations-owner", + "started_at": "2026-07-30T12:00:00Z", + "completed_at": "2026-07-30T12:05:00Z", + "recovery_seconds": 300, + "data_loss_records": 0, + "observed": f"{name} recovered", + } + for name in required + }, + } + + +def test_drill_report_requires_full_fault_restore_and_rollback_matrix( + tmp_path: Path, +) -> None: + results = _write(tmp_path / "raw/drills.json", _drill_results()) + log = _write(tmp_path / "raw/operator-log.json", {"events": ["passed"]}) + + report = build_drill_report.build_report( + results_path=results, + evidence_paths=[log], + evidence_root=tmp_path, + generated_at="2026-07-30T14:00:00Z", + ) + + assert report["details"] == { + "fault_matrix": "passed", + "backup_restore": "passed", + "rollout_rollback": "passed", + } + + +def test_drill_report_rejects_missing_drill_data_loss_and_rto_breach( + tmp_path: Path, +) -> None: + results = _drill_results() + del results["drills"]["redis_failure"] + path = _write(tmp_path / "raw/drills.json", results) + with pytest.raises(ValueError, match="missing drills"): + build_drill_report.build_report( + results_path=path, + evidence_paths=[], + evidence_root=tmp_path, + ) + + results = _drill_results() + results["drills"]["worker_failure"]["completed_at"] = "2026-07-30T11:00:00Z" + path.write_text(json.dumps(results), encoding="utf-8") + with pytest.raises(ValueError, match="completion"): + build_drill_report.build_report( + results_path=path, + evidence_paths=[], + evidence_root=tmp_path, + ) + + results = _drill_results() + results["drills"]["backup_restore"]["data_loss_records"] = 1 + path.write_text(json.dumps(results), encoding="utf-8") + with pytest.raises(ValueError, match="data loss"): + build_drill_report.build_report( + results_path=path, + evidence_paths=[], + evidence_root=tmp_path, + ) + + results["drills"]["backup_restore"]["data_loss_records"] = 0 + results["drills"]["backup_restore"]["recovery_seconds"] = 3601 + path.write_text(json.dumps(results), encoding="utf-8") + with pytest.raises(ValueError, match="3600"): + build_drill_report.build_report( + results_path=path, + evidence_paths=[], + evidence_root=tmp_path, + ) diff --git a/tests/deployment/test_release_version.py b/tests/deployment/test_release_version.py index 2b10f8d..98907ed 100644 --- a/tests/deployment/test_release_version.py +++ b/tests/deployment/test_release_version.py @@ -79,6 +79,32 @@ def test_ga_workflows_separate_preparation_manual_pypi_and_finalization() -> Non assert Path("release/manual-pypi-v2.0.0.md").is_file() +def test_gate_c_collector_is_protected_and_release_checks_its_identity() -> None: + collector = Path(".github/workflows/collect-v2-ga-evidence.yml").read_text( + encoding="utf-8" + ) + prepare = Path(".github/workflows/release-v2-ga.yml").read_text(encoding="utf-8") + + assert "name: Collect v2.0.0 Gate C evidence" in collector + assert "workflow_dispatch:" in collector + assert "runs-on: [self-hosted, linux, openrath-ga]" in collector + assert "name: ga-evidence" in collector + assert "OPENRATH_GA_EVIDENCE_ROOT" in collector + assert "openrath-v2.0.0-ga-input" in collector + assert "scripts/release/verify_gate_bundle.py" in collector + assert "scripts/release/verify_gate_reports.py" in collector + assert '--source-commit "$GITHUB_SHA"' in collector + assert 'find "$source_real" -type l' in collector + assert "actions/upload-artifact@" in collector + + assert "actions/runs/$EVIDENCE_RUN_ID" in prepare + assert ".github/workflows/collect-v2-ga-evidence.yml" in prepare + assert "Collect v2.0.0 Gate C evidence" in prepare + assert 'jq -r .event <<<"$run_json"' in prepare + assert 'jq -r .head_branch <<<"$run_json"' in prepare + assert '--artifact-root "$gate_dir"' in prepare + + def test_ga_release_documents_are_present_and_not_marked_as_drafts() -> None: notes = Path("release/notes/v2.0.0.md").read_text(encoding="utf-8") checklist = Path("release/checklists/v2.0.0-ga.md").read_text(encoding="utf-8")