muse-code-sdk 1.4.11__tar.gz → 1.4.13__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 (71) hide show
  1. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/PKG-INFO +46 -3
  2. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/README.md +44 -2
  3. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/pyproject.toml +9 -8
  4. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/__init__.py +2 -2
  5. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/approval.py +5 -0
  6. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code_sdk.egg-info/PKG-INFO +46 -3
  7. muse_code_sdk-1.4.13/src/muse_code_sdk.egg-info/requires.txt +2 -0
  8. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_approval.py +47 -0
  9. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_manifests.py +7 -3
  10. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_publish_shape.py +84 -14
  11. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_recovery_observation.py +12 -0
  12. muse_code_sdk-1.4.11/src/muse_code_sdk.egg-info/requires.txt +0 -1
  13. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/LICENSE +0 -0
  14. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/setup.cfg +0 -0
  15. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/connection/__init__.py +0 -0
  16. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/connection/connection.py +0 -0
  17. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/connection/discovery.py +0 -0
  18. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/connection/spawn.py +0 -0
  19. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/connection/validation.py +0 -0
  20. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/errors.py +0 -0
  21. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/__init__.py +0 -0
  22. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/client.py +0 -0
  23. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/discarded.py +0 -0
  24. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/gap_fill.py +0 -0
  25. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/host_death.py +0 -0
  26. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/host_start.py +0 -0
  27. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/session.py +0 -0
  28. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/turn_handle.py +0 -0
  29. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/facade/turn_submit.py +0 -0
  30. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/fold/__init__.py +0 -0
  31. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/fold/item_store.py +0 -0
  32. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/fold/session_fold.py +0 -0
  33. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/fold/state_store.py +0 -0
  34. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/pending/__init__.py +0 -0
  35. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/pending/pending_command_set.py +0 -0
  36. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/py.typed +0 -0
  37. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/replay.py +0 -0
  38. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code/sync_facade.py +0 -0
  39. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code_sdk.egg-info/SOURCES.txt +0 -0
  40. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code_sdk.egg-info/dependency_links.txt +0 -0
  41. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/src/muse_code_sdk.egg-info/top_level.txt +0 -0
  42. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_asyncio_cancellation.py +0 -0
  43. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_compatibility_row.py +0 -0
  44. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_connection.py +0 -0
  45. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_connection_error_arms.py +0 -0
  46. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_docs_generation.py +0 -0
  47. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_external_audience.py +0 -0
  48. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_approval_resend.py +0 -0
  49. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_gap_fill.py +0 -0
  50. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_open_same_chunk.py +0 -0
  51. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_persistent_approval.py +0 -0
  52. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_required_selection.py +0 -0
  53. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_turn.py +0 -0
  54. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_facade_user_input_resend.py +0 -0
  55. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_host_discovery.py +0 -0
  56. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_item_store.py +0 -0
  57. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_muse_serve_child.py +0 -0
  58. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_pending_command_corpus.py +0 -0
  59. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_pending_command_set.py +0 -0
  60. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_pending_command_wire_binding.py +0 -0
  61. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_permission_state_wire_types.py +0 -0
  62. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_public_mirror_text.py +0 -0
  63. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_session_fold.py +0 -0
  64. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_source_closure.py +0 -0
  65. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_spawn_handshake.py +0 -0
  66. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_spawn_host_lost.py +0 -0
  67. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_state_store.py +0 -0
  68. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_sync_facade.py +0 -0
  69. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_tolerance_corpus.py +0 -0
  70. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_transcript_replay.py +0 -0
  71. {muse_code_sdk-1.4.11 → muse_code_sdk-1.4.13}/tests/test_typing_gates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: muse-code-sdk
3
- Version: 1.4.11
3
+ Version: 1.4.13
4
4
  Summary: Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/meta-models/muse-code-sdk
@@ -15,6 +15,7 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: pydantic==2.13.5
18
+ Requires-Dist: muse-code-msp==1.4.13
18
19
  Dynamic: license-file
19
20
 
20
21
  # `muse-code-sdk`
@@ -42,13 +43,14 @@ Status: developer preview. Experimental protocol surfaces may change.
42
43
 
43
44
  The generated MSP wire types live in the companion package
