muse-code-sdk 1.3.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.
- muse_code/__init__.py +129 -0
- muse_code/connection/__init__.py +49 -0
- muse_code/connection/connection.py +905 -0
- muse_code/connection/discovery.py +55 -0
- muse_code/connection/spawn.py +1067 -0
- muse_code/connection/validation.py +149 -0
- muse_code/errors.py +199 -0
- muse_code/facade/__init__.py +89 -0
- muse_code/facade/approval.py +279 -0
- muse_code/facade/client.py +444 -0
- muse_code/facade/discarded.py +45 -0
- muse_code/facade/gap_fill.py +483 -0
- muse_code/facade/host_death.py +254 -0
- muse_code/facade/session.py +977 -0
- muse_code/facade/turn_handle.py +440 -0
- muse_code/facade/turn_submit.py +289 -0
- muse_code/fold/__init__.py +82 -0
- muse_code/fold/item_store.py +268 -0
- muse_code/fold/session_fold.py +785 -0
- muse_code/fold/state_store.py +141 -0
- muse_code/pending/__init__.py +53 -0
- muse_code/pending/pending_command_set.py +842 -0
- muse_code/py.typed +0 -0
- muse_code/replay.py +144 -0
- muse_code/sync_facade.py +493 -0
- muse_code_sdk-1.3.0.dist-info/METADATA +47 -0
- muse_code_sdk-1.3.0.dist-info/RECORD +30 -0
- muse_code_sdk-1.3.0.dist-info/WHEEL +5 -0
- muse_code_sdk-1.3.0.dist-info/licenses/LICENSE +21 -0
- muse_code_sdk-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""The FR-638-019b approval round trip: inbound ``approval/requested`` → the
|
|
2
|
+
consumer's handler → outbound ``approval/decide`` (spec 638 T031, carrying
|
|
3
|
+
spec 14990 FR-019; tdd SS5.4). Port of ``clients/sdk-ts/src/facade/approval.ts``.
|
|
4
|
+
|
|
5
|
+
Split out of ``session.py`` rather than added to it: the router owns a
|
|
6
|
+
handler, a per-stage latch, and a decision-shaping guard chain that have
|
|
7
|
+
nothing to do with folding a view, and ``Session`` was already the package's
|
|
8
|
+
largest module.
|
|
9
|
+
|
|
10
|
+
SHAPE NOTE, established from the bundle rather than assumed. ``approval/request``
|
|
11
|
+
as a SERVER REQUEST is not enrolled for the view stream this router rides —
|
|
12
|
+
the schema carries ``approval/requested`` as a NOTIFICATION — so the round
|
|
13
|
+
trip is notification-in / command-out, which is exactly the pair FR-019
|
|
14
|
+
names. The server-request form would be a #206 enrollment request
|
|
15
|
+
(INV-638-01), never a local interface.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import inspect
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Awaitable, Callable, Literal, Union
|
|
23
|
+
|
|
24
|
+
from muse_code_msp import ApprovalDecideParams, ApprovalRequestParams
|
|
25
|
+
|
|
26
|
+
from ..connection.connection import Connection
|
|
27
|
+
|
|
28
|
+
# Every ``approval/decide`` member the guard chain below forwards. Composition
|
|
29
|
+
# alone does not stop drift — the chain is hand-listed — so a regenerated
|
|
30
|
+
# member reds THIS import (and thus every test) until it is forwarded, exactly
|
|
31
|
+
# as the ``MuseClient`` option pins do. ``commandId`` is excluded because the
|
|
32
|
+
# connection is the single minter (INV-638-04 carrying INV-013).
|
|
33
|
+
_DECIDE_FORWARDED = frozenset(
|
|
34
|
+
{"approvalId", "choiceId", "requirementId", "sessionId", "feedback"}
|
|
35
|
+
)
|
|
36
|
+
assert _DECIDE_FORWARDED | {"commandId"} == (
|
|
37
|
+
ApprovalDecideParams.__required_keys__ | ApprovalDecideParams.__optional_keys__
|
|
38
|
+
), "approval/decide grew a member — forward it through ApprovalRouter"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ApprovalDecisionInput:
|
|
43
|
+
"""What an approval handler answers with: a choice the SERVER offered, and
|
|
44
|
+
optional feedback (C-638-1 idiomatic surface over the generated params).
|
|
45
|
+
|
|
46
|
+
D-006 is select-never-create, and this TYPE cannot enforce it —
|
|
47
|
+
``choiceId`` is a string on the wire — so the router checks the answer
|
|
48
|
+
against the request's own ``availableChoices`` before the frame is built.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
choice_id: The chosen ``availableChoices`` entry's ``choiceId``.
|
|
52
|
+
feedback: Optional feedback text; omitted from the frame when ``None``
|
|
53
|
+
(SS1.2 — valid only on choices with ``acceptsFeedback``).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
choice_id: str
|
|
57
|
+
feedback: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ApprovalHandler = Callable[
|
|
61
|
+
[ApprovalRequestParams],
|
|
62
|
+
"ApprovalDecisionInput | Awaitable[ApprovalDecisionInput]",
|
|
63
|
+
]
|
|
64
|
+
"""FR-638-019b: the consumer's decision callback. May be sync or async."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class UnofferedChoice:
|
|
69
|
+
"""The handler picked a ``choiceId`` the request never offered (D-006)."""
|
|
70
|
+
|
|
71
|
+
approval_id: str
|
|
72
|
+
choice_id: str
|
|
73
|
+
available_choice_ids: tuple[str, ...]
|
|
74
|
+
kind: Literal["unofferedChoice"] = "unofferedChoice"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class HandlerRaised:
|
|
79
|
+
"""The consumer's decision handler raised; nothing reached the wire."""
|
|
80
|
+
|
|
81
|
+
approval_id: str
|
|
82
|
+
error: BaseException
|
|
83
|
+
kind: Literal["handlerThrew"] = "handlerThrew"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class SubmitFailed:
|
|
88
|
+
"""The ``approval/decide`` round trip itself failed (or there was no
|
|
89
|
+
connection to author it on)."""
|
|
90
|
+
|
|
91
|
+
approval_id: str
|
|
92
|
+
error: BaseException
|
|
93
|
+
kind: Literal["submitFailed"] = "submitFailed"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
ApprovalFailure = Union[UnofferedChoice, HandlerRaised, SubmitFailed]
|
|
97
|
+
"""Why one approval round trip did not complete.
|
|
98
|
+
|
|
99
|
+
REPORTED, not raised: the trip is driven from a notification, so an exception
|
|
100
|
+
would escape into the consumer's pump with nothing to catch it — the same
|
|
101
|
+
hazard ``Session.apply`` already refuses a malformed frame for. Each arm names
|
|
102
|
+
a DIFFERENT owner (the consumer's handler, the consumer's choice, the host),
|
|
103
|
+
because the repair differs.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
ApprovalFailureHandler = Callable[[ApprovalFailure], None]
|
|
107
|
+
"""Receives each :data:`ApprovalFailure`; its ``kind`` names whose repair it is."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ApprovalRouter:
|
|
111
|
+
"""Routes folded ``approval/requested`` frames to the consumer's handler
|
|
112
|
+
and authors the ``approval/decide`` command (spec 638 FR-638-019b)."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, session_id: str, connection: Connection | None) -> None:
|
|
115
|
+
"""Builds the router; ``connection`` is absent on a fold-only session.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
session_id: The owning session's id, echoed into every decide.
|
|
119
|
+
connection: The wire seam the decide command authors through.
|
|
120
|
+
"""
|
|
121
|
+
self._session_id = session_id
|
|
122
|
+
self._connection = connection
|
|
123
|
+
self._handler: ApprovalHandler | None = None
|
|
124
|
+
self._on_failure: ApprovalFailureHandler | None = None
|
|
125
|
+
# Approval STAGES already decided, keyed by ``approvalId`` +
|
|
126
|
+
# ``requirementId``. Not ``approvalId`` alone: ``requirementId`` is
|
|
127
|
+
# SS5.4's multi-stage race guard, so a stage-2 request must get its own
|
|
128
|
+
# decision or the approval pends forever. Not per-request either: a
|
|
129
|
+
# redelivered ``approval/requested`` for the SAME stage must author no
|
|
130
|
+
# second decide, or a two-stage approval races itself. The key is kept
|
|
131
|
+
# on FAILURE too — a genuinely advancing stage brings a new
|
|
132
|
+
# ``requirementId`` and therefore a new key, so "ask the consumer once
|
|
133
|
+
# per stage" holds absolutely.
|
|
134
|
+
self._decided_stages: set[str] = set()
|
|
135
|
+
|
|
136
|
+
def on_approval(self, handler: ApprovalHandler) -> None:
|
|
137
|
+
"""Register the decision callback (FR-638-019b).
|
|
138
|
+
|
|
139
|
+
REPLACES any previous handler rather than adding to a list: two
|
|
140
|
+
handlers would race to decide one approval and only one decision can
|
|
141
|
+
win, so the loser's would be silently discarded.
|
|
142
|
+
"""
|
|
143
|
+
self._handler = handler
|
|
144
|
+
|
|
145
|
+
def on_approval_error(self, handler: ApprovalFailureHandler) -> None:
|
|
146
|
+
"""Observe round trips that did not complete. See :data:`ApprovalFailure`."""
|
|
147
|
+
self._on_failure = handler
|
|
148
|
+
|
|
149
|
+
def requested(self, params: ApprovalRequestParams) -> Awaitable[None] | None:
|
|
150
|
+
"""An ``approval/requested`` folded. Returns the decision round trip to
|
|
151
|
+
await, or ``None`` when this call schedules nothing — no handler, an
|
|
152
|
+
already-decided stage, or a fold-only session (whose ``SubmitFailed``
|
|
153
|
+
is reported synchronously before returning).
|
|
154
|
+
|
|
155
|
+
No handler at all is the supported default posture, NOT a degraded one
|
|
156
|
+
(D-008): the client runs under the server's own default-deny, and this
|
|
157
|
+
SDK parks nothing — no queued decision, no synthesized denial, no
|
|
158
|
+
local hold.
|
|
159
|
+
"""
|
|
160
|
+
handler = self._handler
|
|
161
|
+
if handler is None:
|
|
162
|
+
return None
|
|
163
|
+
stage = self._stage_key(params)
|
|
164
|
+
if stage in self._decided_stages:
|
|
165
|
+
return None
|
|
166
|
+
# Latched BEFORE anything can yield: a redelivery folded on the very
|
|
167
|
+
# next frame must find the stage already claimed, and an await boundary
|
|
168
|
+
# here would let both pass the check.
|
|
169
|
+
self._decided_stages.add(stage)
|
|
170
|
+
connection = self._connection
|
|
171
|
+
# Checked HERE, synchronously, not inside the coroutine: a fold-only
|
|
172
|
+
# session may apply events with no running loop at all, and a returned
|
|
173
|
+
# coroutine would force ``Session.apply`` to schedule one just to
|
|
174
|
+
# deliver this report (PR #32249 review round 1). Before the handler
|
|
175
|
+
# runs, deliberately — asking a consumer to decide and then dropping
|
|
176
|
+
# the answer is worse than not asking.
|
|
177
|
+
if connection is None:
|
|
178
|
+
# A plain ``RuntimeError``: constructing fold-only and then
|
|
179
|
+
# registering a decide handler is API misuse, not a protocol STATE
|
|
180
|
+
# a typed error names.
|
|
181
|
+
self._report(
|
|
182
|
+
SubmitFailed(
|
|
183
|
+
approval_id=params["approvalId"],
|
|
184
|
+
error=RuntimeError(
|
|
185
|
+
"approval/decide needs a connection; this Session was "
|
|
186
|
+
"constructed fold-only"
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return None
|
|
191
|
+
return self._decide(connection, params, handler)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _stage_key(params: ApprovalRequestParams) -> str:
|
|
195
|
+
requirement = params["currentRequirementId"]
|
|
196
|
+
return (
|
|
197
|
+
f"{params['approvalId']} "
|
|
198
|
+
f"{requirement['approvalId']}:{requirement['sourceIndex']}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def _decide(
|
|
202
|
+
self,
|
|
203
|
+
connection: Connection,
|
|
204
|
+
params: ApprovalRequestParams,
|
|
205
|
+
handler: ApprovalHandler,
|
|
206
|
+
) -> None:
|
|
207
|
+
try:
|
|
208
|
+
answered = handler(params)
|
|
209
|
+
decision = (
|
|
210
|
+
await answered if inspect.isawaitable(answered) else answered
|
|
211
|
+
)
|
|
212
|
+
# The reads are INSIDE the try: a malformed return (a handler that
|
|
213
|
+
# forgot its ``return``, a wrong type) is the consumer's callback
|
|
214
|
+
# misbehaving, exactly like a raise — reported as ``handlerThrew``,
|
|
215
|
+
# never escaped into the never-rejecting ``io`` (PR #32249 review
|
|
216
|
+
# round 1).
|
|
217
|
+
choice_id = decision.choice_id
|
|
218
|
+
feedback = decision.feedback
|
|
219
|
+
except Exception as error: # noqa: BLE001 - the consumer's callback
|
|
220
|
+
self._report(HandlerRaised(approval_id=params["approvalId"], error=error))
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
available_choice_ids = tuple(
|
|
224
|
+
choice["choiceId"] for choice in params["availableChoices"]
|
|
225
|
+
)
|
|
226
|
+
if choice_id not in available_choice_ids:
|
|
227
|
+
# D-006 select-never-create, enforced on THIS side of the
|
|
228
|
+
# transport. An invented choice bounces -32052 a round trip later,
|
|
229
|
+
# by which time the stage may have advanced — so the diagnosis a
|
|
230
|
+
# consumer would actually see is a stale-requirement error about a
|
|
231
|
+
# frame it should never have sent.
|
|
232
|
+
self._report(
|
|
233
|
+
UnofferedChoice(
|
|
234
|
+
approval_id=params["approvalId"],
|
|
235
|
+
choice_id=choice_id,
|
|
236
|
+
available_choice_ids=available_choice_ids,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
decide_params: dict[str, Any] = {
|
|
242
|
+
"approvalId": params["approvalId"],
|
|
243
|
+
"choiceId": choice_id,
|
|
244
|
+
# Echoed from the request, never remembered: SS5.4 makes a stale
|
|
245
|
+
# value a -32053, and the request in hand is the only current
|
|
246
|
+
# statement of it.
|
|
247
|
+
"requirementId": params["currentRequirementId"],
|
|
248
|
+
"sessionId": self._session_id,
|
|
249
|
+
}
|
|
250
|
+
# Omitted rather than nulled (SS1.2): ``feedback`` is valid only on
|
|
251
|
+
# choices with ``acceptsFeedback``, so an explicit null would be
|
|
252
|
+
# rejected by the host.
|
|
253
|
+
if feedback is not None:
|
|
254
|
+
decide_params["feedback"] = feedback
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
# No explicit ``command_id``: unlike ``send_user_turn``, nothing
|
|
258
|
+
# here needs the id before the ack, so ``Connection.command()``
|
|
259
|
+
# mints and stamps it — and its own SS3.1.1 retry then reuses that
|
|
260
|
+
# id for free.
|
|
261
|
+
await connection.command("approval/decide", decide_params)
|
|
262
|
+
except Exception as error: # noqa: BLE001 - reported, never escapes
|
|
263
|
+
self._report(SubmitFailed(approval_id=params["approvalId"], error=error))
|
|
264
|
+
|
|
265
|
+
def _report(self, failure: ApprovalFailure) -> None:
|
|
266
|
+
handler = self._on_failure
|
|
267
|
+
if handler is None:
|
|
268
|
+
return
|
|
269
|
+
try:
|
|
270
|
+
handler(failure)
|
|
271
|
+
except Exception: # noqa: BLE001, S110
|
|
272
|
+
# The consumer's failure OBSERVER raised. Swallowed, deliberately:
|
|
273
|
+
# an exception escaping here rides ``SessionApplyOutcome.io``,
|
|
274
|
+
# whose contract is "IT NEVER REJECTS" — and since most consumers
|
|
275
|
+
# never await ``io``, it would surface as an escaping failure in
|
|
276
|
+
# the pump, the exact hazard that contract exists to prevent. The
|
|
277
|
+
# failure itself was already handed to the observer; there is no
|
|
278
|
+
# one left to tell.
|
|
279
|
+
pass
|