polyforge-v3 3.0.16__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 (94) hide show
  1. polyforge_v3/__init__.py +4 -0
  2. polyforge_v3/aihub/__init__.py +5 -0
  3. polyforge_v3/aihub/_bearer.py +20 -0
  4. polyforge_v3/aihub/adapter.py +302 -0
  5. polyforge_v3/aihub/client.py +584 -0
  6. polyforge_v3/aihub/errors.py +70 -0
  7. polyforge_v3/aihub/http_const.py +49 -0
  8. polyforge_v3/aihub/models.py +477 -0
  9. polyforge_v3/aihub/session_file.py +220 -0
  10. polyforge_v3/auth.py +74 -0
  11. polyforge_v3/cli/__init__.py +49 -0
  12. polyforge_v3/cli/admin.py +109 -0
  13. polyforge_v3/cli/doctor.py +1280 -0
  14. polyforge_v3/cli/init.py +182 -0
  15. polyforge_v3/config/__init__.py +187 -0
  16. polyforge_v3/core/__init__.py +0 -0
  17. polyforge_v3/core/artifacts.py +95 -0
  18. polyforge_v3/core/conflicts.py +58 -0
  19. polyforge_v3/core/event_schemas/__init__.py +43 -0
  20. polyforge_v3/core/event_schemas/attempt_completed.schema.json +15 -0
  21. polyforge_v3/core/event_schemas/attempt_lease_renewed.schema.json +12 -0
  22. polyforge_v3/core/event_schemas/attempt_paused.schema.json +11 -0
  23. polyforge_v3/core/event_schemas/attempt_started.schema.json +26 -0
  24. polyforge_v3/core/event_schemas/attempt_taken_over.schema.json +14 -0
  25. polyforge_v3/core/event_schemas/auth_revoked.schema.json +17 -0
  26. polyforge_v3/core/event_schemas/branch_force_pushed.schema.json +15 -0
  27. polyforge_v3/core/event_schemas/cleanup_failed.schema.json +15 -0
  28. polyforge_v3/core/event_schemas/commit.schema.json +14 -0
  29. polyforge_v3/core/event_schemas/conflict_prediction_emitted.schema.json +35 -0
  30. polyforge_v3/core/event_schemas/conflict_prediction_overridden.schema.json +37 -0
  31. polyforge_v3/core/event_schemas/conflict_resolution_assisted.schema.json +20 -0
  32. polyforge_v3/core/event_schemas/error.schema.json +14 -0
  33. polyforge_v3/core/event_schemas/external_artifact_orphaned.schema.json +17 -0
  34. polyforge_v3/core/event_schemas/external_artifact_reconciled.schema.json +22 -0
  35. polyforge_v3/core/event_schemas/lock_acquired.schema.json +16 -0
  36. polyforge_v3/core/event_schemas/lock_released.schema.json +16 -0
  37. polyforge_v3/core/event_schemas/memory_redacted.schema.json +13 -0
  38. polyforge_v3/core/event_schemas/note.schema.json +11 -0
  39. polyforge_v3/core/event_schemas/pr_opened.schema.json +16 -0
  40. polyforge_v3/core/event_schemas/pr_updated.schema.json +17 -0
  41. polyforge_v3/core/event_schemas/prepared_workspace.schema.json +15 -0
  42. polyforge_v3/core/event_schemas/push.schema.json +14 -0
  43. polyforge_v3/core/event_schemas/push_base_check_skipped.schema.json +13 -0
  44. polyforge_v3/core/event_schemas/push_blocked_base_moved.schema.json +15 -0
  45. polyforge_v3/core/event_schemas/pushed_tag.schema.json +13 -0
  46. polyforge_v3/core/event_schemas/release_included_wi.schema.json +11 -0
  47. polyforge_v3/core/event_schemas/repo_blocked.schema.json +16 -0
  48. polyforge_v3/core/event_schemas/repo_committed.schema.json +14 -0
  49. polyforge_v3/core/event_schemas/repo_prepared.schema.json +15 -0
  50. polyforge_v3/core/event_schemas/repo_recovered.schema.json +24 -0
  51. polyforge_v3/core/event_schemas/skill_returned.schema.json +15 -0
  52. polyforge_v3/core/event_schemas/tagged_repo.schema.json +14 -0
  53. polyforge_v3/core/event_schemas/work_item_completed.schema.json +16 -0
  54. polyforge_v3/core/event_schemas/work_item_filed.schema.json +20 -0
  55. polyforge_v3/core/event_schemas/work_item_updated.schema.json +17 -0
  56. polyforge_v3/core/events.py +118 -0
  57. polyforge_v3/core/events_emit.py +65 -0
  58. polyforge_v3/core/lease.py +297 -0
  59. polyforge_v3/core/locks.py +44 -0
  60. polyforge_v3/core/lost_lease.py +372 -0
  61. polyforge_v3/core/memory.py +64 -0
  62. polyforge_v3/core/run_attempts.py +108 -0
  63. polyforge_v3/core/work_items.py +86 -0
  64. polyforge_v3/ids.py +21 -0
  65. polyforge_v3/logging.py +27 -0
  66. polyforge_v3/methodology/__init__.py +5 -0
  67. polyforge_v3/methodology/artifact_types.py +64 -0
  68. polyforge_v3/methodology/memory_adapter.py +85 -0
  69. polyforge_v3/methodology/schemas/plan.schema.json +31 -0
  70. polyforge_v3/methodology/schemas/retro.schema.json +30 -0
  71. polyforge_v3/methodology/schemas/review.schema.json +30 -0
  72. polyforge_v3/methodology/schemas/spec.schema.json +31 -0
  73. polyforge_v3/scenarios/__init__.py +0 -0
  74. polyforge_v3/scenarios/capabilities.py +40 -0
  75. polyforge_v3/scenarios/protocol.py +201 -0
  76. polyforge_v3/scenarios/registry.py +61 -0
  77. polyforge_v3/server/__init__.py +8 -0
  78. polyforge_v3/server/__main__.py +382 -0
  79. polyforge_v3/server/actor_cache.py +108 -0
  80. polyforge_v3/tools/__init__.py +32 -0
  81. polyforge_v3/tools/conflicts.py +107 -0
  82. polyforge_v3/tools/events.py +104 -0
  83. polyforge_v3/tools/lifecycle.py +262 -0
  84. polyforge_v3/tools/locks.py +45 -0
  85. polyforge_v3/tools/maintenance.py +210 -0
  86. polyforge_v3/tools/memory.py +92 -0
  87. polyforge_v3/tools/registry.py +105 -0
  88. polyforge_v3/tools/release.py +364 -0
  89. polyforge_v3/tools/scenarios.py +84 -0
  90. polyforge_v3-3.0.16.dist-info/METADATA +18 -0
  91. polyforge_v3-3.0.16.dist-info/RECORD +94 -0
  92. polyforge_v3-3.0.16.dist-info/WHEEL +5 -0
  93. polyforge_v3-3.0.16.dist-info/entry_points.txt +2 -0
  94. polyforge_v3-3.0.16.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ """polyforge-v3 — single MCP server + scenario via entry_points。
2
+ v3-design.md §3 / §4。
3
+ """
4
+ __version__ = "3.0.1"
@@ -0,0 +1,5 @@
1
+ """polyforge_v3.aihub — AihubClient, ProtocolAdapter, and model definitions."""
2
+ from .adapter import AihubClientProtocolAdapter
3
+ from .client import AihubClient
4
+
5
+ __all__ = ["AihubClient", "AihubClientProtocolAdapter"]
@@ -0,0 +1,20 @@
1
+ """Per-request bearer token override via ContextVar.
2
+
3
+ Placed in `polyforge_v3.aihub` (not `server`) so `client.py` can import it
4
+ without creating a circular dependency. Both `server/__main__.py` (writer)
5
+ and `aihub/client.py` (reader) import from here.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from contextvars import ContextVar
10
+
11
+ # Set to a non-None bearer string before an aihub client call to override the
12
+ # default Authorization header for that request. Reset to None (via
13
+ # ContextVar.reset) after the call returns. Thread / task safe because
14
+ # ContextVar is per-asyncio-Task.
15
+ _CURRENT_BEARER: ContextVar[str | None] = ContextVar("_current_bearer", default=None)
16
+
17
+
18
+ def get_current_bearer() -> str | None:
19
+ """Return the active per-request bearer override, or None."""
20
+ return _CURRENT_BEARER.get()
@@ -0,0 +1,302 @@
1
+ """ProtocolAdapter — bridges AihubClient (Pydantic models) to the dict-based Protocol
2
+ that 1C composite tools expect.
3
+
4
+ design.md §10.1: 1C tools type the client by Protocol (kwargs-in / dict-out). 1B's
5
+ concrete AihubClient takes Pydantic request objects and returns Pydantic response objects.
6
+ This adapter wraps the concrete client, handles the Pydantic⟷dict translation, and stores
7
+ an optional AttemptCredential for mutating endpoints.
8
+
9
+ Usage::
10
+
11
+ from polyforge_v3.aihub.adapter import AihubClientProtocolAdapter
12
+ from polyforge_v3.aihub.client import AihubClient
13
+
14
+ concrete = AihubClient(cfg, session)
15
+ adapter = AihubClientProtocolAdapter(concrete)
16
+ adapter.set_cred(AttemptCredential(attempt_id=..., claim_epoch=..., session_secret=...))
17
+
18
+ # Now use adapter wherever 1C expects the AihubClient Protocol:
19
+ result = await adapter.create_work_item(project="marketplace", goal="fix x", ...)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import warnings
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ from .client import AihubClient
27
+
28
+ if TYPE_CHECKING:
29
+ # Runtime-lazy: at module load time `polyforge_v3.auth` is mid-loading
30
+ # (auth.py:18 triggers aihub/__init__ → this module → here). Top-level
31
+ # `from ..auth import AttemptCredential` would race auth's own class
32
+ # definition. PEP 563 annotations only need the symbol for typing tools.
33
+ from ..auth import AttemptCredential # noqa: F401
34
+ from .models import (
35
+ ClaimRequest,
36
+ CompleteAttemptRequest,
37
+ CompleteWorkItemRequest,
38
+ CreateWorkItemRequest,
39
+ DeclaredResource,
40
+ EmitEventRequest,
41
+ PauseAttemptRequest,
42
+ PredictConflictsRequest,
43
+ RenewLeaseRequest,
44
+ UpdateWorkItemRequest,
45
+ _LockSpec,
46
+ _SessionInfoFrag,
47
+ )
48
+
49
+
50
+ class AihubClientProtocolAdapter:
51
+ """Wrap the concrete AihubClient to satisfy 1C's kwargs-in / dict-out Protocol.
52
+
53
+ Maintains a stored AttemptCredential (set via set_cred() after claim) for
54
+ mutating endpoints that require it. Some 1C calls supply credentials as kwargs
55
+ directly; kwargs always override the stored credential.
56
+
57
+ Lifted from the wire e2e test (test_3stream_wire_e2e.py) into production so
58
+ the real server.py can use it when wiring 1B to 1C.
59
+
60
+ Finding 3 fix: the test had a local ProtocolAdapter; this is its canonical home.
61
+ """
62
+
63
+ def __init__(self, concrete: AihubClient) -> None:
64
+ self._c = concrete
65
+ self._cred: AttemptCredential | None = None
66
+
67
+ def set_cred(self, cred: AttemptCredential) -> None:
68
+ """Store AttemptCredential after claim so mutating endpoints can use it."""
69
+ self._cred = cred
70
+
71
+ # ------------------------------------------------------------------
72
+ # Work items
73
+ # ------------------------------------------------------------------
74
+
75
+ async def create_work_item(
76
+ self,
77
+ *,
78
+ project: str | None,
79
+ goal: str,
80
+ scenario: str,
81
+ declared_resources: list[dict[str, Any]],
82
+ labels: list[str] | None = None,
83
+ priority: str = "normal",
84
+ external_share_type: str | None = None,
85
+ external_share_key: str | None = None,
86
+ parent_work_item_id: str | None = None,
87
+ metadata: dict[str, Any] | None = None,
88
+ ) -> dict[str, Any]:
89
+ req = CreateWorkItemRequest(
90
+ project=project or "",
91
+ scenario=scenario,
92
+ goal=goal,
93
+ declared_resources=[
94
+ DeclaredResource.model_validate(r) for r in (declared_resources or [])
95
+ ],
96
+ labels=labels,
97
+ priority=priority,
98
+ external_share_type=external_share_type,
99
+ external_share_key=external_share_key,
100
+ parent_work_item_id=parent_work_item_id,
101
+ metadata=metadata,
102
+ )
103
+ wi = await self._c.create_work_item(req)
104
+ return {"work_item_id": wi.id, **wi.model_dump()}
105
+
106
+ async def predict_conflicts(
107
+ self,
108
+ *,
109
+ work_item_id: str | None,
110
+ declared_resources: list[dict[str, Any]],
111
+ project: str | None = None,
112
+ ) -> dict[str, Any]:
113
+ if project is not None:
114
+ # Prefer the explicit project kwarg (canonical path)
115
+ effective_project = project
116
+ else:
117
+ # Fall back to URI parsing (deprecated — callers should pass project= explicitly)
118
+ warnings.warn(
119
+ "predict_conflicts: project derivation from URI deprecated; pass project= explicitly",
120
+ DeprecationWarning,
121
+ stacklevel=2,
122
+ )
123
+ effective_project = "marketplace"
124
+ for r in declared_resources:
125
+ uri = r.get("uri", "")
126
+ if uri.startswith("repo:"):
127
+ effective_project = uri.removeprefix("repo:")
128
+ break
129
+ req = PredictConflictsRequest(
130
+ project=effective_project,
131
+ work_item_id=work_item_id,
132
+ declared_resources=[
133
+ DeclaredResource.model_validate(r) for r in (declared_resources or [])
134
+ ],
135
+ )
136
+ resp = await self._c.predict_conflicts(req)
137
+ return {
138
+ "severity": resp.severity,
139
+ "predictions": [p.model_dump() for p in resp.predictions],
140
+ }
141
+
142
+ async def claim_work_item(
143
+ self,
144
+ *,
145
+ work_item_id: str,
146
+ idempotency_key: str,
147
+ session_info: dict[str, Any],
148
+ requested_locks: list[dict[str, Any]],
149
+ ) -> dict[str, Any]:
150
+ # session_secret: prefer session_info kwarg, fall back to client session
151
+ secret = (
152
+ session_info.get("session_secret")
153
+ or self._c.session.session_secret
154
+ )
155
+ machine_id = session_info.get("machine_id") or self._c.session.machine_id
156
+ req = ClaimRequest(
157
+ idempotency_key=idempotency_key,
158
+ session_info=_SessionInfoFrag(
159
+ machine_id=machine_id,
160
+ session_secret=secret,
161
+ ),
162
+ requested_locks=[_LockSpec.model_validate(l) for l in requested_locks],
163
+ )
164
+ resp = await self._c.claim_work_item(work_item_id, req)
165
+ return {
166
+ "attempt_id": resp.attempt_id,
167
+ "claim_epoch": resp.claim_epoch,
168
+ "lease_until": resp.lease_until
169
+ if isinstance(resp.lease_until, str)
170
+ else str(resp.lease_until),
171
+ "session_secret": secret,
172
+ "machine_id": machine_id,
173
+ }
174
+
175
+ async def get_work_item(self, *, work_item_id: str) -> dict[str, Any]:
176
+ resp = await self._c.get_work_item(work_item_id)
177
+ return resp.model_dump(mode="json")
178
+
179
+ async def update_work_item(
180
+ self, *, work_item_id: str, patch_payload: dict[str, Any]
181
+ ) -> dict[str, Any]:
182
+ if self._cred is None:
183
+ raise RuntimeError(
184
+ "update_work_item requires AttemptCredential; call set_cred() first"
185
+ )
186
+ req = UpdateWorkItemRequest(
187
+ attempt_id=self._cred.attempt_id,
188
+ claim_epoch=self._cred.claim_epoch,
189
+ session_secret=self._cred.session_secret,
190
+ patch_payload=patch_payload,
191
+ )
192
+ wi = await self._c.update_work_item(work_item_id, req)
193
+ return {"work_item": wi.model_dump(mode="json")}
194
+
195
+ # ------------------------------------------------------------------
196
+ # Events
197
+ # ------------------------------------------------------------------
198
+
199
+ async def emit_event(
200
+ self,
201
+ *,
202
+ work_item_id: str,
203
+ event_type: str,
204
+ payload: dict[str, Any],
205
+ attempt_id: str | None = None,
206
+ claim_epoch: int | None = None,
207
+ session_secret: str | None = None,
208
+ pinned: bool = False,
209
+ ) -> dict[str, Any]:
210
+ """Emit an attempt-scoped event (requires AttemptCredential in body).
211
+
212
+ attempt_id, claim_epoch, session_secret may be passed as kwargs (1C pattern)
213
+ or resolved from the stored credential. If attempt_id is None the call raises
214
+ ValueError — ensure claim_work_item is called first to obtain AttemptCredential.
215
+ """
216
+ eff_attempt_id = attempt_id
217
+ eff_claim_epoch = claim_epoch
218
+ eff_session_secret = session_secret
219
+ # Fall back to stored cred if kwargs not provided
220
+ if (eff_attempt_id is None or eff_claim_epoch is None or eff_session_secret is None) and self._cred is not None:
221
+ eff_attempt_id = eff_attempt_id or self._cred.attempt_id
222
+ eff_claim_epoch = eff_claim_epoch if eff_claim_epoch is not None else self._cred.claim_epoch
223
+ eff_session_secret = eff_session_secret or self._cred.session_secret
224
+ if eff_attempt_id is None:
225
+ raise ValueError(
226
+ "emit_event requires attempt_id; no credential set. "
227
+ "Call claim_work_item first to obtain AttemptCredential."
228
+ )
229
+ req = EmitEventRequest(
230
+ attempt_id=eff_attempt_id,
231
+ claim_epoch=eff_claim_epoch,
232
+ session_secret=eff_session_secret,
233
+ work_item_id=work_item_id,
234
+ event_type=event_type,
235
+ payload=payload,
236
+ pinned=pinned,
237
+ )
238
+ resp = await self._c.emit_event(req)
239
+ return {"event_id": resp.event_id}
240
+
241
+ # ------------------------------------------------------------------
242
+ # Attempts
243
+ # ------------------------------------------------------------------
244
+
245
+ async def complete_attempt(
246
+ self,
247
+ *,
248
+ attempt_id: str,
249
+ claim_epoch: int,
250
+ session_secret: str,
251
+ status: str,
252
+ ) -> dict[str, Any]:
253
+ req = CompleteAttemptRequest(
254
+ attempt_id=attempt_id,
255
+ claim_epoch=claim_epoch,
256
+ session_secret=session_secret,
257
+ status=status,
258
+ )
259
+ resp = await self._c.complete_attempt(attempt_id, req)
260
+ return {"ok": resp.ok}
261
+
262
+ async def pause_attempt(
263
+ self, *, attempt_id: str, claim_epoch: int,
264
+ session_secret: str, reason: str,
265
+ ) -> dict[str, Any]:
266
+ req = PauseAttemptRequest(
267
+ attempt_id=attempt_id,
268
+ claim_epoch=claim_epoch,
269
+ session_secret=session_secret,
270
+ reason=reason,
271
+ )
272
+ resp = await self._c.pause_attempt(attempt_id, req)
273
+ return {"ok": resp.ok}
274
+
275
+ async def renew_lease(
276
+ self, *, attempt_id: str, claim_epoch: int, session_secret: str
277
+ ) -> dict[str, Any]:
278
+ resp = await self._c.renew_lease(
279
+ attempt_id, RenewLeaseRequest(claim_epoch=claim_epoch, session_secret=session_secret)
280
+ )
281
+ lu = resp.lease_until
282
+ return {"lease_until": lu if isinstance(lu, str) else str(lu)}
283
+
284
+ async def whoami(self) -> dict[str, Any]:
285
+ """Caller identity for admin-gated scenarios (release etc.)."""
286
+ resp = await self._c.whoami()
287
+ return resp.model_dump()
288
+
289
+ async def reconcile_artifacts(self, *, work_item_id: str) -> list[dict[str, Any]]:
290
+ """Fail-loud: composite tools must call reconcile directly, not via this adapter.
291
+
292
+ design §10.1 (B6): composite tools should call
293
+ polyforge_v3_coding.artifacts.reconcile directly (async). This adapter has
294
+ no implementation by design — calling it indicates a caller bug.
295
+ """
296
+ raise NotImplementedError(
297
+ "composite tools should call polyforge_v3_coding.artifacts.reconcile "
298
+ "directly (async); adapter has no impl by design"
299
+ )
300
+
301
+
302
+ __all__ = ["AihubClientProtocolAdapter"]