patchr 0.1.0__py3-none-any.whl

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 (116) hide show
  1. apps/__init__.py +2 -0
  2. apps/api/__init__.py +2 -0
  3. apps/api/main.py +652 -0
  4. apps/benchmarks/__init__.py +1 -0
  5. apps/benchmarks/main.py +20 -0
  6. apps/sandbox/__init__.py +1 -0
  7. apps/sandbox/main.py +20 -0
  8. apps/worker/__init__.py +2 -0
  9. apps/worker/main.py +15 -0
  10. apps/worker/verify.py +14 -0
  11. patchr/__init__.py +12 -0
  12. patchr/sdk/__init__.py +20 -0
  13. patchr/sdk/client.py +12 -0
  14. patchr-0.1.0.dist-info/METADATA +137 -0
  15. patchr-0.1.0.dist-info/RECORD +116 -0
  16. patchr-0.1.0.dist-info/WHEEL +5 -0
  17. patchr-0.1.0.dist-info/entry_points.txt +5 -0
  18. patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
  19. patchr-0.1.0.dist-info/top_level.txt +3 -0
  20. picux/__init__.py +6 -0
  21. picux/agents/__init__.py +5 -0
  22. picux/agents/registry.py +204 -0
  23. picux/api/__init__.py +5 -0
  24. picux/api/service.py +5075 -0
  25. picux/audit/__init__.py +31 -0
  26. picux/audit/activity.py +97 -0
  27. picux/audit/observability.py +55 -0
  28. picux/audit/verification/__init__.py +21 -0
  29. picux/audit/verification/ledger.py +633 -0
  30. picux/benchmarks/__init__.py +5 -0
  31. picux/benchmarks/local.py +286 -0
  32. picux/config.py +140 -0
  33. picux/contracts/__init__.py +22 -0
  34. picux/contracts/handshake.py +122 -0
  35. picux/contracts/integration.py +385 -0
  36. picux/contracts/openapi.py +187 -0
  37. picux/contracts/protocol_map.py +152 -0
  38. picux/contracts/routes.py +980 -0
  39. picux/contracts/schema_catalog.py +125 -0
  40. picux/core/__init__.py +17 -0
  41. picux/core/models.py +148 -0
  42. picux/core/router.py +131 -0
  43. picux/core/runtime.py +42 -0
  44. picux/core/state_machine.py +38 -0
  45. picux/domains/__init__.py +2 -0
  46. picux/domains/bridge/HostRun.py +1104 -0
  47. picux/domains/bridge/__init__.py +6 -0
  48. picux/domains/bridge/engine.py +345 -0
  49. picux/domains/hunt/__init__.py +6 -0
  50. picux/domains/hunt/engine.py +307 -0
  51. picux/domains/hunt/models.py +88 -0
  52. picux/domains/pay/__init__.py +16 -0
  53. picux/domains/pay/adapters.py +607 -0
  54. picux/domains/pay/engine.py +950 -0
  55. picux/domains/pay/models.py +95 -0
  56. picux/domains/proxy/__init__.py +5 -0
  57. picux/domains/proxy/engine.py +466 -0
  58. picux/domains/resolve/__init__.py +5 -0
  59. picux/domains/resolve/engine.py +546 -0
  60. picux/orchestrator/__init__.py +3 -0
  61. picux/orchestrator/engine.py +2840 -0
  62. picux/portals/__init__.py +17 -0
  63. picux/portals/templates.py +272 -0
  64. picux/protocols/__init__.py +1 -0
  65. picux/protocols/a2a/__init__.py +6 -0
  66. picux/protocols/a2a/client.py +51 -0
  67. picux/protocols/a2a/envelope.py +132 -0
  68. picux/protocols/mcp/__init__.py +7 -0
  69. picux/protocols/mcp/client.py +69 -0
  70. picux/protocols/mcp/contract.py +67 -0
  71. picux/protocols/mcp/server.py +76 -0
  72. picux/sandbox/__init__.py +6 -0
  73. picux/sandbox/midnight_arbitrage.py +215 -0
  74. picux/sandbox/models.py +90 -0
  75. picux/sdk/__init__.py +13 -0
  76. picux/sdk/client.py +768 -0
  77. picux/sdk/external.py +245 -0
  78. picux/security/__init__.py +18 -0
  79. picux/security/auth.py +86 -0
  80. picux/security/config_validator.py +58 -0
  81. picux/security/policy.py +158 -0
  82. picux/security/secrets.py +144 -0
  83. picux/signals/__init__.py +1 -0
  84. picux/signals/community/__init__.py +24 -0
  85. picux/signals/community/adapters/__init__.py +7 -0
  86. picux/signals/community/adapters/reddit.py +37 -0
  87. picux/signals/community/adapters/shopify.py +23 -0
  88. picux/signals/community/adapters/web.py +23 -0
  89. picux/signals/community/disambiguation.py +51 -0
  90. picux/signals/community/intake.py +227 -0
  91. picux/signals/community/models.py +102 -0
  92. picux/signals/community/rules.py +91 -0
  93. picux/signals/community/scoring.py +64 -0
  94. picux/storage/__init__.py +41 -0
  95. picux/storage/agents.py +50 -0
  96. picux/storage/cases.py +440 -0
  97. picux/storage/channels.py +476 -0
  98. picux/storage/connectors.py +411 -0
  99. picux/storage/envelopes.py +137 -0
  100. picux/storage/escrows.py +168 -0
  101. picux/storage/events.py +989 -0
  102. picux/storage/keyspace.py +60 -0
  103. picux/storage/mandates.py +107 -0
  104. picux/storage/portals.py +222 -0
  105. picux/storage/postgres.py +2049 -0
  106. picux/storage/providers.py +148 -0
  107. picux/storage/proxy.py +231 -0
  108. picux/storage/receipts.py +131 -0
  109. picux/storage/signals.py +147 -0
  110. picux/storage/tasks.py +179 -0
  111. picux/tools/__init__.py +11 -0
  112. picux/tools/shared.py +2048 -0
  113. picux/verification/__init__.py +5 -0
  114. picux/verification/rollout.py +183 -0
  115. picux/workflows/__init__.py +5 -0
  116. picux/workflows/templates.py +74 -0
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import time
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+ from picux.api import PicuxApiService
11
+ from picux.domains.pay import PayDomain
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class BenchmarkSample:
16
+ name: str
17
+ label: str
18
+ count: int
19
+ ok: bool
20
+ elapsedMs: float
21
+ avgMs: float
22
+ p50Ms: float
23
+ p95Ms: float
24
+ minMs: float
25
+ maxMs: float
26
+ opsPerSec: float
27
+ unit: str
28
+ errors: tuple[str, ...] = ()
29
+
30
+ def toMap(self) -> dict[str, Any]:
31
+ return {
32
+ "name": self.name,
33
+ "label": self.label,
34
+ "count": self.count,
35
+ "ok": self.ok,
36
+ "elapsedMs": self.elapsedMs,
37
+ "avgMs": self.avgMs,
38
+ "p50Ms": self.p50Ms,
39
+ "p95Ms": self.p95Ms,
40
+ "minMs": self.minMs,
41
+ "maxMs": self.maxMs,
42
+ "opsPerSec": self.opsPerSec,
43
+ "unit": self.unit,
44
+ "errors": list(self.errors),
45
+ }
46
+
47
+
48
+ class LocalBenchmark:
49
+ """Reproducible local route benchmark for protocol evidence labels."""
50
+
51
+ def __init__(self, *, service: PicuxApiService | None = None, iterations: int = 100) -> None:
52
+ self.iterations = _iterations(iterations)
53
+ self.service = service or PicuxApiService(pay=PayDomain(verifier=self._verifier, clock=self._clock))
54
+
55
+ def run(self) -> dict[str, Any]:
56
+ generatedAt = self._clock().isoformat().replace("+00:00", "Z")
57
+ samples = [
58
+ self._loop("taskCreate", self._taskCreate, unit="routeCall"),
59
+ self._loop("policyEval", self._policyEval, unit="routeCall"),
60
+ self._loop("settlementPrepare", self._settlementPrepare, unit="routeCall"),
61
+ self._deliveryBatch(),
62
+ ]
63
+ sampleMaps = [sample.toMap() for sample in samples]
64
+ elapsedMs = round(sum(sample.elapsedMs for sample in samples), 3)
65
+ measuredCount = sum(sample.count for sample in samples)
66
+ report = {
67
+ "ok": all(sample.ok for sample in samples),
68
+ "kind": "localBenchmark",
69
+ "label": "measuredLocal",
70
+ "generatedAt": generatedAt,
71
+ "iterations": self.iterations,
72
+ "elapsedMs": elapsedMs,
73
+ "measuredCount": measuredCount,
74
+ "samples": sampleMaps,
75
+ "claims": self._claims(sampleMaps),
76
+ "notes": [
77
+ "Local process benchmark only; no network, provider, database, or payment rail latency is included.",
78
+ "Numbers are evidence for regression tracking, not production SLA claims.",
79
+ ],
80
+ }
81
+ report["reportId"] = self._reportId(report)
82
+ return report
83
+
84
+ def _loop(self, name: str, fn, *, unit: str) -> BenchmarkSample:
85
+ durations: list[int] = []
86
+ errors: list[str] = []
87
+ for idx in range(self.iterations):
88
+ start = time.perf_counter_ns()
89
+ ok, error = fn(idx)
90
+ durations.append(time.perf_counter_ns() - start)
91
+ if not ok:
92
+ errors.append(error or "failed")
93
+ return _sample(name, durations, unit=unit, errors=errors)
94
+
95
+ def _deliveryBatch(self) -> BenchmarkSample:
96
+ for idx in range(self.iterations):
97
+ self.service.handle(
98
+ "POST",
99
+ "/v1/event-deliveries",
100
+ {
101
+ "eventId": f"bench_event_{idx}",
102
+ "subId": "bench_sub",
103
+ "clientId": "bench_client",
104
+ "transport": "poll",
105
+ "createdAt": 1_800_000_000 + idx,
106
+ },
107
+ )
108
+ start = time.perf_counter_ns()
109
+ _status, result = self.service.handle(
110
+ "POST",
111
+ "/v1/event-deliveries/claimBatch",
112
+ {"claimedBy": "bench_worker", "limit": self.iterations, "leaseSeconds": 60},
113
+ )
114
+ duration = time.perf_counter_ns() - start
115
+ count = int(result.get("count", 0) or 0)
116
+ errors = () if result.get("ok") and count == self.iterations else ("claimBatchMismatch",)
117
+ return _sample("eventDeliveryClaimBatch", [duration], unit=f"{count} deliveries", count=count, errors=list(errors))
118
+
119
+ def _taskCreate(self, idx: int) -> tuple[bool, str]:
120
+ status, result = self.service.handle(
121
+ "POST",
122
+ "/v1/tasks",
123
+ {
124
+ "taskId": f"bench_task_{idx}",
125
+ "userId": "bench",
126
+ "domain": "hunt",
127
+ "goal": "benchmark task route",
128
+ },
129
+ )
130
+ return status == 200 and result.get("ok") is True, str(result.get("error", ""))
131
+
132
+ def _policyEval(self, idx: int) -> tuple[bool, str]:
133
+ status, result = self.service.handle("POST", "/v1/security/policy/evaluate", self._policyPayload(idx))
134
+ return status == 200 and result.get("ok") is True, str(result.get("error", ""))
135
+
136
+ def _settlementPrepare(self, idx: int) -> tuple[bool, str]:
137
+ status, result = self.service.handle("POST", "/v1/pay/prepare", self._payPayload(idx))
138
+ return status == 200 and result.get("ok") is True, str(result.get("error", ""))
139
+
140
+ @staticmethod
141
+ def _policyPayload(idx: int) -> dict[str, Any]:
142
+ resource = f"escrow:bench_{idx}"
143
+ return {
144
+ "subjectId": "bench_agent",
145
+ "domain": "pay",
146
+ "action": "pay.settle",
147
+ "resource": resource,
148
+ "mandateId": "bench_payment",
149
+ "povRef": "bench_pov",
150
+ "session": {
151
+ "sessionId": "bench_session",
152
+ "status": "active",
153
+ "subjectId": "bench_agent",
154
+ "mandateId": "bench_payment",
155
+ "domains": ["pay"],
156
+ "actions": ["pay.settle"],
157
+ "expiresAt": "2099-01-01T00:00:00Z",
158
+ },
159
+ "grants": [
160
+ {
161
+ "grantId": "bench_grant",
162
+ "subjectId": "bench_agent",
163
+ "domains": ["pay"],
164
+ "actions": ["pay.settle"],
165
+ "resources": [resource],
166
+ "expiresAt": "2099-01-01T00:00:00Z",
167
+ }
168
+ ],
169
+ }
170
+
171
+ @staticmethod
172
+ def _payPayload(idx: int) -> dict[str, Any]:
173
+ return {
174
+ "paymentMandate": {
175
+ "mandateId": "bench_payment",
176
+ "kind": "payment",
177
+ "intentMandateId": "bench_intent",
178
+ "issuer": {"entityId": "bench_tenant", "publicKey": "benchPub"},
179
+ "payer": {"entityId": "bench_buyer", "accountRef": "vault:bench:payer"},
180
+ "payee": {"entityId": "bench_vendor", "accountRef": "vault:bench:payee"},
181
+ "amount": {"amount": 80, "currency": "USD"},
182
+ "rail": {"railId": "benchRail", "network": "local", "asset": "USD"},
183
+ "constraints": {"resolveRules": ["benchProof"]},
184
+ "validUntil": "2099-01-01T00:00:00Z",
185
+ "signature": "benchSig",
186
+ },
187
+ "request": {
188
+ "taskId": f"bench_task_{idx}",
189
+ "payeeId": "bench_vendor",
190
+ "amount": {"amount": 80, "currency": "USD"},
191
+ "pov": {"povId": "bench_pov", "rules": ["benchProof"]},
192
+ },
193
+ "adapter": {"adapterId": "benchAdapter", "rail": "benchRail"},
194
+ }
195
+
196
+ @staticmethod
197
+ def _claims(samples: list[dict[str, Any]]) -> list[dict[str, Any]]:
198
+ byName = {str(sample.get("name", "")): sample for sample in samples}
199
+ settlement = byName.get("settlementPrepare", {})
200
+ taskCreate = byName.get("taskCreate", {})
201
+ return [
202
+ {
203
+ "claimId": "taskThroughput10k",
204
+ "label": "unverifiedTarget",
205
+ "target": "10000 tasks/sec",
206
+ "measuredLocal": taskCreate.get("opsPerSec", 0.0),
207
+ "note": "The brief target requires a dedicated production-like benchmark before public use.",
208
+ },
209
+ {
210
+ "claimId": "settlementSub500ms",
211
+ "label": "measuredLocal",
212
+ "target": "<500ms local settlement preparation",
213
+ "measuredP95Ms": settlement.get("p95Ms", 0.0),
214
+ "note": "This measures local PAY preparation only, not live payment execution.",
215
+ },
216
+ {
217
+ "claimId": "oversightReduction",
218
+ "label": "unverifiedTarget",
219
+ "target": "94% oversight reduction",
220
+ "note": "Requires case-study instrumentation outside this local benchmark.",
221
+ },
222
+ ]
223
+
224
+ @staticmethod
225
+ def _reportId(report: dict[str, Any]) -> str:
226
+ payload = {key: value for key, value in report.items() if key != "reportId"}
227
+ digest = hashlib.sha256(
228
+ json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
229
+ ).hexdigest()[:24]
230
+ return f"bench_{digest}"
231
+
232
+ @staticmethod
233
+ def _clock() -> datetime:
234
+ return datetime(2026, 5, 9, 12, 0, tzinfo=timezone.utc)
235
+
236
+ @staticmethod
237
+ def _verifier(payload: dict[str, Any], signature: str, publicKey: str) -> bool:
238
+ return payload.get("mandateId") == "bench_payment" and signature == "benchSig" and publicKey == "benchPub"
239
+
240
+
241
+ def _sample(
242
+ name: str,
243
+ durations: list[int],
244
+ *,
245
+ unit: str,
246
+ count: int | None = None,
247
+ errors: list[str] | None = None,
248
+ ) -> BenchmarkSample:
249
+ errors = errors or []
250
+ durations = durations or [0]
251
+ sampleCount = int(count if count is not None else len(durations))
252
+ elapsedNs = sum(durations)
253
+ elapsedMs = elapsedNs / 1_000_000
254
+ avgMs = (elapsedNs / max(1, sampleCount)) / 1_000_000
255
+ opsPerSec = (sampleCount / (elapsedNs / 1_000_000_000)) if elapsedNs > 0 else 0.0
256
+ return BenchmarkSample(
257
+ name=name,
258
+ label="measuredLocal",
259
+ count=sampleCount,
260
+ ok=not errors,
261
+ elapsedMs=round(elapsedMs, 3),
262
+ avgMs=round(avgMs, 3),
263
+ p50Ms=round(_percentile(durations, 0.50) / 1_000_000, 3),
264
+ p95Ms=round(_percentile(durations, 0.95) / 1_000_000, 3),
265
+ minMs=round(min(durations) / 1_000_000, 3),
266
+ maxMs=round(max(durations) / 1_000_000, 3),
267
+ opsPerSec=round(opsPerSec, 3),
268
+ unit=unit,
269
+ errors=tuple(dict.fromkeys(errors)),
270
+ )
271
+
272
+
273
+ def _percentile(values: list[int], pct: float) -> int:
274
+ ordered = sorted(values)
275
+ if not ordered:
276
+ return 0
277
+ idx = min(len(ordered) - 1, max(0, int(round((len(ordered) - 1) * pct))))
278
+ return ordered[idx]
279
+
280
+
281
+ def _iterations(value: int) -> int:
282
+ try:
283
+ parsed = int(value)
284
+ except (TypeError, ValueError):
285
+ return 100
286
+ return max(1, min(parsed, 1000))
picux/config.py ADDED
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Mapping, Optional
6
+
7
+
8
+ LEGACY_ENV_ALIASES: dict[str, tuple[str, ...]] = {
9
+ "PICUX_ENV": ("SWARM_ENV",),
10
+ "PICUX_PRODUCTION_MODE": ("SWARM_PRODUCTION_MODE",),
11
+ "PICUX_API_BASE_URL": ("SWARM_API_BASE_URL",),
12
+ "PICUX_API_TOKEN": ("SWARM_API_TOKEN",),
13
+ "PICUX_REDIS_URL": ("REDIS_URL",),
14
+ "PICUX_POSTGRES_DSN": ("SWARM_POSTGRES_DSN",),
15
+ "PICUX_MCP_BASE_URL": ("SWARM_MCP_BASE_URL",),
16
+ "PICUX_MCP_TOKEN": ("SWARM_MCP_TOKEN",),
17
+ "PICUX_A2A_BASE_URL": ("SWARM_A2A_BASE_URL",),
18
+ "PICUX_A2A_TOKEN": ("SWARM_A2A_TOKEN",),
19
+ "PICUX_BRIDGE_API_BASE": ("SWARM_BRIDGE_API_BASE",),
20
+ "PICUX_BRIDGE_API_TOKEN": ("SWARM_BRIDGE_API_TOKEN",),
21
+ "PICUX_POLICY_ENFORCEMENT": ("SWARM_POLICY_ENFORCEMENT",),
22
+ "PICUX_RESOLVE_OUTCOME_WEBHOOK_TOKEN": ("SWARM_RESOLVE_OUTCOME_WEBHOOK_TOKEN",),
23
+ "PICUX_BRIDGE_OUTCOME_WEBHOOK_TOKEN": ("SWARM_BRIDGE_OUTCOME_WEBHOOK_TOKEN",),
24
+ "PICUX_VAULT_KEY_B64": ("SWARM_VAULT_KEY_B64",),
25
+ }
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class EnvValue:
30
+ name: str
31
+ value: str
32
+ source: str
33
+
34
+
35
+ def resolve_env(
36
+ name: str,
37
+ default: str = "",
38
+ *,
39
+ env: Optional[Mapping[str, str]] = None,
40
+ ) -> EnvValue:
41
+ """Resolve a Picux env var with temporary legacy fallbacks."""
42
+
43
+ source_env = os.environ if env is None else env
44
+ if name in source_env and str(source_env[name]).strip() != "":
45
+ return EnvValue(name=name, value=str(source_env[name]), source=name)
46
+
47
+ for alias in LEGACY_ENV_ALIASES.get(name, ()):
48
+ if alias in source_env and str(source_env[alias]).strip() != "":
49
+ return EnvValue(name=name, value=str(source_env[alias]), source=alias)
50
+
51
+ return EnvValue(name=name, value=default, source="default")
52
+
53
+
54
+ def env_bool(value: str, *, default: bool = False) -> bool:
55
+ if value is None or str(value).strip() == "":
56
+ return default
57
+ return str(value).strip().lower() in {"1", "true", "yes", "on", "prod", "production", "live"}
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class PicuxSettings:
62
+ env: str
63
+ prod: bool
64
+ apiUrl: str
65
+ apiToken: str
66
+ redisUrl: str
67
+ postgresDsn: str
68
+ mcpUrl: str
69
+ mcpToken: str
70
+ a2aUrl: str
71
+ a2aToken: str
72
+ bridgeUrl: str
73
+ bridgeToken: str
74
+ policyEnforcement: bool
75
+ resolveToken: str
76
+ bridgeOutcomeToken: str
77
+ vaultKey: str
78
+ sources: dict[str, str]
79
+
80
+ @classmethod
81
+ def fromEnv(cls, env: Optional[Mapping[str, str]] = None) -> "PicuxSettings":
82
+ resolved = {
83
+ name: resolve_env(name, default=default, env=env)
84
+ for name, default in {
85
+ "PICUX_ENV": "local",
86
+ "PICUX_PRODUCTION_MODE": "false",
87
+ "PICUX_API_BASE_URL": "http://127.0.0.1:8088",
88
+ "PICUX_API_TOKEN": "",
89
+ "PICUX_REDIS_URL": "redis://localhost:6379/0",
90
+ "PICUX_POSTGRES_DSN": "",
91
+ "PICUX_MCP_BASE_URL": "",
92
+ "PICUX_MCP_TOKEN": "",
93
+ "PICUX_A2A_BASE_URL": "",
94
+ "PICUX_A2A_TOKEN": "",
95
+ "PICUX_BRIDGE_API_BASE": "",
96
+ "PICUX_BRIDGE_API_TOKEN": "",
97
+ "PICUX_POLICY_ENFORCEMENT": "",
98
+ "PICUX_RESOLVE_OUTCOME_WEBHOOK_TOKEN": "",
99
+ "PICUX_BRIDGE_OUTCOME_WEBHOOK_TOKEN": "",
100
+ "PICUX_VAULT_KEY_B64": "",
101
+ }.items()
102
+ }
103
+
104
+ env_name = resolved["PICUX_ENV"].value.strip() or "local"
105
+ explicit_production = env_bool(resolved["PICUX_PRODUCTION_MODE"].value, default=False)
106
+ prod = explicit_production or env_name.strip().lower() in {"prod", "production", "live"}
107
+
108
+ return cls(
109
+ env=env_name,
110
+ prod=prod,
111
+ apiUrl=resolved["PICUX_API_BASE_URL"].value,
112
+ apiToken=resolved["PICUX_API_TOKEN"].value,
113
+ redisUrl=resolved["PICUX_REDIS_URL"].value,
114
+ postgresDsn=resolved["PICUX_POSTGRES_DSN"].value,
115
+ mcpUrl=resolved["PICUX_MCP_BASE_URL"].value,
116
+ mcpToken=resolved["PICUX_MCP_TOKEN"].value,
117
+ a2aUrl=resolved["PICUX_A2A_BASE_URL"].value,
118
+ a2aToken=resolved["PICUX_A2A_TOKEN"].value,
119
+ bridgeUrl=resolved["PICUX_BRIDGE_API_BASE"].value,
120
+ bridgeToken=resolved["PICUX_BRIDGE_API_TOKEN"].value,
121
+ policyEnforcement=env_bool(resolved["PICUX_POLICY_ENFORCEMENT"].value, default=prod),
122
+ resolveToken=resolved["PICUX_RESOLVE_OUTCOME_WEBHOOK_TOKEN"].value,
123
+ bridgeOutcomeToken=resolved["PICUX_BRIDGE_OUTCOME_WEBHOOK_TOKEN"].value,
124
+ vaultKey=resolved["PICUX_VAULT_KEY_B64"].value,
125
+ sources={name: item.source for name, item in resolved.items()},
126
+ )
127
+
128
+ def publicSummary(self) -> dict[str, object]:
129
+ return {
130
+ "env": self.env,
131
+ "prod": self.prod,
132
+ "api": bool(self.apiUrl),
133
+ "redis": bool(self.redisUrl),
134
+ "postgres": bool(self.postgresDsn),
135
+ "mcp": bool(self.mcpUrl),
136
+ "a2a": bool(self.a2aUrl),
137
+ "bridge": bool(self.bridgeUrl),
138
+ "policyEnforcement": self.policyEnforcement,
139
+ "vaultKey": bool(self.vaultKey),
140
+ }
@@ -0,0 +1,22 @@
1
+ """Machine-readable Picux protocol contracts."""
2
+
3
+ from .handshake import integrationHandshake
4
+ from .integration import integrationManifest
5
+ from .openapi import contractSummary, mcpTools, openapiSpec
6
+ from .protocol_map import protocolMap, protocolMapDigest
7
+ from .routes import ContractEndpoint, CONTRACT_ENDPOINTS
8
+ from .schema_catalog import getSchema, schemaCatalog
9
+
10
+ __all__ = [
11
+ "CONTRACT_ENDPOINTS",
12
+ "ContractEndpoint",
13
+ "contractSummary",
14
+ "getSchema",
15
+ "integrationHandshake",
16
+ "integrationManifest",
17
+ "mcpTools",
18
+ "openapiSpec",
19
+ "protocolMap",
20
+ "protocolMapDigest",
21
+ "schemaCatalog",
22
+ ]
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from picux.contracts.integration import integrationManifest
6
+ from picux.contracts.protocol_map import protocolMapDigest
7
+
8
+
9
+ REQUIRED_SURFACES = {
10
+ "agent": ("api", "a2a", "schemas"),
11
+ "app": ("api", "schemas"),
12
+ "service": ("api", "schemas"),
13
+ }
14
+
15
+
16
+ def integrationHandshake(payload: dict[str, Any] | None = None) -> dict[str, Any]:
17
+ """Validate an external caller's declared Picux integration compatibility."""
18
+
19
+ payload = payload or {}
20
+ manifest = integrationManifest()
21
+ kind = _kind(payload.get("kind", payload.get("type", "service")))
22
+ clientId = str(payload.get("clientId", payload.get("agentId", payload.get("appId", ""))) or "")
23
+ surfaces = _clean(payload.get("surfaces", []))
24
+ schemaIds = _clean(payload.get("schemaIds", payload.get("schemas", [])))
25
+ mcpTools = _clean(payload.get("mcpTools", payload.get("tools", [])))
26
+ domains = _clean(payload.get("domainIds", payload.get("domains", [])))
27
+
28
+ knownSurfaces = {item["id"] for item in manifest["surfaces"]}
29
+ knownSchemas = set(manifest["schemas"]["ids"])
30
+ knownTools = set(manifest["mcpTools"])
31
+ knownDomains = set(manifest.get("domains", {}).keys())
32
+ required = set(REQUIRED_SURFACES[kind])
33
+ acceptedSurfaces = [item for item in surfaces if item in knownSurfaces]
34
+ acceptedSchemas = [item for item in schemaIds if item in knownSchemas]
35
+ acceptedTools = [item for item in mcpTools if item in knownTools]
36
+ acceptedDomains = [item for item in domains if item in knownDomains]
37
+
38
+ missingSurfaces = sorted(required.difference(surfaces))
39
+ missingSchemas = [item for item in schemaIds if item not in knownSchemas]
40
+ missingTools = [item for item in mcpTools if item not in knownTools]
41
+ unknownDomains = [item for item in domains if item not in knownDomains]
42
+ unknownSurfaces = [item for item in surfaces if item not in knownSurfaces]
43
+ errors: list[str] = []
44
+ if not clientId:
45
+ errors.append("missing:clientId")
46
+ if unknownSurfaces:
47
+ errors.append("unknown:surfaces")
48
+ if missingSchemas:
49
+ errors.append("unknown:schemas")
50
+ if missingTools:
51
+ errors.append("unknown:mcpTools")
52
+ if unknownDomains:
53
+ errors.append("unknown:domains")
54
+ if missingSurfaces:
55
+ errors.append("missing:surfaces")
56
+
57
+ ready = not errors
58
+ domainCapabilities = {
59
+ domain: _domainCapability(manifest["domains"][domain])
60
+ for domain in acceptedDomains
61
+ if isinstance(manifest.get("domains", {}).get(domain), dict)
62
+ }
63
+ contracts = dict(manifest.get("contracts", {})) if isinstance(manifest.get("contracts"), dict) else {}
64
+ return {
65
+ "ok": ready,
66
+ "ready": ready,
67
+ "clientId": clientId,
68
+ "kind": kind,
69
+ "acceptedSurfaces": acceptedSurfaces,
70
+ "missingSurfaces": missingSurfaces,
71
+ "unknownSurfaces": unknownSurfaces,
72
+ "acceptedSchemas": acceptedSchemas,
73
+ "missingSchemas": missingSchemas,
74
+ "acceptedTools": acceptedTools,
75
+ "missingTools": missingTools,
76
+ "acceptedDomains": acceptedDomains,
77
+ "unknownDomains": unknownDomains,
78
+ "domainCapabilities": domainCapabilities,
79
+ "contracts": contracts,
80
+ "manifest": {"version": manifest["version"], "url": "/v1/manifest"},
81
+ "protocolMap": _protocolMapRef(contracts, str(manifest.get("version", "") or ""), protocolMapDigest()),
82
+ "errors": errors,
83
+ }
84
+
85
+
86
+ def _kind(value: Any) -> str:
87
+ kind = str(value or "service").strip().lower()
88
+ return kind if kind in REQUIRED_SURFACES else "service"
89
+
90
+
91
+ def _clean(value: Any) -> list[str]:
92
+ if isinstance(value, str):
93
+ value = [value]
94
+ if not isinstance(value, (list, tuple, set)):
95
+ return []
96
+ cleaned: list[str] = []
97
+ for item in value:
98
+ text = str(item or "").strip()
99
+ if text and text not in cleaned:
100
+ cleaned.append(text)
101
+ return cleaned
102
+
103
+
104
+ def _domainCapability(value: dict[str, Any]) -> dict[str, Any]:
105
+ return {
106
+ "routes": value.get("routes", []) if isinstance(value.get("routes"), list) else [],
107
+ "schemas": value.get("schemas", []) if isinstance(value.get("schemas"), list) else [],
108
+ "mcpTools": value.get("mcpTools", []) if isinstance(value.get("mcpTools"), list) else [],
109
+ "sdkHelpers": value.get("sdkHelpers", []) if isinstance(value.get("sdkHelpers"), list) else [],
110
+ "policy": value.get("policy", {}) if isinstance(value.get("policy"), dict) else {},
111
+ "guide": str(value.get("guide", "") or ""),
112
+ }
113
+
114
+
115
+ def _protocolMapRef(contracts: dict[str, Any], version: str = "", digest: str = "") -> dict[str, str]:
116
+ return {
117
+ "url": str(contracts.get("protocolMap", "/v1/protocol-map") or "/v1/protocol-map"),
118
+ "version": version,
119
+ "digest": digest,
120
+ "schemaId": "protocolMap",
121
+ "mcpTool": "picux.protocolMap",
122
+ }