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.
- apps/__init__.py +2 -0
- apps/api/__init__.py +2 -0
- apps/api/main.py +652 -0
- apps/benchmarks/__init__.py +1 -0
- apps/benchmarks/main.py +20 -0
- apps/sandbox/__init__.py +1 -0
- apps/sandbox/main.py +20 -0
- apps/worker/__init__.py +2 -0
- apps/worker/main.py +15 -0
- apps/worker/verify.py +14 -0
- patchr/__init__.py +12 -0
- patchr/sdk/__init__.py +20 -0
- patchr/sdk/client.py +12 -0
- patchr-0.1.0.dist-info/METADATA +137 -0
- patchr-0.1.0.dist-info/RECORD +116 -0
- patchr-0.1.0.dist-info/WHEEL +5 -0
- patchr-0.1.0.dist-info/entry_points.txt +5 -0
- patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
- patchr-0.1.0.dist-info/top_level.txt +3 -0
- picux/__init__.py +6 -0
- picux/agents/__init__.py +5 -0
- picux/agents/registry.py +204 -0
- picux/api/__init__.py +5 -0
- picux/api/service.py +5075 -0
- picux/audit/__init__.py +31 -0
- picux/audit/activity.py +97 -0
- picux/audit/observability.py +55 -0
- picux/audit/verification/__init__.py +21 -0
- picux/audit/verification/ledger.py +633 -0
- picux/benchmarks/__init__.py +5 -0
- picux/benchmarks/local.py +286 -0
- picux/config.py +140 -0
- picux/contracts/__init__.py +22 -0
- picux/contracts/handshake.py +122 -0
- picux/contracts/integration.py +385 -0
- picux/contracts/openapi.py +187 -0
- picux/contracts/protocol_map.py +152 -0
- picux/contracts/routes.py +980 -0
- picux/contracts/schema_catalog.py +125 -0
- picux/core/__init__.py +17 -0
- picux/core/models.py +148 -0
- picux/core/router.py +131 -0
- picux/core/runtime.py +42 -0
- picux/core/state_machine.py +38 -0
- picux/domains/__init__.py +2 -0
- picux/domains/bridge/HostRun.py +1104 -0
- picux/domains/bridge/__init__.py +6 -0
- picux/domains/bridge/engine.py +345 -0
- picux/domains/hunt/__init__.py +6 -0
- picux/domains/hunt/engine.py +307 -0
- picux/domains/hunt/models.py +88 -0
- picux/domains/pay/__init__.py +16 -0
- picux/domains/pay/adapters.py +607 -0
- picux/domains/pay/engine.py +950 -0
- picux/domains/pay/models.py +95 -0
- picux/domains/proxy/__init__.py +5 -0
- picux/domains/proxy/engine.py +466 -0
- picux/domains/resolve/__init__.py +5 -0
- picux/domains/resolve/engine.py +546 -0
- picux/orchestrator/__init__.py +3 -0
- picux/orchestrator/engine.py +2840 -0
- picux/portals/__init__.py +17 -0
- picux/portals/templates.py +272 -0
- picux/protocols/__init__.py +1 -0
- picux/protocols/a2a/__init__.py +6 -0
- picux/protocols/a2a/client.py +51 -0
- picux/protocols/a2a/envelope.py +132 -0
- picux/protocols/mcp/__init__.py +7 -0
- picux/protocols/mcp/client.py +69 -0
- picux/protocols/mcp/contract.py +67 -0
- picux/protocols/mcp/server.py +76 -0
- picux/sandbox/__init__.py +6 -0
- picux/sandbox/midnight_arbitrage.py +215 -0
- picux/sandbox/models.py +90 -0
- picux/sdk/__init__.py +13 -0
- picux/sdk/client.py +768 -0
- picux/sdk/external.py +245 -0
- picux/security/__init__.py +18 -0
- picux/security/auth.py +86 -0
- picux/security/config_validator.py +58 -0
- picux/security/policy.py +158 -0
- picux/security/secrets.py +144 -0
- picux/signals/__init__.py +1 -0
- picux/signals/community/__init__.py +24 -0
- picux/signals/community/adapters/__init__.py +7 -0
- picux/signals/community/adapters/reddit.py +37 -0
- picux/signals/community/adapters/shopify.py +23 -0
- picux/signals/community/adapters/web.py +23 -0
- picux/signals/community/disambiguation.py +51 -0
- picux/signals/community/intake.py +227 -0
- picux/signals/community/models.py +102 -0
- picux/signals/community/rules.py +91 -0
- picux/signals/community/scoring.py +64 -0
- picux/storage/__init__.py +41 -0
- picux/storage/agents.py +50 -0
- picux/storage/cases.py +440 -0
- picux/storage/channels.py +476 -0
- picux/storage/connectors.py +411 -0
- picux/storage/envelopes.py +137 -0
- picux/storage/escrows.py +168 -0
- picux/storage/events.py +989 -0
- picux/storage/keyspace.py +60 -0
- picux/storage/mandates.py +107 -0
- picux/storage/portals.py +222 -0
- picux/storage/postgres.py +2049 -0
- picux/storage/providers.py +148 -0
- picux/storage/proxy.py +231 -0
- picux/storage/receipts.py +131 -0
- picux/storage/signals.py +147 -0
- picux/storage/tasks.py +179 -0
- picux/tools/__init__.py +11 -0
- picux/tools/shared.py +2048 -0
- picux/verification/__init__.py +5 -0
- picux/verification/rollout.py +183 -0
- picux/workflows/__init__.py +5 -0
- 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
|
+
}
|