palonexus 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {palonexus-0.3.2 → palonexus-0.3.3}/PKG-INFO +1 -1
  2. palonexus-0.3.3/src/palonexus/_build.py +2 -0
  3. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/_sdist_provenance.py +1 -1
  4. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/_version.py +2 -2
  5. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/cli/commands.py +77 -0
  6. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/client.py +109 -0
  7. palonexus-0.3.3/src/palonexus/developer/evidence.py +422 -0
  8. palonexus-0.3.2/src/palonexus/_build.py +0 -2
  9. {palonexus-0.3.2 → palonexus-0.3.3}/.gitignore +0 -0
  10. {palonexus-0.3.2 → palonexus-0.3.3}/LICENSE +0 -0
  11. {palonexus-0.3.2 → palonexus-0.3.3}/hatch_build.py +0 -0
  12. {palonexus-0.3.2 → palonexus-0.3.3}/pyproject.toml +0 -0
  13. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/__init__.py +0 -0
  14. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/_canonicalize.py +0 -0
  15. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/_generated/__init__.py +0 -0
  16. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/_generated/protocol.py +0 -0
  17. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/approvals.py +0 -0
  18. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/async_client.py +0 -0
  19. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/cli/__init__.py +0 -0
  20. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/cli/main.py +0 -0
  21. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/client.py +0 -0
  22. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/context.py +0 -0
  23. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/credentials.py +0 -0
  24. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/__init__.py +0 -0
  25. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/context.py +0 -0
  26. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/contracts.py +0 -0
  27. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/credentials.py +0 -0
  28. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/guard.py +0 -0
  29. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/runner.py +0 -0
  30. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/scaffold.py +0 -0
  31. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/subagents.py +0 -0
  32. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/__init__.py +0 -0
  33. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/plain_python/__init__.py +0 -0
  34. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/plain_python/agent.py +0 -0
  35. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/plain_python/fixtures/release-change.json +0 -0
  36. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/plain_python/palonexus-agent.yaml +0 -0
  37. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/plain_python/palonexus-registration.yaml +0 -0
  38. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/templates/plain_python/pyproject.toml.tmpl +0 -0
  39. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/developer/version.py +0 -0
  40. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/errors.py +0 -0
  41. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/identity/__init__.py +0 -0
  42. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/identity/did.py +0 -0
  43. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/identity/oidc.py +0 -0
  44. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/identity/vc.py +0 -0
  45. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/integrations/__init__.py +0 -0
  46. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/integrations/deepagents.py +0 -0
  47. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/integrations/langchain.py +0 -0
  48. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/integrations/langgraph.py +0 -0
  49. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/keystore.py +0 -0
  50. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/models.py +0 -0
  51. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/protocol.py +0 -0
  52. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/py.typed +0 -0
  53. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/redaction.py +0 -0
  54. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/retry.py +0 -0
  55. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/testing/__init__.py +0 -0
  56. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/testing/fake_transport.py +0 -0
  57. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/testing/mock_server.py +0 -0
  58. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/transports/__init__.py +0 -0
  59. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/transports/base.py +0 -0
  60. {palonexus-0.3.2 → palonexus-0.3.3}/src/palonexus/transports/http.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: palonexus
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Python SDK for the PaloNexus agent control plane
5
5
  Project-URL: Homepage, https://github.com/rogerchucker/palonexus-sdk
6
6
  Project-URL: Repository, https://github.com/rogerchucker/palonexus-sdk
@@ -0,0 +1,2 @@
1
+ # generated by the PaloNexus build hook
2
+ SOURCE_REVISION = "0a3e3eacb5853a48f269e00991b4871d16ff3d9b"
@@ -1,3 +1,3 @@
1
1
  # generated by the PaloNexus build hook
2
2
  SDIST_MARKER = "palonexus-sdk-sdist-v1"
