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,897 @@
1
+ """Headless Python execution (PYTHON-EXEC): inline lines + project files.
2
+
3
+ First-class scripted execution on the prepared worker substrate the
4
+ stdio console shares: every `python_execute` call runs in a fresh
5
+ Bubblewrap-confined worker (pinned interpreter, PDK access, tool
6
+ binaries) with the console prebounds (`project/tools/pdk/gf/np/plot`)
7
+ available and zero setup. Exactly two input modes exist — `lines`
8
+ (inline script text) and `path` (a project-contained script file) —
9
+ and nothing else: no shells, no absolute host paths.
10
+
11
+ Approval resolves per python-console-spec, never inside the run:
12
+ `pending_approval` / `approved` / `denied` / `expired` are approval
13
+ states, distinct from the job states (`queued` / `running` /
14
+ `completed` / `cancelled`) the execution service owns. Agent calls
15
+ request approval; only the human UI route resolves it
16
+ (`resolve_approval(via="ui")`) — MCP self-approval raises
17
+ `approval_forbidden`. Denial and expiry create no run.
18
+
19
+ Completion is never a check verdict: every terminal record carries
20
+ `state`/`ok` plus a `NOT_RUN` verdict — zero exit is not PASS, read
21
+ the execution record. Effects (op calls the cell recorded) replay
22
+ through the shared op dispatch after the worker exits, and their
23
+ child `run_ids` ride on the record as structured data.
24
+ """
25
+
26
+ import hashlib
27
+ import io
28
+ import json
29
+ import tempfile
30
+ import threading
31
+ import time
32
+ import uuid
33
+ from pathlib import Path
34
+
35
+ from ruamel.yaml import YAML
36
+
37
+ from .errors import ExecutionError
38
+ from .service import ExecutionService
39
+ from .pyexec.approvals import _ApprovalMixin
40
+ from .pyexec.runner import PythonExecRunner
41
+ from .pyexec.support import (
42
+ _check_timeout,
43
+ _render_preamble,
44
+ _validate_artifacts,
45
+ CELL_ENTRY,
46
+ DEFAULT_TIMEOUT_S,
47
+ MAX_ARTIFACTS,
48
+ MAX_LINES_BYTES,
49
+ MAX_PATH_CHARS,
50
+ MAX_TIMEOUT_S,
51
+ MIN_TIMEOUT_S,
52
+ OPS_LOG_NAME,
53
+ PRELUDE_DIGEST,
54
+ PRELUDE_TEMPLATE,
55
+ build_worker_source,
56
+ not_run_verdict,
57
+ parse_exception,
58
+ prelude_digest_for,
59
+ resolve_script_path,
60
+ )
61
+
62
+
63
+ __all__ = [
64
+ "CELL_ENTRY",
65
+ "DEFAULT_APPROVAL_TTL_S",
66
+ "DEFAULT_TIMEOUT_S",
67
+ "MAX_ARTIFACTS",
68
+ "MAX_LINES_BYTES",
69
+ "MAX_PATH_CHARS",
70
+ "MAX_TIMEOUT_S",
71
+ "MCP_INPUT_SCHEMA",
72
+ "MIN_TIMEOUT_S",
73
+ "MODES",
74
+ "OPS_LOG_NAME",
75
+ "PRELUDE_DIGEST",
76
+ "PRELUDE_TEMPLATE",
77
+ "PythonExecRunner",
78
+ "PythonExecService",
79
+ "build_worker_source",
80
+ "not_run_verdict",
81
+ "parse_exception",
82
+ "prelude_digest_for",
83
+ "resolve_script_path",
84
+ ]
85
+
86
+ #: Exactly two input modes: inline script text, or a project file.
87
+ MODES = ("lines", "path")
88
+
89
+ #: Pending approvals lapse after this long without a UI decision.
90
+ DEFAULT_APPROVAL_TTL_S = 900
91
+
92
+ #: Live index bound: denied/expired approvals past this count evict
93
+ #: oldest-first. Pending and approved approvals are live
94
+ #: references (runs and status reads resolve through them) and
95
+ #: never reap; the `approvals/*.yaml` audit trail keeps every
96
+ #: record on disk regardless of eviction.
97
+ MAX_TERMINAL_APPROVALS = 100
98
+
99
+
100
+ MCP_INPUT_SCHEMA = {
101
+ "type": "object",
102
+ "description": (
103
+ "Execute a Python cell in a fresh prepared worker. Exactly one "
104
+ "of lines (inline script text) or path (a project-contained "
105
+ "script file) is required; the other must be absent."
106
+ ),
107
+ "properties": {
108
+ "mode": {
109
+ "type": "string",
110
+ "enum": ["lines", "path"],
111
+ "description": "Input mode: inline script text or project file.",
112
+ },
113
+ "lines": {
114
+ "type": "string",
115
+ "maxLength": MAX_LINES_BYTES,
116
+ "description": "Inline script text (lines mode only).",
117
+ },
118
+ "path": {
119
+ "type": "string",
120
+ "maxLength": MAX_PATH_CHARS,
121
+ "description": (
122
+ "Project-relative script path (path mode only); "
123
+ "traversal and out-of-project paths are rejected."
124
+ ),
125
+ },
126
+ "timeout_s": {
127
+ "type": "integer",
128
+ "minimum": MIN_TIMEOUT_S,
129
+ "maximum": MAX_TIMEOUT_S,
130
+ "description": "Worker budget in seconds (default 120).",
131
+ },
132
+ "artifacts": {
133
+ "type": "array",
134
+ "items": {"type": "string"},
135
+ "maxItems": MAX_ARTIFACTS,
136
+ "description": "Scratch-relative paths to harvest.",
137
+ },
138
+ "expected_revision": {
139
+ "type": "string",
140
+ "description": "Project revision the approval binds.",
141
+ },
142
+ },
143
+ "required": ["mode"],
144
+ "oneOf": [
145
+ {
146
+ "properties": {"mode": {"const": "lines"}},
147
+ "required": ["mode", "lines"],
148
+ "not": {"required": ["path"]},
149
+ },
150
+ {
151
+ "properties": {"mode": {"const": "path"}},
152
+ "required": ["mode", "path"],
153
+ "not": {"required": ["lines"]},
154
+ },
155
+ ],
156
+ }
157
+
158
+
159
+ class PythonExecService(_ApprovalMixin):
160
+ """Approval-gated headless execution: request, resolve, run, cancel.
161
+
162
+ Agents request executions; the human UI route resolves the
163
+ approval; admitted runs execute in fresh prepared workers and
164
+ their recorded effects replay through the shared op dispatch.
165
+ Approval states (`pending_approval`/`approved`/`denied`/
166
+ `expired`) live in this service's approval ledger, never in the
167
+ job records — denial and expiry create no run, and a pending
168
+ approval addresses no runnable job.
169
+
170
+ History persists under the service root (`cells/<id>.py` holds
171
+ the exact submitted cell, `executions/<id>.yaml` the terminal
172
+ record, `approvals/<id>.yaml` the approval audit trail).
173
+
174
+ Args:
175
+ project_id: Project executions are bound to.
176
+ actor: Attribution recorded on approvals and runs.
177
+ root: Service state dir; temp dir default.
178
+ execution: Job surface owning run records; an in-memory
179
+ execution service when omitted.
180
+ project_root: Project dir path mode resolves inside; path
181
+ mode refuses loudly without one.
182
+ runner: Worker runner; a prepared runner when omitted
183
+ (built lazily, on the first admitted run).
184
+ dispatcher: Op dispatch effects replay through
185
+ (`execute(operation, arguments)`); without one, ops
186
+ are recorded but never replayed, loudly marked.
187
+ approval_ttl_s: Pending approvals lapse after this long.
188
+ clock: Monotonic clock seconds source (tests inject fakes).
189
+ revision_provider: Zero-arg callable returning the live
190
+ project revision; admission fails closed on drift when
191
+ set, skips the check when omitted.
192
+ """
193
+
194
+ def __init__(self, project_id, actor="agent", root=None, execution=None,
195
+ project_root=None, runner=None, dispatcher=None,
196
+ approval_ttl_s=DEFAULT_APPROVAL_TTL_S,
197
+ clock=time.monotonic, revision_provider=None):
198
+ self._project_id = project_id
199
+ self._actor = actor
200
+ self._root = Path(
201
+ root if root is not None
202
+ else Path(tempfile.gettempdir()) / "rocketeda_pyexec" / project_id
203
+ )
204
+ (self._root / "cells").mkdir(parents=True, exist_ok=True)
205
+ (self._root / "executions").mkdir(parents=True, exist_ok=True)
206
+ (self._root / "approvals").mkdir(parents=True, exist_ok=True)
207
+ self._execution = (
208
+ execution if execution is not None
209
+ else ExecutionService(project_id)
210
+ )
211
+ self._project_root = project_root
212
+ self._runner = runner
213
+ self._dispatcher = dispatcher
214
+ self._approval_ttl_s = approval_ttl_s
215
+ self._clock = clock
216
+ self._revision_provider = revision_provider
217
+ self._generation = uuid.uuid4().hex
218
+ self._approvals = {}
219
+ self._requests = {}
220
+ self._runs = {}
221
+ self._records = {}
222
+ self._cancel_flags = set()
223
+ self._cancel_lock = threading.Lock()
224
+ self._run_lock = threading.Lock()
225
+ self._env_digest = prelude_digest_for(project_id)
226
+ self._cursor = len(list((self._root / "executions").glob("*.yaml")))
227
+ self._yaml = YAML(typ="safe")
228
+
229
+ @property
230
+ def generation(self):
231
+ """Current approval generation; reset retires it.
232
+
233
+ Returns:
234
+ Generation id string.
235
+ """
236
+ return self._generation
237
+
238
+ @property
239
+ def cursor(self):
240
+ """History cursor: count of persisted executions.
241
+
242
+ Returns:
243
+ Execution count as an int.
244
+ """
245
+ return self._cursor
246
+
247
+ @property
248
+ def job_surface(self):
249
+ """Execution service owning this service's run records.
250
+
251
+ Returns:
252
+ Bound execution service.
253
+ """
254
+ return self._execution
255
+
256
+ def _dump(self, data):
257
+ buf = io.StringIO()
258
+ self._yaml.dump(data, buf)
259
+ return buf.getvalue()
260
+
261
+ @staticmethod
262
+ def _normalize(code):
263
+ text = code if code.endswith("\n") else code + "\n"
264
+ return text
265
+
266
+ def _read_cell(self, execution_id):
267
+ return (self._root / "cells" / ("%s.py" % execution_id)).read_text(
268
+ encoding="utf-8")
269
+
270
+ def _cell_digest(self, execution_id):
271
+ try:
272
+ data = (self._root / "cells" / ("%s.py" % execution_id)).read_bytes()
273
+ except OSError:
274
+ return None
275
+ return hashlib.sha256(data).hexdigest()
276
+
277
+ def _sweep_expired(self):
278
+ now = self._clock()
279
+ for record in self._approvals.values():
280
+ if record["state"] == "pending_approval" and now > record["expires_at"]:
281
+ record["state"] = "expired"
282
+ record["reason"] = "Approval TTL elapsed without a UI decision."
283
+ self._persist_approval(record)
284
+ self._reap_terminal()
285
+
286
+ def _reap_terminal(self):
287
+ """Evict oldest terminal approvals past the live index bound.
288
+
289
+ Denied and expired approvals are dead references: no run or
290
+ status path can proceed through them. Past the bound the
291
+ oldest evict first (by creation, then id); pending and
292
+ approved approvals are live and never reap. The YAML audit
293
+ trail on disk is untouched — this bounds the live index,
294
+ not history.
295
+ """
296
+ terminal = [
297
+ record for record in self._approvals.values()
298
+ if record["state"] in ("denied", "expired")
299
+ ]
300
+ excess = len(terminal) - MAX_TERMINAL_APPROVALS
301
+ if excess <= 0:
302
+ return
303
+ # Stable sort: ties keep admission order, so the earliest
304
+ # admitted terminal approval evicts first even when the
305
+ # clock never advanced between admissions.
306
+ terminal.sort(key=lambda record: record.get("created_at", 0))
307
+ for record in terminal[:excess]:
308
+ del self._approvals[record["approval_id"]]
309
+
310
+
311
+ def _persist_execution(self, record):
312
+ stored = {
313
+ **record,
314
+ "project_id": self._project_id,
315
+ "actor": self._actor,
316
+ "generation": self._generation,
317
+ "env_digest": self._env_digest,
318
+ "history_cursor": self._cursor + 1,
319
+ }
320
+ (self._root / "executions" / ("%s.yaml" % record["execution_id"])).write_text(
321
+ self._dump(stored), encoding="utf-8"
322
+ )
323
+ self._cursor += 1
324
+ return stored
325
+
326
+ def validate_request(self, mode, lines=None, path=None, artifacts=(),
327
+ timeout_s=None):
328
+ """Validate one execution request without changing state.
329
+
330
+ Pure pre-check: the Engine runs this before its own
331
+ revision-gated admission so malformed requests fail before
332
+ anything is persisted or queued.
333
+
334
+ Args:
335
+ mode: Input mode, exactly `lines` or `path`.
336
+ lines: Inline script text for lines mode.
337
+ path: Project-relative script path for path mode.
338
+ artifacts: Scratch-relative paths to harvest.
339
+ timeout_s: Worker budget; the bounded default when
340
+ omitted.
341
+
342
+ Returns:
343
+ (display, source_file, budget, artifacts) tuple: the
344
+ normalized cell text, the project-relative source
345
+ (path mode) or None, the resolved budget, and the
346
+ validated artifact names.
347
+
348
+ Raises:
349
+ ExecutionError: invalid_command for a bad mode, a
350
+ mode/field mismatch, oversize input, or a malformed
351
+ timeout, invalid_cell/empty_cell for bad script
352
+ text, path_escape/invalid_path/cell_unreadable for
353
+ bad project files.
354
+ """
355
+ if mode not in MODES:
356
+ raise ExecutionError(
357
+ "invalid_command",
358
+ f"mode must be one of {list(MODES)}, got {mode!r}.",
359
+ )
360
+ if mode == "lines" and (lines is None or path is not None):
361
+ raise ExecutionError(
362
+ "invalid_command",
363
+ "lines mode needs lines text and no path.",
364
+ )
365
+ if mode == "path" and (path is None or lines is not None):
366
+ raise ExecutionError(
367
+ "invalid_command",
368
+ "path mode needs a path and no lines text.",
369
+ )
370
+ budget = DEFAULT_TIMEOUT_S if timeout_s is None else _check_timeout(timeout_s)
371
+ names = _validate_artifacts(artifacts)
372
+ if mode == "lines":
373
+ if not isinstance(lines, str):
374
+ raise ExecutionError(
375
+ "invalid_cell",
376
+ f"Cell code must be UTF-8 text, got {type(lines).__name__}.",
377
+ )
378
+ if not lines.strip():
379
+ raise ExecutionError("empty_cell", "Cell is empty.")
380
+ if len(lines.encode("utf-8")) > MAX_LINES_BYTES:
381
+ raise ExecutionError(
382
+ "invalid_command",
383
+ f"lines exceed {MAX_LINES_BYTES} bytes.",
384
+ )
385
+ return self._normalize(lines), None, budget, names
386
+ script = resolve_script_path(self._project_root, path)
387
+ try:
388
+ text = script.read_text(encoding="utf-8")
389
+ except (OSError, ValueError) as exc:
390
+ raise ExecutionError(
391
+ "cell_unreadable",
392
+ f"Cannot read script file {path}: {exc}.",
393
+ ) from exc
394
+ if not text.strip():
395
+ raise ExecutionError("empty_cell", "Cell is empty.")
396
+ return self._normalize(text), path, budget, names
397
+
398
+ def request_execution(self, mode, lines=None, path=None, request_id=None,
399
+ run_id=None, expected_revision="local",
400
+ artifacts=(), timeout_s=None):
401
+ """Request one execution: persist the cell, open an approval.
402
+
403
+ Job keys:
404
+ mode (required): `lines` for inline script text, `path`
405
+ for a project-contained script file.
406
+ lines: Inline script text (lines mode only).
407
+ path: Project-relative script path (path mode only).
408
+ timeout_s: Worker budget in seconds (default 120).
409
+ artifacts: Scratch-relative paths to harvest.
410
+ expected_revision: Project revision the approval binds.
411
+
412
+ The call returns promptly with an approval handle; it runs
413
+ nothing. Repeating a request_id with identical code returns
414
+ the stored handle; changed code under the same id conflicts.
415
+
416
+ Args:
417
+ mode: Input mode, exactly `lines` or `path`.
418
+ lines: Inline script text for lines mode.
419
+ path: Project-relative script path for path mode.
420
+ request_id: Client envelope id; generated when omitted.
421
+ run_id: Prescribed execution id (the Engine submit ack
422
+ id); minted at approval when omitted.
423
+ expected_revision: Revision the approval binds.
424
+ artifacts: Scratch-relative paths to harvest.
425
+ timeout_s: Worker budget; the bounded default when
426
+ omitted.
427
+
428
+ Returns:
429
+ Approval handle with execution_id, approval_id,
430
+ pending_approval state, code hash, and the run_id
431
+ only when the caller prescribed one.
432
+
433
+ Raises:
434
+ ExecutionError: invalid_command for a bad mode, a
435
+ mode/field mismatch, oversize input, or a malformed
436
+ timeout, invalid_cell/empty_cell for bad script
437
+ text, path_escape/invalid_path/cell_unreadable for
438
+ bad project files, request_conflict for a reused
439
+ request id with new code.
440
+ """
441
+ display, source_file, budget, names = self.validate_request(
442
+ mode, lines=lines, path=path, artifacts=artifacts,
443
+ timeout_s=timeout_s,
444
+ )
445
+ code_sha256 = hashlib.sha256(display.encode("utf-8")).hexdigest()
446
+ request_id = request_id or uuid.uuid4().hex
447
+ prev = self._requests.get(request_id)
448
+ if prev is not None:
449
+ if prev["code_sha256"] != code_sha256:
450
+ raise ExecutionError(
451
+ "request_conflict",
452
+ f"Request id reused with different code: {request_id}.",
453
+ )
454
+ return self._handle_for(prev["execution_id"], request_id)
455
+ if run_id is not None and run_id in self._runs:
456
+ raise ExecutionError(
457
+ "invalid_command",
458
+ f"run_id already admitted: {run_id}.",
459
+ )
460
+ execution_id = uuid.uuid4().hex
461
+ (self._root / "cells" / ("%s.py" % execution_id)).write_text(
462
+ display, encoding="utf-8")
463
+ approval_id = uuid.uuid4().hex
464
+ now = self._clock()
465
+ record = {
466
+ "approval_id": approval_id,
467
+ "execution_id": execution_id,
468
+ "run_id": run_id,
469
+ "request_id": request_id,
470
+ "mode": mode,
471
+ "source_file": source_file,
472
+ "code_sha256": code_sha256,
473
+ "history_cursor": self._cursor,
474
+ "env_digest": self._env_digest,
475
+ "expected_revision": expected_revision,
476
+ "generation": self._generation,
477
+ "timeout_s": budget,
478
+ "artifacts": names,
479
+ "state": "pending_approval",
480
+ "reason": None,
481
+ "created_at": now,
482
+ "expires_at": now + self._approval_ttl_s,
483
+ }
484
+ self._approvals[approval_id] = record
485
+ if run_id is not None:
486
+ self._runs[run_id] = execution_id
487
+ self._requests[request_id] = {
488
+ "execution_id": execution_id,
489
+ "approval_id": approval_id,
490
+ "code_sha256": code_sha256,
491
+ }
492
+ self._persist_approval(record)
493
+ self._reap_terminal()
494
+ return self._handle_for(execution_id, request_id)
495
+
496
+ def _handle_for(self, execution_id, request_id):
497
+ record = next(
498
+ item for item in self._approvals.values()
499
+ if item["execution_id"] == execution_id
500
+ )
501
+ handle = {
502
+ "request_id": request_id,
503
+ "execution_id": execution_id,
504
+ "approval_id": record["approval_id"],
505
+ "approval_state": record["state"],
506
+ "mode": record["mode"],
507
+ "source_file": record["source_file"],
508
+ "code_sha256": record["code_sha256"],
509
+ "history_cursor": record["history_cursor"],
510
+ "expected_revision": record["expected_revision"],
511
+ "timeout_s": record["timeout_s"],
512
+ }
513
+ if record["run_id"] is not None:
514
+ handle["run_id"] = record["run_id"]
515
+ return handle
516
+
517
+
518
+ def run_job(self, run_id, on_live=None):
519
+ """Execute an admitted run in a fresh worker, then replay ops.
520
+
521
+ Second preflight precedes any spawn: a lapsed approval (or a
522
+ retired generation) cancels the run instead of executing a
523
+ stranger's cell, and a cancelled run never spawns. Effects
524
+ the cell recorded replay through the shared op dispatch
525
+ after the worker exits; their child run_ids ride on the
526
+ terminal record as structured data.
527
+
528
+ Args:
529
+ run_id: Admitted execution id from resolve_approval.
530
+ on_live: Optional callback while the worker is live.
531
+
532
+ Returns:
533
+ Terminal execution record with state, bounded stdio,
534
+ the parsed exception location, replayed effects
535
+ with child run_ids, and the NOT_RUN verdict.
536
+
537
+ Raises:
538
+ ExecutionError: unknown_run for an id never admitted,
539
+ approval_pending/approval_denied/approval_expired
540
+ when the run's approval lapses, approval_invalid
541
+ for binding drift or tampered cell bytes.
542
+ """
543
+ try:
544
+ execution_id = self._runs[run_id]
545
+ except KeyError:
546
+ raise ExecutionError(
547
+ "unknown_run", f"Unknown run: {run_id}."
548
+ ) from None
549
+ record = next(
550
+ item for item in self._approvals.values()
551
+ if item["execution_id"] == execution_id
552
+ )
553
+ approval_state = record["state"]
554
+ if approval_state == "pending_approval":
555
+ self._sweep_expired()
556
+ if record["state"] == "expired":
557
+ raise ExecutionError(
558
+ "approval_expired", "Approval expired; no run exists."
559
+ )
560
+ raise ExecutionError(
561
+ "approval_pending",
562
+ "Approval is still pending; the UI route has not resolved it.",
563
+ )
564
+ if approval_state == "denied":
565
+ raise ExecutionError(
566
+ "approval_denied", "Approval was denied; no run exists."
567
+ )
568
+ if approval_state == "expired":
569
+ raise ExecutionError(
570
+ "approval_expired", "Approval expired; no run exists."
571
+ )
572
+ with self._run_lock:
573
+ if run_id in self._records:
574
+ return dict(self._records[run_id])
575
+ state = self._execution.status(run_id)["state"]
576
+ if state == "cancelled":
577
+ return self._store_cancelled(
578
+ run_id, execution_id, spawned=False,
579
+ reason="cancelled before dispatch; worker never spawned.",
580
+ )
581
+ if state != "queued":
582
+ raise ExecutionError(
583
+ "invalid_command",
584
+ f"Run is already {state}: {run_id}.",
585
+ )
586
+ self._execution.mark_running(run_id)
587
+ lapse = self._lapse_reason(record)
588
+ if lapse is not None:
589
+ self._execution.cancel(run_id)
590
+ return self._store_cancelled(
591
+ run_id, execution_id, spawned=False, reason=lapse,
592
+ )
593
+ if self._cell_digest(execution_id) != record["code_sha256"]:
594
+ self._execution.cancel(run_id)
595
+ raise ExecutionError(
596
+ "approval_invalid",
597
+ "Cell bytes changed under their approval.",
598
+ )
599
+ display = self._read_cell(execution_id)
600
+ source = build_worker_source(display, project_id=self._project_id)
601
+ # Display line 1 sits one blank line below the preamble.
602
+ cell_line_offset = (
603
+ _render_preamble(self._project_id).count("\n") + 1
604
+ )
605
+ runner = self._runner or PythonExecRunner(
606
+ artifacts_root=str(self._root / "harvest"))
607
+ try:
608
+ result = runner.run(
609
+ source,
610
+ run_id=run_id,
611
+ timeout_s=record["timeout_s"],
612
+ artifacts=(OPS_LOG_NAME,) + tuple(record["artifacts"]),
613
+ on_live=on_live,
614
+ is_cancelled=lambda: self._cancelled(run_id),
615
+ cell_name=record["source_file"] or CELL_ENTRY,
616
+ cell_line_offset=cell_line_offset,
617
+ )
618
+ finally:
619
+ with self._cancel_lock:
620
+ self._cancel_flags.discard(run_id)
621
+ ops, skipped = self._load_harvested_ops(result)
622
+ op_results = self._replay(ops)
623
+ run_ids = [
624
+ item["result"]["run_id"] for item in op_results
625
+ if item["ok"] and isinstance(item.get("result"), dict)
626
+ and isinstance(item["result"].get("run_id"), str)
627
+ ]
628
+ replayed = self._dispatcher is not None
629
+ effects = {
630
+ "ops": ops,
631
+ "op_results": op_results,
632
+ "ops_ok": all(item["ok"] for item in op_results) if replayed else not ops,
633
+ "ops_skipped": skipped,
634
+ "run_ids": run_ids,
635
+ "replayed": replayed,
636
+ }
637
+ verdict = not_run_verdict(self._verdict_reason(result, record))
638
+ terminal = {
639
+ "execution_id": execution_id,
640
+ "run_id": run_id,
641
+ "mode": record["mode"],
642
+ "source_file": record["source_file"],
643
+ "code_sha256": record["code_sha256"],
644
+ "state": result["state"],
645
+ "ok": bool(result["ok"]),
646
+ "reason": result.get("reason"),
647
+ "exit_code": result.get("exit_code"),
648
+ "stdout": result.get("stdout", ""),
649
+ "stderr": result.get("stderr", ""),
650
+ "partial": bool(result.get("partial", False)),
651
+ "exception": result.get("exception"),
652
+ "effects": effects,
653
+ "artifacts": result.get("artifacts", []),
654
+ "rejected_artifacts": result.get("rejected_artifacts", []),
655
+ "network_isolated": result.get("network_isolated", False),
656
+ "credential_revoked": result.get("credential_revoked", False),
657
+ "tree_killed": result.get("tree_killed", False),
658
+ "spawned": True,
659
+ "timeout_s": record["timeout_s"],
660
+ "verdict": verdict,
661
+ }
662
+ self._persist_execution(terminal)
663
+ self._records[run_id] = dict(terminal)
664
+ if result["state"] == "cancelled":
665
+ self._execution.cancel(run_id)
666
+ else:
667
+ self._execution.mark_completed(run_id, verdict)
668
+ return terminal
669
+
670
+
671
+ def _store_cancelled(self, run_id, execution_id, spawned, reason):
672
+ record = next(
673
+ item for item in self._approvals.values()
674
+ if item["execution_id"] == execution_id
675
+ )
676
+ verdict = not_run_verdict(reason + " Not a check verdict.")
677
+ terminal = {
678
+ "execution_id": execution_id,
679
+ "run_id": run_id,
680
+ "mode": record["mode"],
681
+ "source_file": record["source_file"],
682
+ "code_sha256": record["code_sha256"],
683
+ "state": "cancelled",
684
+ "ok": False,
685
+ "reason": reason,
686
+ "exit_code": None,
687
+ "stdout": "",
688
+ "stderr": "",
689
+ "partial": False,
690
+ "exception": None,
691
+ "effects": {
692
+ "ops": [], "op_results": [], "ops_ok": True,
693
+ "ops_skipped": 0, "run_ids": [], "replayed": True,
694
+ },
695
+ "artifacts": [],
696
+ "rejected_artifacts": [],
697
+ "network_isolated": False,
698
+ "credential_revoked": False,
699
+ "tree_killed": False,
700
+ "spawned": spawned,
701
+ "timeout_s": record["timeout_s"],
702
+ "verdict": verdict,
703
+ }
704
+ self._persist_execution(terminal)
705
+ self._records[run_id] = dict(terminal)
706
+ return terminal
707
+
708
+ @staticmethod
709
+ def _verdict_reason(result, record):
710
+ state = result["state"]
711
+ if state == "completed":
712
+ return (
713
+ "python_execute is not a check: exit 0 is not PASS; "
714
+ "read the execution record (state/ok/effects)."
715
+ )
716
+ if state == "failed":
717
+ exc = result.get("exception") or {}
718
+ where = exc.get("location") or "unknown location"
719
+ what = exc.get("type") or "error"
720
+ return (
721
+ f"Script failed ({what} at {where}; "
722
+ f"exit {result.get('exit_code')}); not a check verdict."
723
+ )
724
+ if state == "timeout":
725
+ return (
726
+ f"Timeout after {record['timeout_s']}s; partials retained; "
727
+ "not a check verdict."
728
+ )
729
+ if state == "crashed":
730
+ return "Worker died by signal; not a check verdict."
731
+ return "Cancelled via the job surface; not a check verdict."
732
+
733
+ def _cancelled(self, run_id):
734
+ with self._cancel_lock:
735
+ return run_id in self._cancel_flags
736
+
737
+ @staticmethod
738
+ def _load_harvested_ops(result):
739
+ ops, skipped = [], 0
740
+ for entry in result.get("artifacts", []):
741
+ if entry.get("name") != OPS_LOG_NAME:
742
+ continue
743
+ try:
744
+ lines = Path(entry["path"]).read_text(
745
+ encoding="utf-8").splitlines()
746
+ except OSError:
747
+ skipped += 1
748
+ continue
749
+ for line in lines:
750
+ if not line.strip():
751
+ continue
752
+ try:
753
+ item = json.loads(line)
754
+ except ValueError:
755
+ skipped += 1
756
+ continue
757
+ if (
758
+ not isinstance(item, dict)
759
+ or not isinstance(item.get("operation"), str)
760
+ or not isinstance(item.get("arguments"), dict)
761
+ ):
762
+ skipped += 1
763
+ continue
764
+ ops.append({"operation": item["operation"],
765
+ "arguments": dict(item["arguments"])})
766
+ return ops, skipped
767
+
768
+ def _replay(self, ops):
769
+ if self._dispatcher is None:
770
+ return []
771
+ replayed = []
772
+ for op in ops:
773
+ try:
774
+ payload = self._dispatcher.execute(
775
+ op["operation"], op["arguments"]
776
+ )
777
+ except Exception as exc: # per-op truth: record, never raise
778
+ replayed.append({
779
+ "operation": op["operation"],
780
+ "ok": False,
781
+ "error": {
782
+ "code": getattr(exc, "code", None)
783
+ or "internal_error",
784
+ "message": str(exc),
785
+ },
786
+ })
787
+ continue
788
+ replayed.append({
789
+ "operation": op["operation"],
790
+ "ok": True,
791
+ "result": json.loads(json.dumps(payload, default=str)),
792
+ })
793
+ return replayed
794
+
795
+ def cancel(self, run_id):
796
+ """Cancel a queued or running run through the job surface.
797
+
798
+ The cooperative flag kills a live worker tree promptly;
799
+ finished runs keep their terminal state, truthfully.
800
+
801
+ Args:
802
+ run_id: Admitted execution id to cancel.
803
+
804
+ Returns:
805
+ Mapping with run_id and state.
806
+
807
+ Raises:
808
+ ExecutionError: unknown_run for an id never admitted.
809
+ """
810
+ with self._cancel_lock:
811
+ self._cancel_flags.add(run_id)
812
+ return self._execution.cancel(run_id)
813
+
814
+ def status(self, run_id):
815
+ """Read one run's job state, verdict, and evidence links.
816
+
817
+ Pure job surface: approval states never appear here — read
818
+ them from approval_status instead.
819
+
820
+ Args:
821
+ run_id: Admitted execution id to inspect.
822
+
823
+ Returns:
824
+ Run state mapping with run_id, operation, state,
825
+ snapshot_id, and verdict.
826
+
827
+ Raises:
828
+ ExecutionError: unknown_run for an id never admitted.
829
+ """
830
+ return self._execution.status(run_id)
831
+
832
+ def execution_record(self, run_id):
833
+ """Read one run's terminal execution record, when finished.
834
+
835
+ Args:
836
+ run_id: Admitted execution id to inspect.
837
+
838
+ Returns:
839
+ Terminal record copy, or None while unfinished.
840
+ """
841
+ record = self._records.get(run_id)
842
+ return dict(record) if record is not None else None
843
+
844
+ def console_view(self):
845
+ """Read generation, environment, and pending approvals.
846
+
847
+ Args:
848
+ None.
849
+
850
+ Returns:
851
+ Console view with project_id, generation, env_digest,
852
+ history cursor, pending approval count, and the
853
+ pending approval ids.
854
+ """
855
+ self._sweep_expired()
856
+ pending = [
857
+ item["approval_id"] for item in self._approvals.values()
858
+ if item["state"] == "pending_approval"
859
+ ]
860
+ return {
861
+ "project_id": self._project_id,
862
+ "generation": self._generation,
863
+ "env_digest": self._env_digest,
864
+ "cursor": self._cursor,
865
+ "pending_approvals": len(pending),
866
+ "pending_approval_ids": pending,
867
+ "executions": self._cursor,
868
+ }
869
+
870
+ def reset(self):
871
+ """Retire the generation; lapse pending, cancel queued.
872
+
873
+ Pending approvals expire (their generation is gone);
874
+ approved-but-unrun jobs cancel through the job surface.
875
+ Live workers are cancel territory, not reset: interrupting
876
+ one needs cancel(run_id), never reset.
877
+
878
+ Returns:
879
+ Mapping with the fresh generation id.
880
+ """
881
+ self._generation = uuid.uuid4().hex
882
+ for record in self._approvals.values():
883
+ if record["state"] == "pending_approval":
884
+ record["state"] = "expired"
885
+ record["reason"] = "Approval generation retired; request again."
886
+ self._persist_approval(record)
887
+ elif record["state"] == "approved" and record["run_id"] is not None:
888
+ run_id = record["run_id"]
889
+ if run_id not in self._records:
890
+ with self._cancel_lock:
891
+ self._cancel_flags.add(run_id)
892
+ try:
893
+ self._execution.cancel(run_id)
894
+ except ExecutionError:
895
+ pass
896
+ self._reap_terminal()
897
+ return {"generation": self._generation}