rocket-engine 0.0.2b1__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 (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,797 @@
1
+ """Shared-engine port bus (Step 7A, ECS-FLOW slices 1-4; Step 7 notify).
2
+
3
+ One shared Engine per process serves many attached ports (stdio
4
+ by default; MCP / loopback-HTTP toggled live). A change through
5
+ any port modifies the world once; the bus re-emits the appended
6
+ ledger event to the other ports over async per-port queues with
7
+ client acks (QoS-1, NATS-JetStream style). Submit/admission stays
8
+ synchronous on the writer's path — only notification fans out
9
+ async, so a slow port never stalls writers.
10
+
11
+ Delivery model (ADR-017): at-least-once with acks; ledger
12
+ position (the event seq) is the packet id, so duplicates
13
+ collapse by position and every port handler applies
14
+ idempotently (position check, then apply) through the shared
15
+ PositionGuard. Slow consumers degrade to restart-and-resync:
16
+ the staged 10/15/20s ladder warns, quarantines (backlog
17
+ dropped), then hands a resync directive to the client, which
18
+ re-runs its idempotent onboarding ritual from the current
19
+ cursor. The engine flags, never kills: quarantine is
20
+ engine-side bookkeeping plus a resync flag, never client
21
+ state. A queue-depth ceiling beside the time threshold bounds
22
+ a port that acks slowly but never trips the ladder.
23
+
24
+ Port toggles are local-only (LOCAL-SINGLE-USER): enabling or
25
+ disabling a port, or swapping the boot profile, requires the
26
+ caller to assert a local caller; remote callers are refused
27
+ loudly instead of rewired.
28
+ """
29
+
30
+ import threading
31
+ import time
32
+
33
+ from .errors import ExecutionError
34
+
35
+ #: Staged no-ack escalation, seconds (ADR-017 D5, tunable per
36
+ #: bus): warn, quarantine (drop backlog), resync directive.
37
+ WARN_AFTER_S = 10.0
38
+ QUARANTINE_AFTER_S = 15.0
39
+ RESYNC_AFTER_S = 20.0
40
+
41
+ #: Boot-time default port profile: stdio alone (ADR-017 D6).
42
+ BOOT_PROFILE = ("stdio",)
43
+
44
+ #: Port states: active delivery, warned past the first rung,
45
+ #: quarantined (backlog dropped, resync flag pending).
46
+ _PORT_STATES = ("active", "warned", "quarantined")
47
+
48
+
49
+ class PositionGuard:
50
+ """Shared position-check-then-apply guard all ports use.
51
+
52
+ QoS-1 redelivers facts; redelivery of one ledger position
53
+ must apply once. Every port handler routes through this one
54
+ helper so per-port hand checks cannot drift.
55
+
56
+ Args:
57
+ None.
58
+ """
59
+
60
+ def __init__(self):
61
+ """Create a guard with no applied positions.
62
+
63
+ Returns:
64
+ Empty guard; every first position applies.
65
+ """
66
+ self._lock = threading.RLock()
67
+ self._applied = {}
68
+
69
+ def applies(self, port, seq):
70
+ """Check whether one ledger position still needs applying.
71
+
72
+ Args:
73
+ port: Port name the position would apply on.
74
+ seq: Ledger position (event seq) to check.
75
+
76
+ Returns:
77
+ True when the position is newer than everything the
78
+ port applied before; False for a duplicate or an
79
+ older position.
80
+
81
+ Raises:
82
+ ExecutionError: invalid_command for a non-integer
83
+ position.
84
+ """
85
+ if (not isinstance(seq, int) or
86
+ isinstance(seq, bool)):
87
+ raise ExecutionError(
88
+ "invalid_command",
89
+ f"Position must be an integer seq, got {seq!r}.",
90
+ )
91
+ with self._lock:
92
+ return (port not in self._applied or
93
+ seq > self._applied[port])
94
+
95
+ def record(self, port, seq):
96
+ """Mark one ledger position applied without calling anything.
97
+
98
+ Args:
99
+ port: Port name the position applied on.
100
+ seq: Ledger position (event seq) now applied.
101
+
102
+ Returns:
103
+ The recorded position.
104
+
105
+ Raises:
106
+ ExecutionError: invalid_command for a non-integer
107
+ position.
108
+ """
109
+ if (not isinstance(seq, int) or
110
+ isinstance(seq, bool)):
111
+ raise ExecutionError(
112
+ "invalid_command",
113
+ f"Position must be an integer seq, got {seq!r}.",
114
+ )
115
+ with self._lock:
116
+ if (port not in self._applied or
117
+ seq > self._applied[port]):
118
+ self._applied[port] = seq
119
+ return self._applied[port]
120
+
121
+ def apply_once(self, port, seq, func, *args, **kwargs):
122
+ """Apply one position through func at most once per port.
123
+
124
+ Args:
125
+ port: Port name the position applies on.
126
+ seq: Ledger position (event seq) to apply.
127
+ func: Apply callable run only when the position is
128
+ new for the port.
129
+ args: Positional arguments forwarded to func.
130
+ kwargs: Keyword arguments forwarded to func.
131
+
132
+ Returns:
133
+ (applied, result) pair: applied True plus the func
134
+ result on first application, (False, None) on a
135
+ duplicate position.
136
+
137
+ Raises:
138
+ ExecutionError: invalid_command for a non-integer
139
+ position.
140
+ Exception: Whatever func raises propagates, and the
141
+ position is left unrecorded so redelivery retries.
142
+ """
143
+ if not self.applies(port, seq):
144
+ return False, None
145
+ result = func(*args, **kwargs)
146
+ self.record(port, seq)
147
+ return True, result
148
+
149
+
150
+ class PortBus:
151
+ """Async per-port queues with acks over the ledger event log.
152
+
153
+ The bus never executes and never merges: writers append to
154
+ the ledger on their own synchronous path, then hand the
155
+ appended events to ingest (or the Engine syncs them with
156
+ sync_ports). Ingest fans out to every enabled push port
157
+ without blocking — a full or silent port is quarantined,
158
+ never a reason to stall a writer.
159
+
160
+ Args:
161
+ clock: Nullary callable returning seconds; time.time
162
+ when omitted. Tests inject a manual clock so the
163
+ ladder stays deterministic.
164
+ warn_after: Seconds of unacked backlog before the warn
165
+ directive (ADR-017 default 10).
166
+ quarantine_after: Seconds of unacked backlog before the
167
+ backlog is dropped and the port quarantines
168
+ (default 15).
169
+ resync_after: Seconds of unacked backlog before the
170
+ resync directive fires (default 20).
171
+ max_depth: Per-port queue ceiling beside the time
172
+ ladder; a port past it quarantines immediately
173
+ (default 1024).
174
+ """
175
+
176
+ def __init__(self, clock=None, warn_after=WARN_AFTER_S,
177
+ quarantine_after=QUARANTINE_AFTER_S,
178
+ resync_after=RESYNC_AFTER_S, max_depth=1024):
179
+ self._clock = clock or time.time
180
+ self._warn_after = float(warn_after)
181
+ self._quarantine_after = float(quarantine_after)
182
+ self._resync_after = float(resync_after)
183
+ self._max_depth = int(max_depth)
184
+ self._lock = threading.RLock()
185
+ self._ports = {}
186
+ self._guards = {}
187
+ self._head = 0
188
+ self._pumps = {}
189
+
190
+ def _now(self):
191
+ """Read the bus clock.
192
+
193
+ Returns:
194
+ Current time in seconds.
195
+ """
196
+ return float(self._clock())
197
+
198
+ def _require(self, name):
199
+ """Fetch one attached port record or fail loudly.
200
+
201
+ Args:
202
+ name: Port name to look up.
203
+
204
+ Returns:
205
+ Live port record mapping.
206
+
207
+ Raises:
208
+ ExecutionError: unknown_port for a detached name.
209
+ """
210
+ try:
211
+ return self._ports[name]
212
+ except KeyError:
213
+ raise ExecutionError(
214
+ "unknown_port", f"No such attached port: {name}."
215
+ ) from None
216
+
217
+ def attach(self, name, push=True, max_depth=None):
218
+ """Attach one port to the shared world.
219
+
220
+ Args:
221
+ name: Port name (stdio, mcp, ide, loopback-http).
222
+ push: True for async push delivery; False leaves
223
+ the port pull-only (MCP default per ADR-017 D7:
224
+ it polls the cursor log, never wakes on push).
225
+ max_depth: Per-port queue ceiling; the bus default
226
+ when omitted.
227
+
228
+ Returns:
229
+ Attached port descriptor (name, push, enabled,
230
+ state, cursor).
231
+
232
+ Raises:
233
+ ExecutionError: invalid_command for a non-text or
234
+ empty name, port_exists for an attached name.
235
+ """
236
+ if (not isinstance(name, str) or not name):
237
+ raise ExecutionError(
238
+ "invalid_command",
239
+ f"Port needs a non-empty text name, got {name!r}.",
240
+ )
241
+ with self._lock:
242
+ if name in self._ports:
243
+ raise ExecutionError(
244
+ "port_exists",
245
+ f"Port already attached: {name}.",
246
+ )
247
+ depth = (int(max_depth) if max_depth is not None
248
+ else self._max_depth)
249
+ record = {"name": name, "push": bool(push),
250
+ "enabled": True, "state": "active",
251
+ "resync_required": False, "queue": [],
252
+ "seen": 0, "acked": 0,
253
+ "last_ack_at": self._now(),
254
+ "max_depth": depth}
255
+ self._ports[name] = record
256
+ self._guards[name] = PositionGuard()
257
+ return self._describe_locked(record)
258
+
259
+ def detach(self, name):
260
+ """Detach one port; its backlog is dropped with it.
261
+
262
+ Args:
263
+ name: Attached port name to remove.
264
+
265
+ Returns:
266
+ Detached port descriptor.
267
+
268
+ Raises:
269
+ ExecutionError: unknown_port for a detached name.
270
+ """
271
+ with self._lock:
272
+ record = self._require(name)
273
+ self.stop(name)
274
+ del self._ports[name]
275
+ self._guards.pop(name, None)
276
+ return self._describe_locked(record)
277
+
278
+ def guard_for(self, name):
279
+ """Fetch one port's bound position guard.
280
+
281
+ Args:
282
+ name: Attached port name.
283
+
284
+ Returns:
285
+ The port's shared PositionGuard.
286
+
287
+ Raises:
288
+ ExecutionError: unknown_port for a detached name.
289
+ """
290
+ with self._lock:
291
+ self._require(name)
292
+ return self._guards[name]
293
+
294
+ def _describe_locked(self, record):
295
+ """Render one port record without its queued events.
296
+
297
+ Args:
298
+ record: Live port record mapping.
299
+
300
+ Returns:
301
+ Descriptor with name, push, enabled, state,
302
+ resync_required, backlog, cursor, acked, and
303
+ max_depth.
304
+ """
305
+ return {"name": record["name"], "push": record["push"],
306
+ "enabled": record["enabled"],
307
+ "state": record["state"],
308
+ "resync_required": record["resync_required"],
309
+ "backlog": len(record["queue"]),
310
+ "cursor": record["seen"],
311
+ "acked": record["acked"],
312
+ "max_depth": record["max_depth"]}
313
+
314
+ def port_state(self, name):
315
+ """Read one port's engine-side bookkeeping.
316
+
317
+ Args:
318
+ name: Attached port name.
319
+
320
+ Returns:
321
+ Descriptor with name, push, enabled, state,
322
+ resync_required, backlog, cursor, acked, and
323
+ max_depth.
324
+
325
+ Raises:
326
+ ExecutionError: unknown_port for a detached name.
327
+ """
328
+ with self._lock:
329
+ return self._describe_locked(self._require(name))
330
+
331
+ @property
332
+ def cursor(self):
333
+ """Highest ledger position ingested across all ports.
334
+
335
+ Returns:
336
+ Bus head seq; ports resync to this position.
337
+ """
338
+ with self._lock:
339
+ return self._head
340
+
341
+ def _quarantine_locked(self, record, reason):
342
+ """Quarantine one port: drop backlog, flag resync.
343
+
344
+ The engine flags, never kills: the client process is
345
+ untouched; only delivery stops until the client
346
+ re-runs its onboarding ritual and resyncs.
347
+
348
+ Args:
349
+ record: Live port record mapping.
350
+ reason: Why the port quarantined (ladder or depth).
351
+
352
+ Returns:
353
+ Quarantine directive mapping.
354
+ """
355
+ record["queue"].clear()
356
+ record["state"] = "quarantined"
357
+ record["resync_required"] = True
358
+ return {"port": record["name"], "directive": reason,
359
+ "resync_required": True}
360
+
361
+ def ingest(self, events):
362
+ """Fan appended ledger events out to every push port.
363
+
364
+ Never blocks and never raises for port conditions: a
365
+ port past its depth ceiling quarantines (backlog
366
+ dropped, resync flagged) while writers and every other
367
+ port continue. Disabled, pull-only, and quarantined
368
+ ports are skipped — a quarantined port converges only
369
+ through resync, never through catch-up flooding.
370
+
371
+ Args:
372
+ events: Ledger event mappings, each with an
373
+ integer seq, oldest first.
374
+
375
+ Returns:
376
+ Per-port enqueue counts for the ports that took
377
+ the batch.
378
+
379
+ Raises:
380
+ ExecutionError: invalid_command for a non-mapping
381
+ event or a non-integer seq.
382
+ """
383
+ batch = list(events or [])
384
+ for event in batch:
385
+ if not isinstance(event, dict):
386
+ raise ExecutionError(
387
+ "invalid_command",
388
+ f"Bus events must be mappings, got {event!r}.",
389
+ )
390
+ seq = event.get("seq")
391
+ if (not isinstance(seq, int) or
392
+ isinstance(seq, bool)):
393
+ raise ExecutionError(
394
+ "invalid_command",
395
+ f"Bus events need an integer seq, got {event!r}.",
396
+ )
397
+ counts = {}
398
+ with self._lock:
399
+ for event in batch:
400
+ seq = event["seq"]
401
+ if seq > self._head:
402
+ self._head = seq
403
+ for record in self._ports.values():
404
+ if (not record["enabled"] or
405
+ not record["push"] or
406
+ record["state"] == "quarantined"):
407
+ continue
408
+ if seq <= record["seen"]:
409
+ continue
410
+ record["queue"].append(dict(event))
411
+ record["seen"] = seq
412
+ counts[record["name"]] = (
413
+ counts.get(record["name"], 0) + 1)
414
+ if (len(record["queue"]) >
415
+ record["max_depth"]):
416
+ self._quarantine_locked(record,
417
+ "depth_ceiling")
418
+ return counts
419
+
420
+ def pending(self, name):
421
+ """Read one port's queued events without consuming them.
422
+
423
+ Args:
424
+ name: Attached port name.
425
+
426
+ Returns:
427
+ Queued event copies, oldest first.
428
+
429
+ Raises:
430
+ ExecutionError: unknown_port for a detached name.
431
+ """
432
+ with self._lock:
433
+ record = self._require(name)
434
+ return [dict(event) for event in record["queue"]]
435
+
436
+ def ack(self, name, seq):
437
+ """Acknowledge delivery through one ledger position.
438
+
439
+ Any ack — even for an already-dropped position — counts
440
+ as client activity and refreshes the ladder clock.
441
+ Queue entries at or below the position are dropped.
442
+
443
+ Args:
444
+ name: Attached port name.
445
+ seq: Ledger position acknowledged.
446
+
447
+ Returns:
448
+ Ack record with port, acked position, and backlog.
449
+
450
+ Raises:
451
+ ExecutionError: unknown_port for a detached name,
452
+ invalid_command for a non-integer position.
453
+ """
454
+ if (not isinstance(seq, int) or
455
+ isinstance(seq, bool)):
456
+ raise ExecutionError(
457
+ "invalid_command",
458
+ f"Ack needs an integer seq, got {seq!r}.",
459
+ )
460
+ with self._lock:
461
+ record = self._require(name)
462
+ if seq > record["acked"]:
463
+ record["acked"] = seq
464
+ record["queue"] = [event for event in record["queue"]
465
+ if event["seq"] > record["acked"]]
466
+ record["last_ack_at"] = self._now()
467
+ return {"port": name, "acked": record["acked"],
468
+ "backlog": len(record["queue"])}
469
+
470
+ def port_evaluate(self):
471
+ """Run the staged no-ack ladder over every attached port.
472
+
473
+ A port with an unacked backlog escalates: past
474
+ warn_after it is flagged warned, past
475
+ quarantine_after its backlog is dropped and it
476
+ quarantines; past resync_after the engine-side resync
477
+ flag (the resync directive) fires for the drained
478
+ port. Each rung fires once per episode; a port that
479
+ keeps consuming or acking never escalates.
480
+
481
+ Args:
482
+ None.
483
+
484
+ Returns:
485
+ Directive mappings (port, directive, resync_required
486
+ for the terminal rungs) in attach order.
487
+ """
488
+ directives = []
489
+ with self._lock:
490
+ now = self._now()
491
+ for record in self._ports.values():
492
+ if not record["enabled"]:
493
+ continue
494
+ age = now - record["last_ack_at"]
495
+ if record["queue"]:
496
+ if (age > self._warn_after and
497
+ record["state"] == "active"):
498
+ record["state"] = "warned"
499
+ directives.append(
500
+ {"port": record["name"],
501
+ "directive": "warn", "age_s": age})
502
+ if (age > self._quarantine_after and
503
+ record["state"] == "warned"):
504
+ record["queue"].clear()
505
+ record["state"] = "quarantined"
506
+ record["resync_required"] = True
507
+ directives.append(
508
+ {"port": record["name"],
509
+ "directive": "quarantine",
510
+ "age_s": age,
511
+ "resync_required": True})
512
+ if (age > self._resync_after and
513
+ record["state"] == "quarantined" and
514
+ record["resync_required"] and
515
+ not record["queue"]):
516
+ directives.append(
517
+ {"port": record["name"],
518
+ "directive": "resync_required",
519
+ "age_s": age, "resync_required": True})
520
+ return directives
521
+
522
+ def port_resync(self, name, head=None):
523
+ """Converge one port onto the current ledger head.
524
+
525
+ The client re-runs its idempotent onboarding ritual
526
+ from this position: the backlog is dropped, the cursor
527
+ jumps to head, flags clear. Quarantine is dodgeable
528
+ only through a fresh port otherwise — reconnects come
529
+ back as new ports with fresh cursors.
530
+
531
+ Args:
532
+ name: Attached port name.
533
+ head: Ledger position to converge on; the bus head
534
+ when omitted.
535
+
536
+ Returns:
537
+ Resync receipt with port, cursor, state, and the
538
+ cleared resync flag.
539
+
540
+ Raises:
541
+ ExecutionError: unknown_port for a detached name,
542
+ invalid_command for a non-integer head.
543
+ """
544
+ if head is not None and (
545
+ not isinstance(head, int) or
546
+ isinstance(head, bool)):
547
+ raise ExecutionError(
548
+ "invalid_command",
549
+ f"Resync head must be an integer seq, got {head!r}.",
550
+ )
551
+ with self._lock:
552
+ record = self._require(name)
553
+ target = self._head if head is None else head
554
+ record["queue"].clear()
555
+ record["seen"] = max(record["seen"], target)
556
+ record["acked"] = max(record["acked"], target)
557
+ record["state"] = "active"
558
+ record["resync_required"] = False
559
+ record["last_ack_at"] = self._now()
560
+ return {"port": name, "cursor": record["seen"],
561
+ "state": "active",
562
+ "resync_required": False}
563
+
564
+ def set_enabled(self, name, enabled, local=False):
565
+ """Enable or disable one port live (local-only toggle).
566
+
567
+ Disabling quarantines like a dead ship: the backlog is
568
+ dropped and the resync flag is set, so a returning
569
+ port must resync before it converges. Enabling never
570
+ clears the flag — the client resyncs explicitly.
571
+
572
+ Args:
573
+ name: Attached port name.
574
+ enabled: True to attach the port to live delivery,
575
+ False to park it.
576
+ local: Caller asserts a local (loopback/stdio)
577
+ caller; anything else is refused loudly per
578
+ LOCAL-SINGLE-USER.
579
+
580
+ Returns:
581
+ Updated port descriptor.
582
+
583
+ Raises:
584
+ ExecutionError: unknown_port for a detached name,
585
+ remote_toggle_denied for a non-local caller.
586
+ """
587
+ if local is not True:
588
+ raise ExecutionError(
589
+ "remote_toggle_denied",
590
+ "Port toggles are local-only; a remote caller "
591
+ f"cannot change port {name}.",
592
+ )
593
+ with self._lock:
594
+ record = self._require(name)
595
+ record["enabled"] = bool(enabled)
596
+ if not enabled:
597
+ record["queue"].clear()
598
+ record["state"] = "quarantined"
599
+ record["resync_required"] = True
600
+ return self._describe_locked(record)
601
+
602
+ def set_profile(self, names, local=False):
603
+ """Swap the live port profile (local-only toggle).
604
+
605
+ Missing names attach as push ports; every other
606
+ attached port disables like a dead ship (backlog
607
+ dropped, resync flagged). The boot profile is stdio
608
+ alone; MCP attaches when the agent arrives, loopback
609
+ HTTP when the IDE wants network.
610
+
611
+ Args:
612
+ names: Port names forming the live profile.
613
+ local: Caller asserts a local (loopback/stdio)
614
+ caller; anything else is refused loudly per
615
+ LOCAL-SINGLE-USER.
616
+
617
+ Returns:
618
+ Profile mapping with the enabled names sorted.
619
+
620
+ Raises:
621
+ ExecutionError: remote_toggle_denied for a
622
+ non-local caller, invalid_command for a
623
+ non-list or empty profile.
624
+ """
625
+ if local is not True:
626
+ raise ExecutionError(
627
+ "remote_toggle_denied",
628
+ "Port profile swaps are local-only; a remote "
629
+ "caller cannot change the profile.",
630
+ )
631
+ wanted = list(names or [])
632
+ if (not isinstance(names, (list, tuple)) or
633
+ not wanted):
634
+ raise ExecutionError(
635
+ "invalid_command",
636
+ "A port profile needs a non-empty list of names.",
637
+ )
638
+ with self._lock:
639
+ for name in wanted:
640
+ if name not in self._ports:
641
+ self._ports[name] = {
642
+ "name": name, "push": True,
643
+ "enabled": True, "state": "active",
644
+ "resync_required": False, "queue": [],
645
+ "seen": 0, "acked": 0,
646
+ "last_ack_at": self._now(),
647
+ "max_depth": self._max_depth}
648
+ self._guards[name] = PositionGuard()
649
+ for name, record in self._ports.items():
650
+ if name in wanted:
651
+ record["enabled"] = True
652
+ else:
653
+ record["enabled"] = False
654
+ record["queue"].clear()
655
+ record["state"] = "quarantined"
656
+ record["resync_required"] = True
657
+ return {"profile": sorted(wanted)}
658
+
659
+ def drain(self, name, handler, limit=None, guarded=True):
660
+ """Deliver one port's backlog through a handler, then ack.
661
+
662
+ Handlers run outside the bus lock; a slow handler never
663
+ blocks writers or other ports. With the port's bound
664
+ guard, a redelivered position is acknowledged but not
665
+ re-applied. A raising handler keeps its event at the
666
+ head of the queue and stops the drain — nothing is
667
+ lost, nothing is acked past the failure.
668
+
669
+ Args:
670
+ name: Attached port name.
671
+ handler: Callable taking one event mapping.
672
+ limit: Maximum events to deliver; the whole
673
+ backlog when omitted.
674
+ guarded: True routes every position through the
675
+ port's bound PositionGuard; False applies
676
+ every delivery unconditionally.
677
+
678
+ Returns:
679
+ Drain record with port, delivered and applied
680
+ counts, the acked position, and handler
681
+ error messages.
682
+
683
+ Raises:
684
+ ExecutionError: unknown_port for a detached name.
685
+ """
686
+ with self._lock:
687
+ record = self._require(name)
688
+ guard = self._guards[name]
689
+ batch = list(record["queue"])
690
+ if limit is not None:
691
+ batch = batch[:max(0, int(limit))]
692
+ del record["queue"][:len(batch)]
693
+ delivered = 0
694
+ applied = 0
695
+ errors = []
696
+ last_seq = None
697
+ for index, event in enumerate(batch):
698
+ seq = event["seq"]
699
+ try:
700
+ if guarded:
701
+ did, _ = guard.apply_once(
702
+ name, seq, handler, event)
703
+ else:
704
+ handler(event)
705
+ did = True
706
+ except Exception as exc:
707
+ with self._lock:
708
+ live = self._ports.get(name)
709
+ if live is not None:
710
+ live["queue"][0:0] = batch[index:]
711
+ errors.append(f"{type(exc).__name__}: {exc}")
712
+ break
713
+ delivered += 1
714
+ if did:
715
+ applied += 1
716
+ last_seq = seq
717
+ if last_seq is not None:
718
+ self.ack(name, last_seq)
719
+ with self._lock:
720
+ acked = self._require(name)["acked"]
721
+ return {"port": name, "delivered": delivered,
722
+ "applied": applied, "acked": acked,
723
+ "errors": errors}
724
+
725
+ def start(self, name, handler, interval=0.01):
726
+ """Pump one port on a daemon thread until stopped.
727
+
728
+ Args:
729
+ name: Attached port name.
730
+ handler: Callable taking one event mapping; its
731
+ exceptions are contained per drain and
732
+ retried on the next pass.
733
+ interval: Idle seconds between passes when the
734
+ backlog is empty.
735
+
736
+ Returns:
737
+ True once the pump runs.
738
+
739
+ Raises:
740
+ ExecutionError: unknown_port for a detached name,
741
+ delivery_running for an already-pumped port.
742
+ """
743
+ with self._lock:
744
+ self._require(name)
745
+ if name in self._pumps:
746
+ raise ExecutionError(
747
+ "delivery_running",
748
+ f"Port pump already runs: {name}.",
749
+ )
750
+ halt = threading.Event()
751
+ worker = threading.Thread(
752
+ target=self._pump, args=(name, handler,
753
+ float(interval), halt),
754
+ daemon=True, name=f"port-pump-{name}")
755
+ self._pumps[name] = (worker, halt)
756
+ worker.start()
757
+ return True
758
+
759
+ def _pump(self, name, handler, interval, halt):
760
+ """Run one port pump until its halt event is set.
761
+
762
+ Args:
763
+ name: Attached port name.
764
+ handler: Callable taking one event mapping.
765
+ interval: Idle seconds between empty passes.
766
+ halt: Threading event stopping the pump.
767
+
768
+ Returns:
769
+ None.
770
+ """
771
+ while not halt.is_set():
772
+ try:
773
+ out = self.drain(name, handler)
774
+ except ExecutionError:
775
+ return
776
+ if not out["delivered"]:
777
+ halt.wait(interval)
778
+
779
+ def stop(self, name):
780
+ """Stop one port's background pump, if any.
781
+
782
+ Args:
783
+ name: Attached port name.
784
+
785
+ Returns:
786
+ True when a pump stopped, False when the port had
787
+ none running.
788
+ """
789
+ with self._lock:
790
+ entry = self._pumps.pop(name, None)
791
+ if entry is None:
792
+ return False
793
+ worker, halt = entry
794
+ halt.set()
795
+ if threading.current_thread() is not worker:
796
+ worker.join(timeout=5)
797
+ return True