3
- SOURCE_REVISION = "58e2607d1c2ed32be5a4fc7c9a334be66bd8e347"
3
+ SOURCE_REVISION = "0a3e3eacb5853a48f269e00991b4871d16ff3d9b"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.2'
22
- __version_tuple__ = version_tuple = (0, 3, 2)
21
+ __version__ = version = '0.3.3'
22
+ __version_tuple__ = version_tuple = (0, 3, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -36,6 +36,11 @@ from palonexus.developer.credentials import (
36
36
  CredentialStore,
37
37
  CredentialStoreUnavailable,
38
38
  )
39
+ from palonexus.developer.evidence import (
40
+ build_preexchange_evidence,
41
+ build_runtime_attestation,
42
+ ensure_runtime_evidence_credential,
43
+ )
39
44
  from palonexus.developer.runner import Runner
40
45
  from palonexus.developer.scaffold import (
41
46
  ScaffoldError,
@@ -1060,6 +1065,23 @@ def _strict_input(path: Path) -> tuple[dict[str, Any], str]:
1060
1065
  return value, hashlib.sha256(canonical_json(value)).hexdigest()
1061
1066
 
1062
1067
 
1068
+ def _require_runtime_evidence_expiry(value: object) -> str:
1069
+ if not isinstance(value, str) or not value:
1070
+ raise CommandError("runtime lease is missing its evidence expiry")
1071
+ try:
1072
+ expires = datetime.fromisoformat(value.replace("Z", "+00:00"))
1073
+ except ValueError as error:
1074
+ raise CommandError("runtime lease has an invalid evidence expiry") from error
1075
+ if expires.tzinfo is None or expires <= datetime.now(UTC):
1076
+ raise CommandError("runtime lease has an invalid evidence expiry")
1077
+ return (
1078
+ expires.astimezone(UTC)
1079
+ .replace(microsecond=0)
1080
+ .isoformat()
1081
+ .replace("+00:00", "Z")
1082
+ )
1083
+
1084
+
1063
1085
  def _strict_json_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
1064
1086
  value: dict[str, Any] = {}
1065
1087
  for key, item in pairs:
@@ -1077,6 +1099,9 @@ def run_agent(args: Namespace) -> int:
1077
1099
  try:
1078
1100
  if guard is None:
1079
1101
  guard = generate_agent_credential()
1102
+ evidence_guard = ensure_runtime_evidence_credential(guard)
1103
+ if evidence_guard != guard:
1104
+ guard = evidence_guard
1080
1105
  store.save("guard:" + str(descriptor["name"]), guard)
1081
1106
  artifact = "sha256:" + descriptor["descriptor_digest"]
1082
1107
  # Enrollment proofs are one-time. Each process start creates a fresh
@@ -1094,6 +1119,29 @@ def run_agent(args: Namespace) -> int:
1094
1119
  guard_version="pnxs/0.1.0",
1095
1120
  )
1096
1121
  runtime = client.redeem_runtime(session, agent, enrollment)
1122
+ registration_profile = _project_registration_profile()
1123
+ created_at = (
1124
+ datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
1125
+ )
1126
+ expires_at = _require_runtime_evidence_expiry(runtime.get("expires_at"))
1127
+ attestation = build_runtime_attestation(
1128
+ tenant_id=str(session["tenant_id"]),
1129
+ agent_id=str(agent["agent_id"]),
1130
+ runtime_id=str(runtime["runtime_id"]),
1131
+ descriptor_digest=str(descriptor["descriptor_digest"]),
1132
+ runtime_profile=registration_profile["runtime_profile"],
1133
+ guard=guard,
1134
+ created_at=created_at,
1135
+ expires_at=expires_at,
1136
+ )
1137
+ client.submit_runtime_attestation(
1138
+ session,
1139
+ agent,
1140
+ guard,
1141
+ runtime,
1142
+ attestation,
1143
+ idempotency_key=str(uuid.uuid4()),
1144
+ )
1097
1145
  run_key = str(uuid.uuid4())
1098
1146
  run = client.create_developer_run(
1099
1147
  session,
@@ -1104,6 +1152,7 @@ def run_agent(args: Namespace) -> int:
1104
1152
  idempotency_key=run_key,
1105
1153
  )
1106
1154
  persisted: dict[str, Any] = {}
1155
+ preexchange_batch: dict[str, Any] = {}
1107
1156
  persisted_spawn: dict[str, Any] = {}
1108
1157
  spawn_runtime = (
1109
1158
  _subagent_runtime(
@@ -1184,6 +1233,34 @@ def run_agent(args: Namespace) -> int:
1184
1233
  descriptor, envelope["action"], envelope["resource"]
1185
1234
  )
1186
1235
  try:
1236
+ if not preexchange_batch:
1237
+ occurred_at = (
1238
+ datetime.now(UTC)
1239
+ .replace(microsecond=0)
1240
+ .isoformat()
1241
+ .replace("+00:00", "Z")
1242
+ )
1243
+ preexchange_batch.update(
1244
+ build_preexchange_evidence(
1245
+ attestation=attestation,
1246
+ guard=guard,
1247
+ run_id=str(run["runId"]),
1248
+ root_action_id=str(run["rootId"]),
1249
+ action=str(envelope["action"]),
1250
+ resource=str(envelope["resource"]),
1251
+ payload=envelope["payload"],
1252
+ descriptor_digest=str(descriptor["descriptor_digest"]),
1253
+ occurred_at=occurred_at,
1254
+ )
1255
+ )
1256
+ client.submit_runtime_evidence(
1257
+ session,
1258
+ agent,
1259
+ guard,
1260
+ runtime,
1261
+ preexchange_batch,
1262
+ idempotency_key=str(preexchange_batch["idempotencyKey"]),
1263
+ )
1187
1264
  action = client.create_developer_action(
1188
1265
  session,
1189
1266
  agent,
@@ -1842,6 +1842,115 @@ class DeveloperClient:
1842
1842
  },
1843
1843
  )
1844
1844
 
1845
+ def submit_runtime_attestation(
1846
+ self,
1847
+ session: dict[str, Any],
1848
+ agent: dict[str, Any],
1849
+ guard: dict[str, Any],
1850
+ runtime: dict[str, Any],
1851
+ attestation: dict[str, Any],
1852
+ *,
1853
+ idempotency_key: str,
1854
+ ) -> dict[str, Any]:
1855
+ path = "/v1/developer/runtime-attestations"
1856
+ body = canonical_json(attestation)
1857
+ proof = self.mounted_proof(
1858
+ _decode_private_key(guard.get("private_key")),
1859
+ mode="runtime_proof",
1860
+ tenant_id=_require_string(session.get("tenant_id"), "tenant ID"),
1861
+ agent_id=_require_string(agent.get("agent_id"), "agent ID"),
1862
+ agent_generation=_require_positive_int(
1863
+ int(agent.get("agent_generation", "0")), "agent generation"
1864
+ ),
1865
+ method="POST",
1866
+ path=path,
1867
+ body=body,
1868
+ runtime_id=_require_string(runtime.get("runtime_id"), "runtime ID"),
1869
+ )
1870
+ response = self._request(
1871
+ "POST",
1872
+ path,
1873
+ body=body,
1874
+ headers={
1875
+ "X-Palonexus-Developer-Proof": proof,
1876
+ "Idempotency-Key": idempotency_key,
1877
+ },
1878
+ expected={200, 201},
1879
+ allowed_fields={
1880
+ "attestationId",
1881
+ "runtimeSessionId",
1882
+ "manifestHash",
1883
+ "verificationState",
1884
+ "duplicate",
1885
+ },
1886
+ )
1887
+ if (
1888
+ response.get("attestationId") != attestation.get("attestationId")
1889
+ or response.get("runtimeSessionId") != runtime.get("runtime_id")
1890
+ or response.get("manifestHash") != attestation.get("manifestHash")
1891
+ or response.get("verificationState") != "verified"
1892
+ or type(response.get("duplicate")) is not bool
1893
+ ):
1894
+ raise ProtocolError("runtime attestation acknowledgement is not bound")
1895
+ return response
1896
+
1897
+ def submit_runtime_evidence(
1898
+ self,
1899
+ session: dict[str, Any],
1900
+ agent: dict[str, Any],
1901
+ guard: dict[str, Any],
1902
+ runtime: dict[str, Any],
1903
+ batch: dict[str, Any],
1904
+ *,
1905
+ idempotency_key: str,
1906
+ ) -> dict[str, Any]:
1907
+ path = "/v1/developer/runtime-evidence"
1908
+ body = canonical_json(batch)
1909
+ proof = self.mounted_proof(
1910
+ _decode_private_key(guard.get("private_key")),
1911
+ mode="runtime_proof",
1912
+ tenant_id=_require_string(session.get("tenant_id"), "tenant ID"),
1913
+ agent_id=_require_string(agent.get("agent_id"), "agent ID"),
1914
+ agent_generation=_require_positive_int(
1915
+ int(agent.get("agent_generation", "0")), "agent generation"
1916
+ ),
1917
+ method="POST",
1918
+ path=path,
1919
+ body=body,
1920
+ runtime_id=_require_string(runtime.get("runtime_id"), "runtime ID"),
1921
+ )
1922
+ response = self._request(
1923
+ "POST",
1924
+ path,
1925
+ body=body,
1926
+ headers={
1927
+ "X-Palonexus-Developer-Proof": proof,
1928
+ "Idempotency-Key": idempotency_key,
1929
+ },
1930
+ expected={200, 201},
1931
+ allowed_fields={
1932
+ "batchId",
1933
+ "receiptCount",
1934
+ "finalReceiptHash",
1935
+ "achievedLevel",
1936
+ "deliveryState",
1937
+ "duplicate",
1938
+ },
1939
+ )
1940
+ receipts = batch.get("receipts")
1941
+ if (
1942
+ not isinstance(receipts, list)
1943
+ or not receipts
1944
+ or response.get("batchId") != batch.get("batchId")
1945
+ or response.get("receiptCount") != len(receipts)
1946
+ or response.get("finalReceiptHash") != receipts[-1].get("receiptHash")
1947
+ or type(response.get("duplicate")) is not bool
1948
+ or not isinstance(response.get("achievedLevel"), str)
1949
+ or not isinstance(response.get("deliveryState"), str)
1950
+ ):
1951
+ raise ProtocolError("runtime evidence acknowledgement is not bound")
1952
+ return response
1953
+
1845
1954
  def create_developer_run(
1846
1955
  self,
1847
1956
  session: dict[str, Any],
@@ -0,0 +1,422 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Signed runtime assurance evidence for guarded developer runs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import hashlib
8
+ import json
9
+ import re
10
+ import secrets
11
+ import uuid
12
+ from typing import Any
13
+
14
+ import rfc8785
15
+ from cryptography.hazmat.primitives import serialization
16
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
17
+
18
+ VERSION = "palonexus.harness.assurance/v1"
19
+ _HEX_DIGEST = re.compile(r"^[0-9a-f]{64}$")
20
+ _UTC_SECOND = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")
21
+ _LAYERS = (
22
+ ("identity.session", "harness-identity-session", "harness_adapter"),
23
+ ("run.ownership", "harness-run-ownership", "harness_adapter"),
24
+ ("tool.discovery", "pnxs-tool-discovery", "harness_adapter"),
25
+ ("tool.invocation", "pnxs-tool-invocation", "harness_adapter"),
26
+ ("mcp.access", "runtime-guard-mcp-access", "runtime_guard"),
27
+ ("policy.decision", "control-plane-authz", "control_plane"),
28
+ ("authority.delegation", "agent-idp-exact-delegation", "agent_idp_sts"),
29
+ ("credential.exchange", "agent-idp-sts-exchange", "agent_idp_sts"),
30
+ ("target.enforcement", "registered-target-verifier", "registered_target"),
31
+ )
32
+
33
+
34
+ def _canonical(value: Any) -> bytes:
35
+ try:
36
+ return rfc8785.dumps(value)
37
+ except (TypeError, ValueError) as error:
38
+ raise ValueError("runtime evidence is not canonical JSON") from error
39
+
40
+
41
+ def _digest(value: Any) -> str:
42
+ return "sha256:" + hashlib.sha256(_canonical(value)).hexdigest()
43
+
44
+
45
+ def _b64url(value: bytes) -> str:
46
+ return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
47
+
48
+
49
+ def _decode(value: object, *, field: str) -> bytes:
50
+ if not isinstance(value, str) or not value:
51
+ raise ValueError(f"invalid {field}")
52
+ try:
53
+ decoded = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
54
+ except (ValueError, TypeError) as error:
55
+ raise ValueError(f"invalid {field}") from error
56
+ if len(decoded) != 32 or _b64url(decoded) != value:
57
+ raise ValueError(f"invalid {field}")
58
+ return decoded
59
+
60
+
61
+ def _public_x(value: object, *, field: str) -> str:
62
+ if not isinstance(value, str):
63
+ raise ValueError(f"invalid {field}")
64
+ try:
65
+ parsed = json.loads(value)
66
+ except (json.JSONDecodeError, TypeError) as error:
67
+ raise ValueError(f"invalid {field}") from error
68
+ if not isinstance(parsed, dict) or set(parsed) != {"crv", "kty", "x"}:
69
+ raise ValueError(f"invalid {field}")
70
+ if parsed["crv"] != "Ed25519" or parsed["kty"] != "OKP":
71
+ raise ValueError(f"invalid {field}")
72
+ x = parsed["x"]
73
+ if not isinstance(x, str):
74
+ raise ValueError(f"invalid {field}")
75
+ _decode(x, field=field)
76
+ return x
77
+
78
+
79
+ def _new_credential() -> dict[str, str]:
80
+ private = Ed25519PrivateKey.generate()
81
+ private_raw = private.private_bytes(
82
+ serialization.Encoding.Raw,
83
+ serialization.PrivateFormat.Raw,
84
+ serialization.NoEncryption(),
85
+ )
86
+ public_raw = private.public_key().public_bytes(
87
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw
88
+ )
89
+ public = {"crv": "Ed25519", "kty": "OKP", "x": _b64url(public_raw)}
90
+ return {
91
+ "private_key": _b64url(private_raw),
92
+ "public_key_jwk": _canonical(public).decode("utf-8"),
93
+ }
94
+
95
+
96
+ def ensure_runtime_evidence_credential(guard: dict[str, str]) -> dict[str, str]:
97
+ """Return a guard credential with a distinct harness-receipt signing key."""
98
+
99
+ value = dict(guard)
100
+ _decode(value.get("private_key"), field="guard private key")
101
+ _public_x(value.get("public_key_jwk"), field="guard public key")
102
+ private = value.get("harness_receipt_private_key")
103
+ public = value.get("harness_receipt_public_key_jwk")
104
+ if private is None and public is None:
105
+ receipt = _new_credential()
106
+ value["harness_receipt_private_key"] = receipt["private_key"]
107
+ value["harness_receipt_public_key_jwk"] = receipt["public_key_jwk"]
108
+ elif private is None or public is None:
109
+ raise ValueError("incomplete harness receipt credential")
110
+ _decode(value.get("harness_receipt_private_key"), field="receipt private key")
111
+ _public_x(value.get("harness_receipt_public_key_jwk"), field="receipt public key")
112
+ if value["public_key_jwk"] == value["harness_receipt_public_key_jwk"]:
113
+ raise ValueError("runtime guard and harness receipt keys must be distinct")
114
+ return value
115
+
116
+
117
+ def _key_id(prefix: str, x: str) -> str:
118
+ return prefix + hashlib.sha256(x.encode("ascii")).hexdigest()[:32]
119
+
120
+
121
+ def _jwk(x: str, key_id: str) -> dict[str, str]:
122
+ return {
123
+ "kty": "OKP",
124
+ "crv": "Ed25519",
125
+ "x": x,
126
+ "kid": key_id,
127
+ "use": "sig",
128
+ "alg": "EdDSA",
129
+ }
130
+
131
+
132
+ def _binding(
133
+ *, x: str, key_id: str, signer_class: str, purpose: str, token: bool
134
+ ) -> dict[str, Any]:
135
+ return {
136
+ "keyId": key_id,
137
+ "algorithm": "Ed25519",
138
+ "publicKeyJwk": _jwk(x, key_id),
139
+ "signerClass": signer_class,
140
+ "purpose": purpose,
141
+ "grantsAuthority": False,
142
+ "tokenRequestPermitted": token,
143
+ }
144
+
145
+
146
+ def _require_digest(value: object, field: str) -> str:
147
+ if not isinstance(value, str) or _HEX_DIGEST.fullmatch(value) is None:
148
+ raise ValueError(f"invalid {field}")
149
+ return value
150
+
151
+
152
+ def _require_time(value: str, field: str) -> str:
153
+ if _UTC_SECOND.fullmatch(value) is None:
154
+ raise ValueError(f"invalid {field}")
155
+ return value
156
+
157
+
158
+ def build_runtime_attestation(
159
+ *,
160
+ tenant_id: str,
161
+ agent_id: str,
162
+ runtime_id: str,
163
+ descriptor_digest: str,
164
+ runtime_profile: dict[str, Any],
165
+ guard: dict[str, str],
166
+ created_at: str,
167
+ expires_at: str,
168
+ evidence_id: str | None = None,
169
+ manifest_id: str | None = None,
170
+ freshness_nonce: str | None = None,
171
+ ) -> dict[str, Any]:
172
+ guard = ensure_runtime_evidence_credential(guard)
173
+ descriptor_digest = _require_digest(descriptor_digest, "descriptor digest")
174
+ profile_digest = _require_digest(
175
+ runtime_profile.get("digest"), "runtime profile digest"
176
+ )
177
+ profile_id = runtime_profile.get("id") or runtime_profile.get("kind")
178
+ if not all(
179
+ isinstance(item, str) and item
180
+ for item in (tenant_id, agent_id, runtime_id, profile_id)
181
+ ):
182
+ raise ValueError("incomplete runtime evidence identity")
183
+ if (
184
+ type(runtime_profile.get("version")) is not int
185
+ or runtime_profile["version"] < 1
186
+ ):
187
+ raise ValueError("invalid runtime profile version")
188
+ created_at = _require_time(created_at, "created time")
189
+ expires_at = _require_time(expires_at, "expiry time")
190
+ evidence_id = evidence_id or secrets.token_hex(32)
191
+ if _HEX_DIGEST.fullmatch(evidence_id) is None:
192
+ raise ValueError("invalid runtime evidence ID")
193
+ manifest_id = manifest_id or str(uuid.uuid4())
194
+ freshness_nonce = freshness_nonce or secrets.token_hex(16)
195
+ if not manifest_id or not freshness_nonce:
196
+ raise ValueError("invalid runtime evidence nonce")
197
+
198
+ guard_x = _public_x(guard["public_key_jwk"], field="guard public key")
199
+ receipt_x = _public_x(
200
+ guard["harness_receipt_public_key_jwk"], field="receipt public key"
201
+ )
202
+ guard_key_id = _key_id("pnxs-runtime-guard-", guard_x)
203
+ receipt_key_id = _key_id("pnxs-harness-receipt-", receipt_x)
204
+ required = [
205
+ {
206
+ "order": order,
207
+ "layerId": layer,
208
+ "adapterId": adapter,
209
+ "signerClass": signer,
210
+ "requiredWhen": "always",
211
+ }
212
+ for order, (layer, adapter, signer) in enumerate(_LAYERS, start=1)
213
+ ]
214
+ adapter_version = "1.0.0"
215
+ enforcement = [
216
+ {
217
+ "layerId": layer,
218
+ "adapterId": adapter,
219
+ "version": adapter_version,
220
+ "digest": _digest(
221
+ {
222
+ "purpose": "palonexus.runtime-enforcement-adapter.v1",
223
+ "descriptorDigest": descriptor_digest,
224
+ "runtimeProfileDigest": profile_digest,
225
+ "layerId": layer,
226
+ "adapterId": adapter,
227
+ "signerClass": signer,
228
+ }
229
+ ),
230
+ "signerClass": signer,
231
+ }
232
+ for layer, adapter, signer in _LAYERS
233
+ ]
234
+ framework = {
235
+ "layerId": "framework.lifecycle",
236
+ "adapterId": "pnxs-plain-python",
237
+ "version": adapter_version,
238
+ "digest": _digest(
239
+ {
240
+ "purpose": "palonexus.runtime-framework-adapter.v1",
241
+ "descriptorDigest": descriptor_digest,
242
+ "runtimeProfileDigest": profile_digest,
243
+ }
244
+ ),
245
+ "signerClass": "harness_adapter",
246
+ }
247
+ manifest = {
248
+ "version": VERSION,
249
+ "manifestId": manifest_id,
250
+ "tenantId": tenant_id,
251
+ "agentId": agent_id,
252
+ "profileId": profile_id,
253
+ "runtimeSessionId": runtime_id,
254
+ "harnessPackageDigest": "sha256:" + descriptor_digest,
255
+ # A plain-Python runtime has no container. The registered runtime-profile
256
+ # digest is the immutable runtime environment identity for this field.
257
+ "containerImageDigest": "sha256:" + profile_digest,
258
+ "frameworkAdapter": framework,
259
+ "enforcementAdapters": enforcement,
260
+ "requiredReceipts": required,
261
+ "requiredLayerManifestHash": _digest(required),
262
+ "harnessReceiptKey": _binding(
263
+ x=receipt_x,
264
+ key_id=receipt_key_id,
265
+ signer_class="harness_adapter",
266
+ purpose="evidence_receipt",
267
+ token=False,
268
+ ),
269
+ "guardWorkloadKey": _binding(
270
+ x=guard_x,
271
+ key_id=guard_key_id,
272
+ signer_class="runtime_guard",
273
+ purpose="workload_proof_and_evidence",
274
+ token=True,
275
+ ),
276
+ "proofConfirmationMethod": "ed25519-jwk-thumbprint",
277
+ "createdAt": created_at,
278
+ "expiresAt": expires_at,
279
+ "freshnessNonce": freshness_nonce,
280
+ "workloadIdentityEvidenceDigest": _digest(
281
+ {
282
+ "tenantId": tenant_id,
283
+ "agentId": agent_id,
284
+ "runtimeSessionId": runtime_id,
285
+ "guardPublicKeyJwk": _jwk(guard_x, guard_key_id),
286
+ }
287
+ ),
288
+ }
289
+ unsigned = {
290
+ "version": VERSION,
291
+ "attestationId": "runtime-guard:" + evidence_id,
292
+ "manifest": manifest,
293
+ "manifestHash": _digest(manifest),
294
+ "attestedAt": created_at,
295
+ "signerKeyId": guard_key_id,
296
+ "signatureAlgorithm": "Ed25519",
297
+ }
298
+ private = Ed25519PrivateKey.from_private_bytes(
299
+ _decode(guard["private_key"], field="guard private key")
300
+ )
301
+ return {**unsigned, "signature": _b64url(private.sign(_canonical(unsigned)))}
302
+
303
+
304
+ def build_preexchange_evidence(
305
+ *,
306
+ attestation: dict[str, Any],
307
+ guard: dict[str, str],
308
+ run_id: str,
309
+ root_action_id: str,
310
+ action: str,
311
+ resource: str,
312
+ payload: dict[str, Any],
313
+ descriptor_digest: str,
314
+ occurred_at: str,
315
+ batch_id: str | None = None,
316
+ idempotency_key: str | None = None,
317
+ ) -> dict[str, Any]:
318
+ guard = ensure_runtime_evidence_credential(guard)
319
+ descriptor_digest = _require_digest(descriptor_digest, "descriptor digest")
320
+ occurred_at = _require_time(occurred_at, "receipt time")
321
+ if not all(
322
+ isinstance(item, str) and item
323
+ for item in (run_id, root_action_id, action, resource)
324
+ ):
325
+ raise ValueError("incomplete pre-exchange evidence context")
326
+ if not isinstance(payload, dict):
327
+ raise ValueError("invalid action payload")
328
+ manifest = attestation.get("manifest")
329
+ if not isinstance(manifest, dict) or attestation.get("manifestHash") != _digest(
330
+ manifest
331
+ ):
332
+ raise ValueError("invalid runtime attestation")
333
+ required = manifest.get("requiredReceipts")
334
+ if not isinstance(required, list) or [
335
+ (item.get("layerId"), item.get("adapterId"), item.get("signerClass"))
336
+ for item in required[:5]
337
+ ] != list(_LAYERS[:5]):
338
+ raise ValueError("runtime manifest does not carry the pre-exchange contract")
339
+
340
+ receipt_private = Ed25519PrivateKey.from_private_bytes(
341
+ _decode(guard["harness_receipt_private_key"], field="receipt private key")
342
+ )
343
+ guard_private = Ed25519PrivateKey.from_private_bytes(
344
+ _decode(guard["private_key"], field="guard private key")
345
+ )
346
+ base_context = {
347
+ "tenantId": manifest["tenantId"],
348
+ "agentId": manifest["agentId"],
349
+ "runtimeSessionId": manifest["runtimeSessionId"],
350
+ "runId": run_id,
351
+ "rootActionId": root_action_id,
352
+ "canonicalAction": action,
353
+ "resource": resource,
354
+ "payloadDigest": _digest(payload),
355
+ "descriptorDigest": descriptor_digest,
356
+ }
357
+ reasons = {
358
+ "identity.session": "runtime_identity_observed",
359
+ "run.ownership": "runtime_run_ownership_observed",
360
+ "tool.discovery": "declared_tool_discovered",
361
+ "tool.invocation": "guarded_tool_invocation_observed",
362
+ "mcp.access": "runtime_guard_mcp_access_observed",
363
+ }
364
+ receipts: list[dict[str, Any]] = []
365
+ prior: str | None = None
366
+ for sequence, (layer, adapter, signer_class) in enumerate(_LAYERS[:5], start=1):
367
+ binding_name = (
368
+ "guardWorkloadKey"
369
+ if signer_class == "runtime_guard"
370
+ else "harnessReceiptKey"
371
+ )
372
+ private = guard_private if signer_class == "runtime_guard" else receipt_private
373
+ unsigned = {
374
+ "version": VERSION,
375
+ "receiptId": str(uuid.uuid4()),
376
+ "tenantId": manifest["tenantId"],
377
+ "agentId": manifest["agentId"],
378
+ "runId": run_id,
379
+ "taskId": root_action_id,
380
+ "rootActionId": root_action_id,
381
+ "attestationId": attestation["attestationId"],
382
+ "manifestHash": attestation["manifestHash"],
383
+ "layerId": layer,
384
+ "adapterId": adapter,
385
+ "signerClass": signer_class,
386
+ "policyVersion": "pnxs-runtime-assurance/v1",
387
+ "catalogVersion": "1",
388
+ "decision": "not_applicable",
389
+ "priorReceiptHash": prior,
390
+ "inputDigest": _digest(
391
+ {**base_context, "layerId": layer, "phase": "input"}
392
+ ),
393
+ "outputDigest": _digest(
394
+ {**base_context, "layerId": layer, "outcome": "observed"}
395
+ ),
396
+ "outcome": "observed",
397
+ "reasonCode": reasons[layer],
398
+ "sequence": sequence,
399
+ "occurredAt": occurred_at,
400
+ "signerKeyId": manifest[binding_name]["keyId"],
401
+ "signatureAlgorithm": "Ed25519",
402
+ }
403
+ signed = {**unsigned, "signature": _b64url(private.sign(_canonical(unsigned)))}
404
+ receipt = {**signed, "receiptHash": _digest(signed)}
405
+ receipts.append(receipt)
406
+ prior = receipt["receiptHash"]
407
+ return {
408
+ "version": VERSION,
409
+ "batchId": batch_id or str(uuid.uuid4()),
410
+ "tenantId": manifest["tenantId"],
411
+ "submitterSignerClass": "runtime_guard",
412
+ "receipts": receipts,
413
+ "submittedAt": occurred_at,
414
+ "idempotencyKey": idempotency_key or str(uuid.uuid4()),
415
+ }
416
+
417
+
418
+ __all__ = [
419
+ "build_preexchange_evidence",
420
+ "build_runtime_attestation",
421
+ "ensure_runtime_evidence_credential",
422
+ ]
@@ -1,2 +0,0 @@
1
- # generated by the PaloNexus build hook
2
- SOURCE_REVISION = "58e2607d1c2ed32be5a4fc7c9a334be66bd8e347"
File without changes
File without changes
File without changes
File without changes