44
45
  [`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
45
- restated here. One runtime dependency: `pydantic`, exact-pinned.
46
+ restated here; it is installed with this package, pinned to the same version.
47
+ One further runtime dependency: `pydantic`, exact-pinned.
46
48
  Python 3.10+. Fully typed (`py.typed`).
47
49
 
48
50
  ## Install
49
51
 
50
52
  ```sh
51
- pip install muse-code-sdk muse-code-msp
53
+ pip install muse-code-sdk
52
54
  ```
53
55
 
54
56
  You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
@@ -82,6 +84,47 @@ async def main() -> None:
82
84
  asyncio.run(main())
83
85
  ```
84
86
 
87
+ To run a session and the child agents it starts without approval prompts,
88
+ start it under the built-in `:unrestricted` permission profile. Current
89
+ releases admit every child of such a session without prompts; a child admitted
90
+ with a narrower authority would stay gated at it.
91
+
92
+ That profile is described, verbatim, as "No filesystem sandbox or approval
93
+ prompts; local commands have direct network access; project trust is
94
+ unchanged." It also sets the approval reviewer to none. Use it only for a
95
+ trusted workspace and trusted prompts. Never use it where the prompts or the
96
+ sandbox are what protects you.
97
+
98
+ Start it with that profile, and pass the same profile when you resume it.
99
+ Paste inside `main()` above, indented, before `await client.close()`, with the same client:
100
+
101
+ ```python
102
+ from muse_code import ResumeSessionOptions, StartSessionOptions
103
+
104
+ session = await client.start_session(StartSessionOptions(
105
+ workspace_root="/path/to/repo", # an absolute path
106
+ permission_profile=":unrestricted",
107
+ ))
108
+ saved_session_id = session.session_id
109
+ # ... later, in the same client:
110
+ session = await client.resume_session(ResumeSessionOptions(
111
+ session_id=saved_session_id,
112
+ permission_profile=":unrestricted",
113
+ ))
114
+ ```
115
+
116
+ On start, `require_permission_profile=True` makes the client prove that the
117
+ pick was applied, and raise `PermissionProfileNotApplied` if it cannot (a
118
+ failed `session/read` read-back raises its own error). A host
119
+ that does not report the committed selection fails that proof even when the
120
+ pick was applied (seen with Muse Code 1.4.12). Resume has no
121
+ `require_permission_profile`, so the client never verifies the pick there.
122
+
123
+ `approval_mode` alone is not enough: it changes the session's own approval
124
+ mode, but its child agents are capped by the session's permission profile.
125
+ Under a prompting profile (such as `:ask-me`, whose approval is `on_request`),
126
+ the children still ask.
127
+
85
128
  ## Documentation
86
129
 
87
130
  - Guides, cookbook recipes, and the protocol reference:
@@ -23,13 +23,14 @@ Status: developer preview. Experimental protocol surfaces may change.
23
23
 
24
24
  The generated MSP wire types live in the companion package
25
25
  [`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
26
- restated here. One runtime dependency: `pydantic`, exact-pinned.
26
+ restated here; it is installed with this package, pinned to the same version.
27
+ One further runtime dependency: `pydantic`, exact-pinned.
27
28
  Python 3.10+. Fully typed (`py.typed`).
28
29
 
29
30
  ## Install
30
31
 
31
32
  ```sh
32
- pip install muse-code-sdk muse-code-msp
33
+ pip install muse-code-sdk
33
34
  ```
34
35
 
35
36
  You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
@@ -63,6 +64,47 @@ async def main() -> None:
63
64
  asyncio.run(main())
64
65
  ```
65
66
 
67
+ To run a session and the child agents it starts without approval prompts,
68
+ start it under the built-in `:unrestricted` permission profile. Current
69
+ releases admit every child of such a session without prompts; a child admitted
70
+ with a narrower authority would stay gated at it.
71
+
72
+ That profile is described, verbatim, as "No filesystem sandbox or approval
73
+ prompts; local commands have direct network access; project trust is
74
+ unchanged." It also sets the approval reviewer to none. Use it only for a
75
+ trusted workspace and trusted prompts. Never use it where the prompts or the
76
+ sandbox are what protects you.
77
+
78
+ Start it with that profile, and pass the same profile when you resume it.
79
+ Paste inside `main()` above, indented, before `await client.close()`, with the same client:
80
+
81
+ ```python
82
+ from muse_code import ResumeSessionOptions, StartSessionOptions
83
+
84
+ session = await client.start_session(StartSessionOptions(
85
+ workspace_root="/path/to/repo", # an absolute path
86
+ permission_profile=":unrestricted",
87
+ ))
88
+ saved_session_id = session.session_id
89
+ # ... later, in the same client:
90
+ session = await client.resume_session(ResumeSessionOptions(
91
+ session_id=saved_session_id,
92
+ permission_profile=":unrestricted",
93
+ ))
94
+ ```
95
+
96
+ On start, `require_permission_profile=True` makes the client prove that the
97
+ pick was applied, and raise `PermissionProfileNotApplied` if it cannot (a
98
+ failed `session/read` read-back raises its own error). A host
99
+ that does not report the committed selection fails that proof even when the
100
+ pick was applied (seen with Muse Code 1.4.12). Resume has no
101
+ `require_permission_profile`, so the client never verifies the pick there.
102
+
103
+ `approval_mode` alone is not enough: it changes the session's own approval
104
+ mode, but its child agents are capped by the session's permission profile.
105
+ Under a prompting profile (such as `:ask-me`, whose approval is `on_request`),
106
+ the children still ask.
107
+
66
108
  ## Documentation
67
109
 
68
110
  - Guides, cookbook recipes, and the protocol reference:
@@ -5,19 +5,20 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "muse-code-sdk"
8
- version = "1.4.11"
8
+ version = "1.4.13"
9
9
  description = "Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
12
12
  license = "MIT"
13
13
  license-files = ["LICENSE"]
14
- # Exactly ONE runtime dependency, exact-pinned: a deliberate, ruled
15
- # supply-chain exception. Any second dependency is an owner escalation;
16
- # bumps are deliberate reviewed PRs.
17
- # muse-code-msp is deliberately NOT declared: it is not on the registry yet,
18
- # and promoting it is the owner escalation decided with the first publish
19
- # (the @muse-code/msp devDependency precedent; test_publish_shape pins this).
20
- dependencies = ["pydantic==2.13.5"]
14
+ # Two runtime dependencies, both exact-pinned. pydantic is the deliberate,
15
+ # ruled supply-chain exception; bumps are deliberate reviewed PRs.
16
+ # muse-code-msp is the companion wire-types package generated and released
17
+ # from the same tree, so its pin is this package's own version: a plain
18
+ # `pip install muse-code-sdk` must import, and the release bump moves this
19
+ # package's version and its muse-code-msp pin together; pydantic bumps stay
20
+ # reviewed PRs (test_publish_shape pins the pairing).
21
+ dependencies = ["pydantic==2.13.5", "muse-code-msp==1.4.13"]
21
22
  classifiers = [
22
23
  # Alpha states the withheld promise: publication is permitted, but the
23
24
  # stability promise and conformance claim stay withheld — experimental
@@ -80,7 +80,7 @@ __all__ = [
80
80
  ]
81
81
 
82
82
  EXPECTED_SCHEMA_FINGERPRINT: Final[str] = (
83
- "sha256:f282ad16e21520d0cda68cb190149b86e2b062eef1dd282749360c05aa3d1082"
83
+ "sha256:7fddaf646dbc317e5fcac0785e135eb04df14fbb0db1863089dfd6da21730e5b"
84
84
  )
85
85
  """The stable schema-bundle fingerprint this SDK was built against.
86
86
 
@@ -92,7 +92,7 @@ Python SDK reds this lane instead of drifting silently.
92
92
  # The lockstep rule: the release train bumps pyproject.toml, the
93
93
  # distribution-metadata authority; this constant is aligned at publication
94
94
  # time.
95
- __version__: Final[str] = "1.4.11"
95
+ __version__: Final[str] = "1.4.13"
96
96
 
97
97
  # The facade is imported last so the constants above are already bound: the
98
98
  # spawn path reads ``EXPECTED_SCHEMA_FINGERPRINT`` (lazily, at initialize
@@ -61,6 +61,7 @@ _REFRESH_FROM_UPDATE = frozenset(
61
61
  _REFRESH_FROM_REQUEST = frozenset(
62
62
  {
63
63
  "approvalId",
64
+ "cuaCardFacts",
64
65
  "itemId",
65
66
  "judgeEscalated",
66
67
  "protectedWrite",
@@ -297,6 +298,10 @@ class ApprovalRouter:
297
298
  # Omitted rather than nulled when absent, like ``feedback`` below.
298
299
  if "subagentOrigin" in params:
299
300
  refreshed["subagentOrigin"] = params["subagentOrigin"]
301
+ # A Computer Use card's facts belong to the call, so they stay with
302
+ # the retained request across stages.
303
+ if "cuaCardFacts" in requested:
304
+ refreshed["cuaCardFacts"] = requested["cuaCardFacts"]
300
305
  acked = self._in_flight.get(requested["approvalId"])
301
306
  if acked is not None:
302
307
  return self._after_ack(acked, refreshed)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: muse-code-sdk
3
- Version: 1.4.11
3
+ Version: 1.4.13
4
4
  Summary: Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/meta-models/muse-code-sdk
@@ -15,6 +15,7 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: pydantic==2.13.5
18
+ Requires-Dist: muse-code-msp==1.4.13
18
19
  Dynamic: license-file
19
20
 
20
21
  # `muse-code-sdk`
@@ -42,13 +43,14 @@ Status: developer preview. Experimental protocol surfaces may change.
42
43
 
43
44
  The generated MSP wire types live in the companion package
44
45
  [`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
45
- restated here. One runtime dependency: `pydantic`, exact-pinned.
46
+ restated here; it is installed with this package, pinned to the same version.
47
+ One further runtime dependency: `pydantic`, exact-pinned.
46
48
  Python 3.10+. Fully typed (`py.typed`).
47
49
 
48
50
  ## Install
49
51
 
50
52
  ```sh
51
- pip install muse-code-sdk muse-code-msp
53
+ pip install muse-code-sdk
52
54
  ```
53
55
 
54
56
  You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
@@ -82,6 +84,47 @@ async def main() -> None:
82
84
  asyncio.run(main())
83
85
  ```
84
86
 
87
+ To run a session and the child agents it starts without approval prompts,
88
+ start it under the built-in `:unrestricted` permission profile. Current
89
+ releases admit every child of such a session without prompts; a child admitted
90
+ with a narrower authority would stay gated at it.
91
+
92
+ That profile is described, verbatim, as "No filesystem sandbox or approval
93
+ prompts; local commands have direct network access; project trust is
94
+ unchanged." It also sets the approval reviewer to none. Use it only for a
95
+ trusted workspace and trusted prompts. Never use it where the prompts or the
96
+ sandbox are what protects you.
97
+
98
+ Start it with that profile, and pass the same profile when you resume it.
99
+ Paste inside `main()` above, indented, before `await client.close()`, with the same client:
100
+
101
+ ```python
102
+ from muse_code import ResumeSessionOptions, StartSessionOptions
103
+
104
+ session = await client.start_session(StartSessionOptions(
105
+ workspace_root="/path/to/repo", # an absolute path
106
+ permission_profile=":unrestricted",
107
+ ))
108
+ saved_session_id = session.session_id
109
+ # ... later, in the same client:
110
+ session = await client.resume_session(ResumeSessionOptions(
111
+ session_id=saved_session_id,
112
+ permission_profile=":unrestricted",
113
+ ))
114
+ ```
115
+
116
+ On start, `require_permission_profile=True` makes the client prove that the
117
+ pick was applied, and raise `PermissionProfileNotApplied` if it cannot (a
118
+ failed `session/read` read-back raises its own error). A host
119
+ that does not report the committed selection fails that proof even when the
120
+ pick was applied (seen with Muse Code 1.4.12). Resume has no
121
+ `require_permission_profile`, so the client never verifies the pick there.
122
+
123
+ `approval_mode` alone is not enough: it changes the session's own approval
124
+ mode, but its child agents are capped by the session's permission profile.
125
+ Under a prompting profile (such as `:ask-me`, whose approval is `on_request`),
126
+ the children still ask.
127
+
85
128
  ## Documentation
86
129
 
87
130
  - Guides, cookbook recipes, and the protocol reference:
@@ -0,0 +1,2 @@
1
+ pydantic==2.13.5
2
+ muse-code-msp==1.4.13
@@ -687,6 +687,53 @@ async def test_an_approval_updated_that_advances_the_requirement_re_asks_the_han
687
687
  assert session.fold.pending_approvals() == []
688
688
 
689
689
 
690
+ CUA_CARD_FACTS: Params = {
691
+ "page": {"host": "shop.example.com", "hostAscii": "shop.example.com"},
692
+ "steps": [
693
+ {
694
+ "step": 1,
695
+ "kind": "verb:buy",
696
+ "action": "click",
697
+ "effect": "may buy or pay with your account",
698
+ "roleWord": "button",
699
+ "label": "Place order",
700
+ }
701
+ ],
702
+ }
703
+
704
+
705
+ @pytest.mark.asyncio
706
+ @pytest.mark.parametrize("facts", [CUA_CARD_FACTS, None])
707
+ async def test_a_stage_advance_keeps_the_requests_cua_card_facts(facts: Any) -> None:
708
+ # `cuaCardFacts` belongs to the call and `approval/updated` doesn't carry
709
+ # it, so the refreshed stage-2 request keeps the original request's facts,
710
+ # and a request without facts stays without the key.
711
+ transport, session, minted = wired()
712
+ seen: List[Params] = []
713
+
714
+ def handler(request: Any) -> ApprovalDecisionInput:
715
+ seen.append(dict(request))
716
+ return ApprovalDecisionInput(choice_id="allow_once")
717
+
718
+ session.on_approval(handler)
719
+ overrides: Params = {} if facts is None else {"cuaCardFacts": facts}
720
+ first = session.apply(approval_requested(**overrides))
721
+ await wait_for_writes(transport, 1)
722
+ answer(transport, 0, {**decide_result("mint-0"), "terminal": False})
723
+ await settled_io(first.io)
724
+
725
+ updated = session.apply(stage_resolved(2, CHOICES))
726
+ await wait_for_writes(transport, 2)
727
+ assert minted == ["mint-0", "mint-1"]
728
+ assert len(seen) == 2
729
+ if facts is None:
730
+ assert "cuaCardFacts" not in seen[1]
731
+ else:
732
+ assert seen[1]["cuaCardFacts"] == facts
733
+ answer(transport, 1, decide_result("mint-1"))
734
+ await settled_io(updated.io)
735
+
736
+
690
737
  @pytest.mark.asyncio
691
738
  async def test_an_approval_updated_naming_the_decided_stage_asks_nothing() -> None:
692
739
  # The per-stage latch: an update that still names the stage this client
@@ -3,7 +3,7 @@
3
3
  The governing rule: the two package manifests carry the chartered shape
4
4
  (names, floor, wheel purity, license, ``py.typed``), the dependency posture
5
5
  holds (zero runtime deps in ``muse_code_msp``; exactly one exact-pinned
6
- pydantic in ``muse_code``; every dev tool exact-pinned in the committed
6
+ pydantic plus ``muse-code-msp`` at the facade's own version in ``muse_code``; every dev tool exact-pinned in the committed
7
7
  lock), and the schema-fingerprint pin binds to the stable bundle manifest so
8
8
  a schema advance that forgets the Python SDK reds this lane.
9
9
  """
@@ -66,13 +66,17 @@ def test_facade_package_manifest_shape() -> None:
66
66
  assert project["name"] == "muse-code-sdk"
67
67
  assert project["requires-python"] == ">=3.10"
68
68
  deps = project.get("dependencies", [])
69
- assert len(deps) == 1, (
69
+ assert len(deps) == 2, (
70
70
  f"muse_code declares {len(deps)} runtime dependencies; the ruled "
71
- "grant is exactly one (pydantic, exact pin)"
71
+ "grant is exactly two: pydantic (exact pin) and the companion "
72
+ "wire-types package at the facade's own version"
72
73
  )
73
74
  assert re.fullmatch(r"pydantic==\d+(\.\d+)+", deps[0]), (
74
75
  f"the pydantic dependency must be an exact pin, got: {deps[0]!r}"
75
76
  )
77
+ assert deps[1] == f"muse-code-msp=={project['version']}", (
78
+ f"the wire-types dependency must be pinned to the facade's own version, got: {deps[1]!r}"
79
+ )
76
80
  assert "license" in project
77
81
  assert (SDK_DIR / "src" / "muse_code" / "py.typed").is_file()
78
82
 
@@ -16,6 +16,7 @@ regressed package.
16
16
  from __future__ import annotations
17
17
 
18
18
  import json
19
+ import os
19
20
  import re
20
21
  import subprocess
21
22
  import sys
@@ -181,28 +182,39 @@ def test_the_classifiers_state_the_posture() -> None:
181
182
  )
182
183
 
183
184
 
184
- def test_the_wire_types_stay_out_of_the_facade_dependencies() -> None:
185
- # The rule's letter: exactly one runtime dependency (the pydantic pin);
186
- # any second, for either package, is an owner escalation. muse-code-msp is
187
- # therefore deliberately NOT declared, the @muse-code/msp devDependency
188
- # precedent: it does not exist on the registry, and declaring it before it
189
- # does would turn a working editable install into a hard `pip install`
190
- # failure. The gate script's printed owner-run sequence publishes
191
- # muse-code-msp FIRST; promoting it into dependencies at that point is the
192
- # owner escalation the governing rule names, decided with the publish itself.
193
- deps = _pyproject(SDK_DIR)["project"].get("dependencies", [])
194
- assert all(not d.startswith("muse-code-msp") for d in deps)
185
+ def test_the_wire_types_are_a_same_version_pin_of_the_facade() -> None:
186
+ # The facade imports the generated wire types at import time, so a plain
187
+ # `pip install muse-code-sdk` must bring them along: muse-code-msp is the
188
+ # facade's second runtime dependency, exact-pinned to the facade's OWN
189
+ # version (both packages are versioned and released from the same tree,
190
+ # wire types first). The release bump rewrites the facade's version and
191
+ # its muse-code-msp pin together (pydantic bumps stay reviewed PRs); a
192
+ # drift between them, or a range, is a packaging defect. The wire-types
193
+ # package itself still declares no runtime dependency.
194
+ sdk = _pyproject(SDK_DIR)["project"]
195
+ deps = sdk.get("dependencies", [])
196
+ msp_pins = [d for d in deps if d.startswith("muse-code-msp")]
197
+ assert msp_pins == [f"muse-code-msp=={sdk['version']}"], (
198
+ f"muse-code-msp must be pinned to the facade's own version {sdk['version']!r}, got {msp_pins!r}"
199
+ )
200
+ assert sdk["version"] == _pyproject(MSP_DIR)["project"]["version"]
201
+ assert [d for d in deps if not d.startswith("muse-code-msp")] == [
202
+ d for d in deps if d.startswith("pydantic==")
203
+ ], "besides the wire types, pydantic (exact pin) is the only runtime dependency"
195
204
  assert _pyproject(MSP_DIR)["project"].get("dependencies", []) == []
196
205
 
197
206
 
198
207
  def _run_gate(
199
- *args: str, env: dict[str, str] | None = None
208
+ *args: str, env: dict[str, str] | None = None, script: Path = GATE_SCRIPT
200
209
  ) -> subprocess.CompletedProcess[str]:
201
210
  # The env is built from scratch — GITHUB_ACTIONS and the OIDC variables
202
211
  # are absent unless a test passes them — so the refusal arms below see
203
- # exactly the context they assert, wherever the suite itself runs.
212
+ # exactly the context they assert, wherever the suite itself runs. Every
213
+ # gate run, the checked-in script or a copied layout's, goes through here,
214
+ # so each carries the same wall (review 5581715933: an uncapped child wait
215
+ # would hang the lane instead of failing a named test).
204
216
  return subprocess.run(
205
- [str(GATE_SCRIPT), *args],
217
+ [str(script), *args],
206
218
  capture_output=True,
207
219
  text=True,
208
220
  timeout=GATE_WALL_SECONDS,
@@ -361,6 +373,64 @@ def test_the_publish_path_cannot_ship_a_wheel_without_its_row(tmp_path: Path) ->
361
373
  assert "do not match the derived rows" in result.stderr
362
374
 
363
375
 
376
+ def _copied_gate_layout(tmp_path: Path) -> Path:
377
+ # The gate reads its repo root from its own location, so a copy of the
378
+ # script plus exactly the files its manifest-identity gates read makes a
379
+ # seedable tree; build-only mode notes a missing anchor and tolerates a
380
+ # non-git tree, so the run reaches the dependency-posture gate.
381
+ for source in (
382
+ "scripts/publish-sdk-pypi.sh",
383
+ "scripts/check-public-mirror-text.py",
384
+ "clients/msp-py/pyproject.toml",
385
+ "clients/msp-py/LICENSE",
386
+ "clients/msp-py/README.md",
387
+ "clients/sdk-py/pyproject.toml",
388
+ "clients/sdk-py/LICENSE",
389
+ "clients/sdk-py/README.md",
390
+ "schema/msp/stable/manifest.json",
391
+ ):
392
+ target = tmp_path / source
393
+ target.parent.mkdir(parents=True, exist_ok=True)
394
+ target.write_bytes((PROJECT_ROOT / source).read_bytes())
395
+ script = tmp_path / "scripts" / "publish-sdk-pypi.sh"
396
+ script.chmod(0o755)
397
+ return script
398
+
399
+
400
+ def _run_copied_gate(script: Path) -> subprocess.CompletedProcess[str]:
401
+ return _run_gate("--build-only", env={"HOME": os.environ.get("HOME", "/tmp")}, script=script)
402
+
403
+
404
+ def test_the_dependency_gate_refuses_a_drifted_or_missing_wire_types_pin(tmp_path: Path) -> None:
405
+ # The same-version rule: the wire-types pin must equal the facade's own
406
+ # version. The committed tree can never carry a drift (the pairing test
407
+ # above), so each fault is seeded into a copied layout and the gate must
408
+ # die naming the facade's own version, not pass to the build.
409
+ manifest = PROJECT_ROOT / "clients" / "sdk-py" / "pyproject.toml"
410
+ own = _pyproject(SDK_DIR)["project"]["version"]
411
+ for label, seeded in (
412
+ ("drifted", manifest.read_text().replace('"muse-code-msp==', '"muse-code-msp==1.0.0-never-', 1)),
413
+ ("missing", re.sub(r',\s*"muse-code-msp==[^"]+"', "", manifest.read_text(), count=1)),
414
+ # A valid-shape pin at the WRONG version: the shape regex accepts it, only the ==own-version half refuses
415
+ # it (review 5580868376 P1: without this seed a gate trimmed to the shape check stays green).
416
+ ("stale", manifest.read_text().replace(f'"muse-code-msp=={own}"', '"muse-code-msp==0.0.1"', 1)),
417
+ ):
418
+ root = tmp_path / label
419
+ script = _copied_gate_layout(root)
420
+ copy = root / "clients" / "sdk-py" / "pyproject.toml"
421
+ assert seeded != manifest.read_text(), f"{label}: the fault must change the manifest"
422
+ copy.write_text(seeded)
423
+ result = _run_copied_gate(script)
424
+ assert result.returncode != 0, f"{label}: the gate must refuse:\n{result.stdout}\n{result.stderr}"
425
+ assert "the facade's own version" in (result.stdout + result.stderr), (
426
+ f"{label}: the refusal must name the same-version rule:\n{result.stdout}\n{result.stderr}"
427
+ )
428
+ if label == "stale":
429
+ assert "muse-code-msp==0.0.1" in (result.stdout + result.stderr), (
430
+ f"{label}: the refusal must quote the stale pin it found:\n{result.stdout}\n{result.stderr}"
431
+ )
432
+
433
+
364
434
  def test_a_pre_release_version_refuses_the_row_derivation(tmp_path: Path) -> None:
365
435
  # The plain-X.Y.Z refusal, proven under its fault: setuptools would
366
436
  # normalize a PEP 440 suffix in the built wheel's filename, so the
@@ -151,6 +151,18 @@ async def test_absent_malformed_or_unbound_observation_is_unknown(diagnose, obse
151
151
  @pytest.mark.asyncio
152
152
  @pytest.mark.parametrize("delta, support, expected", [
153
153
  ({"effectiveMode": "on", "effectiveSource": "request", "qualification": "qualified"}, "recognized", "qualified"),
154
+ pytest.param(
155
+ {"effectiveMode": "on", "effectiveSource": "retained", "qualification": "qualified"},
156
+ "recognized", "qualified", id="retained-source",
157
+ ),
158
+ pytest.param(
159
+ {"effectiveMode": "off", "effectiveSource": "hostPolicy", "qualification": "notRequired"},
160
+ "recognized", "unknown", id="host-policy-source",
161
+ ),
162
+ pytest.param(
163
+ {"effectiveMode": "on", "effectiveSource": "request", "qualification": "unknown"},
164
+ "recognized", "unknown", id="unknown-qualification",
165
+ ),
154
166
  pytest.param(
155
167
  {"effectiveMode": "on", "effectiveSource": "request", "qualification": "future-value"},
156
168
  "unknown", "unknown", id="future-qualification",
@@ -1 +0,0 @@
1
- pydantic==2.13.5
File without changes
File without changes