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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
"""Stdio console adapter: MATLAB-esque script console over shared dispatch.
|
|
2
|
+
|
|
3
|
+
A REPL gateway onto the shared op dispatch: cells are Python with the
|
|
4
|
+
engine world prebound (`project/tools/pdk/gf/np/plot`), executed one
|
|
5
|
+
per fresh WORKERS worker with zero setup, while every `tools.<op>`
|
|
6
|
+
call inside a cell compiles to the same op call the batch command
|
|
7
|
+
surface runs — console history replays as command scripts by
|
|
8
|
+
construction. This adapter faces the use-case layer (`execution`
|
|
9
|
+
dispatch, console approvals, workers) directly; it never calls
|
|
10
|
+
another adapter, and no adapter calls it.
|
|
11
|
+
|
|
12
|
+
Framing (what the IDE xterm panel consumes): the interactive loop
|
|
13
|
+
reads `submit` + cell lines until `:end` (multiline cells),
|
|
14
|
+
`submit-file PATH` (script-file cells), `history`, `reset`, `help`,
|
|
15
|
+
`quit`. Human frames print the cell id, verbatim worker stdout,
|
|
16
|
+
harvested op calls as replayable command lines, then the terminal
|
|
17
|
+
execution line. `--json` prints one stable envelope per command.
|
|
18
|
+
|
|
19
|
+
History persists under the console root (`console.yaml`,
|
|
20
|
+
`cells/<execution_id>.py` holding the exact displayed cell,
|
|
21
|
+
`executions/<execution_id>.yaml` holding the terminal record):
|
|
22
|
+
reopening the same root continues numbering. Single console per
|
|
23
|
+
panel: one live handle per root, no tabs, no console-id multiplexing.
|
|
24
|
+
|
|
25
|
+
Approvals are reused from `execution.console`, never reimplemented:
|
|
26
|
+
a human Run requests and admits approval of the exact displayed
|
|
27
|
+
cell in one step (identical binding). Runs are serial and each
|
|
28
|
+
worker run already revokes its credential and confirms its tree-kill
|
|
29
|
+
before its terminal record, so `reset` retires the generation and
|
|
30
|
+
kills every outstanding approval; interrupting a live run is cancel
|
|
31
|
+
territory, not reset. Worker-staged apply intents land through
|
|
32
|
+
`Engine.apply_batch` as one atomic batch per cell.
|
|
33
|
+
|
|
34
|
+
Local-only trust: this console speaks stdio to the local IDE panel
|
|
35
|
+
under OS identity. There is no token check, login, network listener,
|
|
36
|
+
remote execution, or second user to protect against — and deliberately
|
|
37
|
+
no code for any of those. See TRUST_NOTE.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import hashlib
|
|
41
|
+
import io
|
|
42
|
+
import json
|
|
43
|
+
import shlex
|
|
44
|
+
import sys
|
|
45
|
+
import threading
|
|
46
|
+
import uuid
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
from ruamel.yaml import YAML
|
|
50
|
+
|
|
51
|
+
from ..clients.runtime import RuntimeClient
|
|
52
|
+
from ..composition import Engine
|
|
53
|
+
from ..execution.console.apply_session import ConsoleSession
|
|
54
|
+
from ..execution.pyexec.apply_context import session_prelude
|
|
55
|
+
from ..execution.routing.calls import (
|
|
56
|
+
collect_run_ids,
|
|
57
|
+
is_tier2,
|
|
58
|
+
stamp_tier2_call,
|
|
59
|
+
)
|
|
60
|
+
from ..execution.dispatch import OpDispatcher
|
|
61
|
+
from ..execution.errors import ExecutionError, UsageError
|
|
62
|
+
from .errors import error_payload
|
|
63
|
+
from ..execution.worker import CellRunner
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"CELL_PRELUDE_TEMPLATE",
|
|
67
|
+
"EXIT_OK",
|
|
68
|
+
"EXIT_USAGE",
|
|
69
|
+
"LOCAL_ONLY_TRUST",
|
|
70
|
+
"OPS_LOG_NAME",
|
|
71
|
+
"PREBOUND_NAMES",
|
|
72
|
+
"PRELUDE_DIGEST",
|
|
73
|
+
"TRUST_NOTE",
|
|
74
|
+
"USAGE",
|
|
75
|
+
"StdioConsole",
|
|
76
|
+
"build_cell_source",
|
|
77
|
+
"format_op_call",
|
|
78
|
+
"main",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
EXIT_OK = 0
|
|
82
|
+
EXIT_USAGE = 2
|
|
83
|
+
|
|
84
|
+
LOCAL_ONLY_TRUST = True
|
|
85
|
+
|
|
86
|
+
TRUST_NOTE = (
|
|
87
|
+
"Local-only trust: this console speaks stdio to the local IDE panel "
|
|
88
|
+
"under OS identity, with no auth step because there is no second party "
|
|
89
|
+
"— no tokens, no broker, no listener, no remote execution, no multi-user. "
|
|
90
|
+
"Human Run on the displayed cell is the approval; sandboxing (not "
|
|
91
|
+
"identity) is the safety boundary."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
PREBOUND_NAMES = ("project", "tools", "pdk", "gf", "np", "plot")
|
|
95
|
+
|
|
96
|
+
OPS_LOG_NAME = "__console_ops.jsonl"
|
|
97
|
+
|
|
98
|
+
CELL_PRELUDE_TEMPLATE = '''\
|
|
99
|
+
# --- rocket-console prelude: pinned trusted bootstrap (do not edit) ---
|
|
100
|
+
import json as _json
|
|
101
|
+
|
|
102
|
+
__CONSOLE_PROJECT__ = @PROJECT_ID@
|
|
103
|
+
__CONSOLE_OPS_LOG__ = "__console_ops.jsonl"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _OpRecorder:
|
|
107
|
+
"""Prebound `tools`: tools.<op>(**kwargs) records one op call.
|
|
108
|
+
|
|
109
|
+
Calls append {"operation", "arguments"} JSON lines to the worker
|
|
110
|
+
scratch ops log; the host replays each through the shared op
|
|
111
|
+
dispatch after the worker exits, so validation and execution are
|
|
112
|
+
exactly what the batch surface runs. Positional args are rejected:
|
|
113
|
+
op calls are keyword-shaped.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
self._index = 0
|
|
118
|
+
|
|
119
|
+
def __getattr__(self, operation):
|
|
120
|
+
def call(*args, **kwargs):
|
|
121
|
+
if args:
|
|
122
|
+
raise TypeError(
|
|
123
|
+
"op calls take keyword arguments only: %s." % operation
|
|
124
|
+
)
|
|
125
|
+
entry = {"operation": operation, "arguments": dict(kwargs)}
|
|
126
|
+
with open(__CONSOLE_OPS_LOG__, "a", encoding="utf-8") as handle:
|
|
127
|
+
handle.write(_json.dumps(entry, sort_keys=True) + "\\n")
|
|
128
|
+
self._index += 1
|
|
129
|
+
return {"state": "recorded", "operation": operation,
|
|
130
|
+
"index": self._index}
|
|
131
|
+
|
|
132
|
+
return call
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
tools = _OpRecorder()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _Project:
|
|
139
|
+
"""Prebound `project`: local identity plus query recording."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, project_id):
|
|
142
|
+
self.id = project_id
|
|
143
|
+
|
|
144
|
+
def top_ref(self):
|
|
145
|
+
return {"project": self.id, "ref": "top"}
|
|
146
|
+
|
|
147
|
+
def snapshot(self):
|
|
148
|
+
return tools.get_project_snapshot()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
project = _Project(__CONSOLE_PROJECT__)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class _Pdk:
|
|
155
|
+
"""Prebound `pdk`: worker-local view (discovery runs on the host)."""
|
|
156
|
+
|
|
157
|
+
def describe(self):
|
|
158
|
+
return {"project": project.id, "profile": "local",
|
|
159
|
+
"note": "worker-local view;"
|
|
160
|
+
" engine catalog discovery runs on the host."}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
pdk = _Pdk()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class _MissingBinding:
|
|
167
|
+
"""Honest placeholder: missing profiles report, never fake."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, name, hint):
|
|
170
|
+
self._name = name
|
|
171
|
+
self._hint = hint
|
|
172
|
+
|
|
173
|
+
def __repr__(self):
|
|
174
|
+
return "<%s unavailable in this worker profile: %s>" % (
|
|
175
|
+
self._name, self._hint)
|
|
176
|
+
|
|
177
|
+
def __getattr__(self, item):
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"%s unavailable in this worker profile: %s"
|
|
180
|
+
% (self._name, self._hint)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def __call__(self, *args, **kwargs):
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"%s unavailable in this worker profile: %s"
|
|
186
|
+
% (self._name, self._hint)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
import gdsfactory as gf
|
|
192
|
+
except Exception as _gf_error:
|
|
193
|
+
gf = _MissingBinding("gf", "gdsfactory not installed (%s)" % _gf_error)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
import numpy as np
|
|
197
|
+
except Exception as _np_error:
|
|
198
|
+
np = _MissingBinding("np", "numpy not installed (%s)" % _np_error)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _Plot:
|
|
202
|
+
"""Prebound `plot`: availability flag plus figure helper."""
|
|
203
|
+
|
|
204
|
+
def __init__(self):
|
|
205
|
+
try:
|
|
206
|
+
import matplotlib
|
|
207
|
+
|
|
208
|
+
matplotlib.use("Agg")
|
|
209
|
+
from matplotlib import pyplot as _pyplot
|
|
210
|
+
except Exception as _plot_error:
|
|
211
|
+
self.available = False
|
|
212
|
+
self._error = str(_plot_error)
|
|
213
|
+
self._pyplot = None
|
|
214
|
+
else:
|
|
215
|
+
self.available = True
|
|
216
|
+
self._error = ""
|
|
217
|
+
self._pyplot = _pyplot
|
|
218
|
+
|
|
219
|
+
def __repr__(self):
|
|
220
|
+
if self.available:
|
|
221
|
+
return "<plot (matplotlib available)>"
|
|
222
|
+
return "<plot unavailable in this worker profile: %s>" % self._error
|
|
223
|
+
|
|
224
|
+
def figure(self, *args, **kwargs):
|
|
225
|
+
if not self.available:
|
|
226
|
+
raise RuntimeError(
|
|
227
|
+
"plot unavailable in this worker profile: %s" % self._error
|
|
228
|
+
)
|
|
229
|
+
return self._pyplot.figure(*args, **kwargs)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
plot = _Plot()
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
PRELUDE_DIGEST = hashlib.sha256(
|
|
236
|
+
CELL_PRELUDE_TEMPLATE.encode("utf-8")
|
|
237
|
+
).hexdigest()
|
|
238
|
+
|
|
239
|
+
USAGE = """usage: rocket-console [--project ID] [--actor NAME] [--root DIR] [--json]
|
|
240
|
+
|
|
241
|
+
MATLAB-esque script console, stdio-only (local IDE panel). One console
|
|
242
|
+
per panel: no tabs, no multiplexing. Commands, one per line; blank
|
|
243
|
+
lines and `#` comments are skipped:
|
|
244
|
+
|
|
245
|
+
submit read cell lines until `:end`, then run them
|
|
246
|
+
submit-file PATH run a script file as one cell (history keeps a copy)
|
|
247
|
+
history list persisted executions
|
|
248
|
+
reset retire the generation, kill outstanding approvals
|
|
249
|
+
help print this usage
|
|
250
|
+
quit leave the console (EOF also leaves)
|
|
251
|
+
|
|
252
|
+
Every cell runs in a fresh worker with project/tools/pdk/gf/np/plot
|
|
253
|
+
prebound and zero setup. tools.<op> calls replay through the shared
|
|
254
|
+
op dispatch after the worker exits — the printed op lines rerun as a
|
|
255
|
+
batch script. --json prints one envelope per command instead.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
_yaml = YAML(typ="safe")
|
|
259
|
+
|
|
260
|
+
_LIVE = set()
|
|
261
|
+
_LIVE_LOCK = threading.Lock()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _render_preamble(project_id):
|
|
265
|
+
return CELL_PRELUDE_TEMPLATE.replace(
|
|
266
|
+
"@PROJECT_ID@", json.dumps(project_id)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def build_cell_source(code, project_id="default", console_id=None,
|
|
271
|
+
generation=None):
|
|
272
|
+
"""Render the worker source: pinned preamble plus the display cell.
|
|
273
|
+
|
|
274
|
+
The project binding renders into the preamble template first, so
|
|
275
|
+
display text is concatenated verbatim afterwards — cell bytes that
|
|
276
|
+
happen to look like the slot stay data. The result ends with the
|
|
277
|
+
exact displayed cell (trailing newline normalized). When a console
|
|
278
|
+
binding is supplied, the apply-context prelude binds between
|
|
279
|
+
preamble and display, so worker cells may stage intents
|
|
280
|
+
(`session.cell(...)`, `session.rect(...)`) that the host lands
|
|
281
|
+
through `Engine.apply_batch`; without it the source is
|
|
282
|
+
byte-identical to preamble plus display.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
code: Display cell text to wrap.
|
|
286
|
+
project_id: Project id rendered into the preamble binding.
|
|
287
|
+
console_id: Console id bound into the apply-context prelude;
|
|
288
|
+
no prelude binds when console_id and generation are both
|
|
289
|
+
omitted.
|
|
290
|
+
generation: Session generation bound into the apply-context
|
|
291
|
+
prelude for replay fencing.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Worker source: pinned preamble, optional apply binding, plus
|
|
295
|
+
the display cell.
|
|
296
|
+
"""
|
|
297
|
+
display = code if code.endswith("\n") else code + "\n"
|
|
298
|
+
source = _render_preamble(project_id) + "\n"
|
|
299
|
+
if console_id is not None or generation is not None:
|
|
300
|
+
source += (
|
|
301
|
+
"__PYEXEC_OPS_LOG__ = __CONSOLE_OPS_LOG__\n"
|
|
302
|
+
+ session_prelude(project_id, console_id or "local",
|
|
303
|
+
generation or "local")
|
|
304
|
+
+ "\n"
|
|
305
|
+
)
|
|
306
|
+
return source + display
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def format_op_call(operation, arguments):
|
|
310
|
+
"""Format one op call as a batch-surface command line (replayable).
|
|
311
|
+
|
|
312
|
+
Values render as JSON (a YAML subset, so types round-trip through
|
|
313
|
+
the one parser of record) and the whole `key=value` token is shell
|
|
314
|
+
quoted when needed. Argument order is sorted for stability.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
operation: Cataloged operation or query name.
|
|
318
|
+
arguments: Operation arguments rendered as key=value pairs.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Replayable batch-surface command line for the op call.
|
|
322
|
+
"""
|
|
323
|
+
parts = [operation]
|
|
324
|
+
for key in sorted(arguments):
|
|
325
|
+
value_text = json.dumps(arguments[key], sort_keys=True, default=str)
|
|
326
|
+
parts.append(shlex.quote("%s=%s" % (key, value_text)))
|
|
327
|
+
return " ".join(parts)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _dump_yaml(data):
|
|
331
|
+
buf = io.StringIO()
|
|
332
|
+
_yaml.dump(data, buf)
|
|
333
|
+
return buf.getvalue()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _load_yaml(text):
|
|
337
|
+
return _yaml.load(text) or {}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class StdioConsole:
|
|
341
|
+
"""One stdio console: cells in fresh workers, ops via dispatch.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
engine: Engine ops replay against; constructed from
|
|
345
|
+
project_id/actor when omitted.
|
|
346
|
+
project_id: Project id stamped on every command.
|
|
347
|
+
actor: Actor stamped on every command.
|
|
348
|
+
root: Console state dir; defaults under cwd `.rocket/`.
|
|
349
|
+
cell_runner: Worker runner; constructed when omitted.
|
|
350
|
+
expected_revision: Revision cells bind approvals against.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, engine=None, project_id="default", actor="console",
|
|
354
|
+
root=None, cell_runner=None, expected_revision="local"):
|
|
355
|
+
self._engine = (
|
|
356
|
+
engine if engine is not None else Engine(project_id, actor=actor)
|
|
357
|
+
)
|
|
358
|
+
self._project_id = project_id
|
|
359
|
+
self._actor = actor
|
|
360
|
+
self._expected_revision = expected_revision
|
|
361
|
+
self._root = (
|
|
362
|
+
Path(root) if root is not None
|
|
363
|
+
else Path.cwd() / ".rocket" / "consoles" / "stdio"
|
|
364
|
+
)
|
|
365
|
+
key = str(self._root.resolve())
|
|
366
|
+
with _LIVE_LOCK:
|
|
367
|
+
if key in _LIVE:
|
|
368
|
+
raise ExecutionError(
|
|
369
|
+
"console_busy",
|
|
370
|
+
f"One console per panel: {self._root} is already open.",
|
|
371
|
+
)
|
|
372
|
+
_LIVE.add(key)
|
|
373
|
+
self._live_key = key
|
|
374
|
+
try:
|
|
375
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
376
|
+
(self._root / "cells").mkdir(exist_ok=True)
|
|
377
|
+
(self._root / "executions").mkdir(exist_ok=True)
|
|
378
|
+
self._dispatch = OpDispatcher(
|
|
379
|
+
self._engine, project_id=project_id, actor=actor
|
|
380
|
+
)
|
|
381
|
+
self._runner = (
|
|
382
|
+
cell_runner if cell_runner is not None
|
|
383
|
+
else CellRunner(artifacts_root=str(self._root / "harvest"))
|
|
384
|
+
)
|
|
385
|
+
self._env_digest = hashlib.sha256(
|
|
386
|
+
_render_preamble(project_id).encode("utf-8")
|
|
387
|
+
).hexdigest()
|
|
388
|
+
self._load_or_create()
|
|
389
|
+
except Exception:
|
|
390
|
+
self.close()
|
|
391
|
+
raise
|
|
392
|
+
|
|
393
|
+
def _load_or_create(self):
|
|
394
|
+
console_file = self._root / "console.yaml"
|
|
395
|
+
if console_file.is_file():
|
|
396
|
+
data = _load_yaml(console_file.read_text(encoding="utf-8"))
|
|
397
|
+
self._console_id = data.get("console_id") or uuid.uuid4().hex
|
|
398
|
+
self._generation = data.get("generation") or uuid.uuid4().hex
|
|
399
|
+
else:
|
|
400
|
+
self._console_id = uuid.uuid4().hex
|
|
401
|
+
self._generation = uuid.uuid4().hex
|
|
402
|
+
self._session = ConsoleSession(
|
|
403
|
+
self._project_id, self._actor, console_id=self._console_id)
|
|
404
|
+
self._session.generation = self._generation
|
|
405
|
+
self._cursor = len(list((self._root / "executions").glob("*.yaml")))
|
|
406
|
+
self._write_console_yaml()
|
|
407
|
+
|
|
408
|
+
def _write_console_yaml(self):
|
|
409
|
+
(self._root / "console.yaml").write_text(
|
|
410
|
+
_dump_yaml({
|
|
411
|
+
"console_id": self._console_id,
|
|
412
|
+
"generation": self._generation,
|
|
413
|
+
"project_id": self._project_id,
|
|
414
|
+
"actor": self._actor,
|
|
415
|
+
"prelude_digest": PRELUDE_DIGEST,
|
|
416
|
+
"env_digest": self._env_digest,
|
|
417
|
+
"executions": self._cursor,
|
|
418
|
+
}),
|
|
419
|
+
encoding="utf-8",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def console_id(self):
|
|
424
|
+
"""Stable console id, persisted across restarts.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Console id string.
|
|
428
|
+
"""
|
|
429
|
+
return self._console_id
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def generation(self):
|
|
433
|
+
"""Current approval generation; reset retires it.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Generation id string.
|
|
437
|
+
"""
|
|
438
|
+
return self._generation
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def cursor(self):
|
|
442
|
+
"""History cursor: count of persisted executions.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Execution count as an int.
|
|
446
|
+
"""
|
|
447
|
+
return self._cursor
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def root(self):
|
|
451
|
+
"""Console state directory.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Root path string.
|
|
455
|
+
"""
|
|
456
|
+
return str(self._root)
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def engine(self):
|
|
460
|
+
"""Engine ops replay against.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Bound engine.
|
|
464
|
+
"""
|
|
465
|
+
return self._engine
|
|
466
|
+
|
|
467
|
+
def runtime_client(self):
|
|
468
|
+
"""Serve this console's engine through the shared client.
|
|
469
|
+
|
|
470
|
+
Cell execution is untouched — this binds the same
|
|
471
|
+
engine's integrated runtime (deferred submit, pump,
|
|
472
|
+
wait, cancel, watch) so console clients share one run
|
|
473
|
+
identity with every other transport.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
RuntimeClient bound to this console's engine.
|
|
477
|
+
"""
|
|
478
|
+
return RuntimeClient(self._engine)
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def gate(self):
|
|
482
|
+
"""Execution-scoped credential gate of the worker runner.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Credential gate.
|
|
486
|
+
"""
|
|
487
|
+
return self._runner.gate
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def admission(self):
|
|
491
|
+
"""Worker isolation admission record.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Admission mapping copy.
|
|
495
|
+
"""
|
|
496
|
+
return dict(self._runner.admission)
|
|
497
|
+
|
|
498
|
+
def _cell_source(self, display):
|
|
499
|
+
"""Render the worker source bound to this console generation.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
display: Normalized display cell text.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Full worker source with preamble plus the apply-context
|
|
506
|
+
binding for this console and generation.
|
|
507
|
+
"""
|
|
508
|
+
return build_cell_source(
|
|
509
|
+
display, project_id=self._project_id,
|
|
510
|
+
console_id=self._console_id, generation=self._generation,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def _normalize(code):
|
|
515
|
+
if not isinstance(code, str):
|
|
516
|
+
raise ExecutionError(
|
|
517
|
+
"invalid_cell", "Cell code must be UTF-8 text."
|
|
518
|
+
)
|
|
519
|
+
if not code.strip():
|
|
520
|
+
raise ExecutionError("empty_cell", "Cell is empty.")
|
|
521
|
+
return code if code.endswith("\n") else code + "\n"
|
|
522
|
+
|
|
523
|
+
def request_approval(self, code):
|
|
524
|
+
"""Approve-later path: bind an approval to one display cell.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
code: Display cell text to bind.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Approval record with approval_id and pending state.
|
|
531
|
+
|
|
532
|
+
Raises:
|
|
533
|
+
ExecutionError: invalid_cell for non-text code,
|
|
534
|
+
empty_cell for a blank cell.
|
|
535
|
+
"""
|
|
536
|
+
display = self._normalize(code)
|
|
537
|
+
full = self._cell_source(display)
|
|
538
|
+
return self._session.request_cell_approval(
|
|
539
|
+
code=full, history_cursor=self._cursor,
|
|
540
|
+
env_digest=self._env_digest,
|
|
541
|
+
expected_revision=self._expected_revision,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def run_approved(self, approval_id, code, artifacts=(), timeout_s=None,
|
|
545
|
+
on_live=None):
|
|
546
|
+
"""Run a cell against an earlier approval (tamper raises).
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
approval_id: Approval bound by request_approval.
|
|
550
|
+
code: Display cell text; must match the binding.
|
|
551
|
+
artifacts: Scratch-relative paths to harvest.
|
|
552
|
+
timeout_s: Worker budget in seconds; runner default
|
|
553
|
+
when omitted.
|
|
554
|
+
on_live: Optional callback while the worker is live.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Persisted execution record with state, stdio, ops
|
|
558
|
+
replay, and artifacts.
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
ExecutionError: approval_invalid when the code drifts
|
|
562
|
+
from its binding, invalid_cell/empty_cell for bad
|
|
563
|
+
code.
|
|
564
|
+
"""
|
|
565
|
+
display = self._normalize(code)
|
|
566
|
+
full = self._cell_source(display)
|
|
567
|
+
return self._run_full(approval_id, full, display,
|
|
568
|
+
{"source_file": None}, artifacts, timeout_s,
|
|
569
|
+
on_live)
|
|
570
|
+
|
|
571
|
+
def submit(self, code, artifacts=(), timeout_s=None, on_live=None):
|
|
572
|
+
"""Human-Run path: approve and run the exact displayed cell.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
code: Display cell text to approve and run.
|
|
576
|
+
artifacts: Scratch-relative paths to harvest.
|
|
577
|
+
timeout_s: Worker budget in seconds; runner default
|
|
578
|
+
when omitted.
|
|
579
|
+
on_live: Optional callback while the worker is live.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Persisted execution record with state, stdio, ops
|
|
583
|
+
replay, and artifacts.
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
ExecutionError: invalid_cell for non-text code,
|
|
587
|
+
empty_cell for a blank cell.
|
|
588
|
+
"""
|
|
589
|
+
display = self._normalize(code)
|
|
590
|
+
full = self._cell_source(display)
|
|
591
|
+
approval = self._session.request_cell_approval(
|
|
592
|
+
code=full, history_cursor=self._cursor,
|
|
593
|
+
env_digest=self._env_digest,
|
|
594
|
+
expected_revision=self._expected_revision,
|
|
595
|
+
)
|
|
596
|
+
return self._run_full(approval["approval_id"], full, display,
|
|
597
|
+
{"source_file": None}, artifacts, timeout_s,
|
|
598
|
+
on_live)
|
|
599
|
+
|
|
600
|
+
def submit_file(self, path, artifacts=(), timeout_s=None, on_live=None):
|
|
601
|
+
"""Run a script file as one cell; history keeps its own copy.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
path: Script file path to run as one cell.
|
|
605
|
+
artifacts: Scratch-relative paths to harvest.
|
|
606
|
+
timeout_s: Worker budget in seconds; runner default
|
|
607
|
+
when omitted.
|
|
608
|
+
on_live: Optional callback while the worker is live.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Persisted execution record with state, stdio, ops
|
|
612
|
+
replay, and artifacts.
|
|
613
|
+
|
|
614
|
+
Raises:
|
|
615
|
+
ExecutionError: cell_unreadable when the file cannot
|
|
616
|
+
be read, invalid_cell/empty_cell for bad content.
|
|
617
|
+
"""
|
|
618
|
+
try:
|
|
619
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
620
|
+
except OSError as exc:
|
|
621
|
+
raise ExecutionError(
|
|
622
|
+
"cell_unreadable", f"Cannot read cell file {path}: {exc}."
|
|
623
|
+
) from exc
|
|
624
|
+
display = self._normalize(text)
|
|
625
|
+
full = self._cell_source(display)
|
|
626
|
+
approval = self._session.request_cell_approval(
|
|
627
|
+
code=full, history_cursor=self._cursor,
|
|
628
|
+
env_digest=self._env_digest,
|
|
629
|
+
expected_revision=self._expected_revision,
|
|
630
|
+
)
|
|
631
|
+
return self._run_full(approval["approval_id"], full, display,
|
|
632
|
+
{"source_file": str(path)}, artifacts,
|
|
633
|
+
timeout_s, on_live)
|
|
634
|
+
|
|
635
|
+
def _run_full(self, approval_id, full, display, extra, artifacts,
|
|
636
|
+
timeout_s, on_live):
|
|
637
|
+
execution_id = uuid.uuid4().hex
|
|
638
|
+
cell_rel = "cells/%s.py" % execution_id
|
|
639
|
+
(self._root / cell_rel).write_text(display, encoding="utf-8")
|
|
640
|
+
base = {
|
|
641
|
+
"execution_id": execution_id,
|
|
642
|
+
"cell": cell_rel,
|
|
643
|
+
"code_sha256": hashlib.sha256(
|
|
644
|
+
display.encode("utf-8")
|
|
645
|
+
).hexdigest(),
|
|
646
|
+
"history_cursor": self._cursor + 1,
|
|
647
|
+
}
|
|
648
|
+
try:
|
|
649
|
+
result = self._runner.run_cell(
|
|
650
|
+
self._session, approval_id, full,
|
|
651
|
+
history_cursor=self._cursor, env_digest=self._env_digest,
|
|
652
|
+
expected_revision=self._expected_revision,
|
|
653
|
+
artifacts=(OPS_LOG_NAME,) + tuple(artifacts),
|
|
654
|
+
timeout_s=timeout_s, on_live=on_live,
|
|
655
|
+
)
|
|
656
|
+
except ExecutionError as exc:
|
|
657
|
+
if exc.code == "approval_invalid":
|
|
658
|
+
raise
|
|
659
|
+
record = {
|
|
660
|
+
**base,
|
|
661
|
+
"state": "not_run",
|
|
662
|
+
"ok": False,
|
|
663
|
+
"code": exc.code,
|
|
664
|
+
"reason": str(exc),
|
|
665
|
+
"exit_code": None,
|
|
666
|
+
"stdout": "",
|
|
667
|
+
"stderr": "",
|
|
668
|
+
"partial": False,
|
|
669
|
+
"ops": [],
|
|
670
|
+
"op_results": [],
|
|
671
|
+
"ops_ok": True,
|
|
672
|
+
"ops_skipped": 0,
|
|
673
|
+
"apply": None,
|
|
674
|
+
"effects": {"run_ids": [], "replayed": True},
|
|
675
|
+
"cli_script": [],
|
|
676
|
+
"artifacts": [],
|
|
677
|
+
"rejected_artifacts": [],
|
|
678
|
+
"network_isolated": self.admission.get(
|
|
679
|
+
"network_isolated", False),
|
|
680
|
+
"run_id": None,
|
|
681
|
+
**extra,
|
|
682
|
+
}
|
|
683
|
+
self._persist(record)
|
|
684
|
+
return record
|
|
685
|
+
ops, skipped = self._load_harvested_ops(result)
|
|
686
|
+
op_results = self._replay(ops)
|
|
687
|
+
apply_entry = self._land_apply_intents(result, execution_id)
|
|
688
|
+
record = {
|
|
689
|
+
**base,
|
|
690
|
+
"state": result["state"],
|
|
691
|
+
"ok": bool(result["ok"]),
|
|
692
|
+
"reason": result.get("reason"),
|
|
693
|
+
"exit_code": result.get("exit_code"),
|
|
694
|
+
"stdout": result.get("stdout", ""),
|
|
695
|
+
"stderr": result.get("stderr", ""),
|
|
696
|
+
"partial": bool(result.get("partial", False)),
|
|
697
|
+
"ops": ops,
|
|
698
|
+
"op_results": op_results,
|
|
699
|
+
"ops_ok": all(item["ok"] for item in op_results),
|
|
700
|
+
"ops_skipped": skipped,
|
|
701
|
+
"apply": apply_entry,
|
|
702
|
+
"effects": {"run_ids": collect_run_ids(op_results),
|
|
703
|
+
"replayed": True},
|
|
704
|
+
"cli_script": [
|
|
705
|
+
format_op_call(op["operation"], op["arguments"]) for op in ops
|
|
706
|
+
],
|
|
707
|
+
"artifacts": result.get("artifacts", []),
|
|
708
|
+
"rejected_artifacts": result.get("rejected_artifacts", []),
|
|
709
|
+
"network_isolated": result.get("network_isolated", False),
|
|
710
|
+
"credential_revoked": result.get("credential_revoked", False),
|
|
711
|
+
"tree_killed": result.get("tree_killed", False),
|
|
712
|
+
"run_id": result.get("run_id"),
|
|
713
|
+
**extra,
|
|
714
|
+
}
|
|
715
|
+
self._persist(record)
|
|
716
|
+
return record
|
|
717
|
+
|
|
718
|
+
def _persist(self, record):
|
|
719
|
+
stored = {
|
|
720
|
+
**record,
|
|
721
|
+
"console_id": self._console_id,
|
|
722
|
+
"generation": self._generation,
|
|
723
|
+
"project_id": self._project_id,
|
|
724
|
+
"prelude_digest": PRELUDE_DIGEST,
|
|
725
|
+
"env_digest": self._env_digest,
|
|
726
|
+
"expected_revision": self._expected_revision,
|
|
727
|
+
}
|
|
728
|
+
(self._root / "executions" / ("%s.yaml" % record["execution_id"])
|
|
729
|
+
).write_text(_dump_yaml(stored), encoding="utf-8")
|
|
730
|
+
self._cursor += 1
|
|
731
|
+
self._write_console_yaml()
|
|
732
|
+
|
|
733
|
+
@staticmethod
|
|
734
|
+
def _load_harvested_ops(result):
|
|
735
|
+
ops, skipped = [], 0
|
|
736
|
+
for entry in result.get("artifacts", []):
|
|
737
|
+
if entry.get("name") != OPS_LOG_NAME:
|
|
738
|
+
continue
|
|
739
|
+
try:
|
|
740
|
+
lines = Path(entry["path"]).read_text(
|
|
741
|
+
encoding="utf-8").splitlines()
|
|
742
|
+
except OSError:
|
|
743
|
+
skipped += 1
|
|
744
|
+
continue
|
|
745
|
+
for line in lines:
|
|
746
|
+
if not line.strip():
|
|
747
|
+
continue
|
|
748
|
+
try:
|
|
749
|
+
item = json.loads(line)
|
|
750
|
+
except ValueError:
|
|
751
|
+
skipped += 1
|
|
752
|
+
continue
|
|
753
|
+
if (
|
|
754
|
+
not isinstance(item, dict)
|
|
755
|
+
or not isinstance(item.get("operation"), str)
|
|
756
|
+
or not isinstance(item.get("arguments"), dict)
|
|
757
|
+
):
|
|
758
|
+
skipped += 1
|
|
759
|
+
continue
|
|
760
|
+
ops.append({"operation": item["operation"],
|
|
761
|
+
"arguments": dict(item["arguments"])})
|
|
762
|
+
return ops, skipped
|
|
763
|
+
|
|
764
|
+
def _replay(self, ops):
|
|
765
|
+
replayed = []
|
|
766
|
+
try:
|
|
767
|
+
head = self._engine.snapshot()["current_revision"]
|
|
768
|
+
except Exception:
|
|
769
|
+
head = None
|
|
770
|
+
for op in ops:
|
|
771
|
+
operation = op["operation"]
|
|
772
|
+
arguments = op["arguments"]
|
|
773
|
+
request_id = None
|
|
774
|
+
if head is not None and is_tier2(operation):
|
|
775
|
+
arguments, request_id = stamp_tier2_call(
|
|
776
|
+
operation, arguments, head
|
|
777
|
+
)
|
|
778
|
+
try:
|
|
779
|
+
payload = self._dispatch.execute(
|
|
780
|
+
operation, arguments, request_id=request_id
|
|
781
|
+
)
|
|
782
|
+
except Exception as exc: # per-op truth: record, never raise
|
|
783
|
+
replayed.append({
|
|
784
|
+
"operation": operation,
|
|
785
|
+
"ok": False,
|
|
786
|
+
"error": {
|
|
787
|
+
"code": getattr(exc, "code", None)
|
|
788
|
+
or "internal_error",
|
|
789
|
+
"message": str(exc),
|
|
790
|
+
},
|
|
791
|
+
})
|
|
792
|
+
continue
|
|
793
|
+
replayed.append({
|
|
794
|
+
"operation": operation,
|
|
795
|
+
"ok": True,
|
|
796
|
+
"result": json.loads(json.dumps(payload, default=str)),
|
|
797
|
+
})
|
|
798
|
+
return replayed
|
|
799
|
+
|
|
800
|
+
def _land_apply_intents(self, result, execution_id):
|
|
801
|
+
"""Land worker-staged apply intents as one atomic batch.
|
|
802
|
+
|
|
803
|
+
The harvested ops log may carry ``{"apply_intent": ...}``
|
|
804
|
+
lines (staged by the apply-context prelude when the cell
|
|
805
|
+
calls `session.cell(...)` or `session.rect(...)`); they
|
|
806
|
+
land through `Engine.apply_batch` exactly like editor
|
|
807
|
+
writes. Op-call replay is untouched — this covers only
|
|
808
|
+
the intent lines. Landing never breaks the cell record:
|
|
809
|
+
a conflict records truthfully beside the completed run.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
result: Terminal worker record with harvested artifacts.
|
|
813
|
+
execution_id: Execution id carried on the batch label.
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
Landing with ok True, the engine receipt, and the
|
|
817
|
+
intent count — or None when nothing was staged or
|
|
818
|
+
the engine cannot land.
|
|
819
|
+
"""
|
|
820
|
+
if getattr(self._engine, "apply_batch", None) is None:
|
|
821
|
+
return None
|
|
822
|
+
for entry in result.get("artifacts", []):
|
|
823
|
+
if entry.get("name") != OPS_LOG_NAME:
|
|
824
|
+
continue
|
|
825
|
+
if not entry.get("path"):
|
|
826
|
+
continue
|
|
827
|
+
try:
|
|
828
|
+
landed = self._session.land_worker_intents(
|
|
829
|
+
self._engine, entry["path"],
|
|
830
|
+
"cell %s" % execution_id,
|
|
831
|
+
)
|
|
832
|
+
except Exception as exc: # landing truth, never a raise
|
|
833
|
+
return {
|
|
834
|
+
"ok": False,
|
|
835
|
+
"error": {
|
|
836
|
+
"code": getattr(exc, "code", None)
|
|
837
|
+
or "internal_error",
|
|
838
|
+
"message": str(exc),
|
|
839
|
+
},
|
|
840
|
+
}
|
|
841
|
+
if landed is not None:
|
|
842
|
+
return landed
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
def history(self):
|
|
846
|
+
"""Persisted executions, oldest first (survives restarts).
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
Execution records sorted by history cursor.
|
|
850
|
+
"""
|
|
851
|
+
records = []
|
|
852
|
+
for path in (self._root / "executions").glob("*.yaml"):
|
|
853
|
+
records.append(_load_yaml(path.read_text(encoding="utf-8")))
|
|
854
|
+
records.sort(key=lambda item: item.get("history_cursor", 0))
|
|
855
|
+
return records
|
|
856
|
+
|
|
857
|
+
def reset(self):
|
|
858
|
+
"""Retire the generation and kill every outstanding approval.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
Mapping with the fresh generation id.
|
|
862
|
+
"""
|
|
863
|
+
out = self._session.reset()
|
|
864
|
+
self._generation = out["generation"]
|
|
865
|
+
self._write_console_yaml()
|
|
866
|
+
return {"generation": self._generation}
|
|
867
|
+
|
|
868
|
+
def close(self):
|
|
869
|
+
"""Release this console's single-panel slot (idempotent)."""
|
|
870
|
+
key = getattr(self, "_live_key", None)
|
|
871
|
+
if key is None:
|
|
872
|
+
return
|
|
873
|
+
with _LIVE_LOCK:
|
|
874
|
+
_LIVE.discard(key)
|
|
875
|
+
self._live_key = None
|
|
876
|
+
|
|
877
|
+
def __enter__(self):
|
|
878
|
+
return self
|
|
879
|
+
|
|
880
|
+
def __exit__(self, *exc_info):
|
|
881
|
+
self.close()
|
|
882
|
+
return False
|
|
883
|
+
|
|
884
|
+
def __del__(self):
|
|
885
|
+
try:
|
|
886
|
+
self.close()
|
|
887
|
+
except Exception:
|
|
888
|
+
pass
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _split_flag(token, name):
|
|
892
|
+
prefix = "--%s=" % name
|
|
893
|
+
if token.startswith(prefix):
|
|
894
|
+
return token[len(prefix):]
|
|
895
|
+
return None
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _parse_argv(argv):
|
|
899
|
+
options = {"json": False, "root": None, "project": "default",
|
|
900
|
+
"actor": "console", "help": False}
|
|
901
|
+
rest = []
|
|
902
|
+
i = 0
|
|
903
|
+
while i < len(argv):
|
|
904
|
+
token = argv[i]
|
|
905
|
+
if token == "--json":
|
|
906
|
+
options["json"] = True
|
|
907
|
+
elif token in ("--help", "-h"):
|
|
908
|
+
options["help"] = True
|
|
909
|
+
elif token == "--root":
|
|
910
|
+
i += 1
|
|
911
|
+
if i >= len(argv):
|
|
912
|
+
raise UsageError(
|
|
913
|
+
"invalid_command", "`--root` needs a directory.")
|
|
914
|
+
options["root"] = argv[i]
|
|
915
|
+
elif token == "--project":
|
|
916
|
+
i += 1
|
|
917
|
+
if i >= len(argv):
|
|
918
|
+
raise UsageError("invalid_command", "`--project` needs an ID.")
|
|
919
|
+
options["project"] = argv[i]
|
|
920
|
+
elif token == "--actor":
|
|
921
|
+
i += 1
|
|
922
|
+
if i >= len(argv):
|
|
923
|
+
raise UsageError("invalid_command", "`--actor` needs a NAME.")
|
|
924
|
+
options["actor"] = argv[i]
|
|
925
|
+
elif _split_flag(token, "root") is not None:
|
|
926
|
+
options["root"] = _split_flag(token, "root")
|
|
927
|
+
elif _split_flag(token, "project") is not None:
|
|
928
|
+
options["project"] = _split_flag(token, "project")
|
|
929
|
+
elif _split_flag(token, "actor") is not None:
|
|
930
|
+
options["actor"] = _split_flag(token, "actor")
|
|
931
|
+
elif token == "--":
|
|
932
|
+
rest.extend(argv[i + 1:])
|
|
933
|
+
break
|
|
934
|
+
elif token.startswith("-"):
|
|
935
|
+
raise UsageError("invalid_command", f"Unknown flag: {token}.")
|
|
936
|
+
else:
|
|
937
|
+
rest.extend(argv[i:])
|
|
938
|
+
break
|
|
939
|
+
i += 1
|
|
940
|
+
return options, rest
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _emit_result(command, result, as_json, stdout):
|
|
944
|
+
if as_json:
|
|
945
|
+
stdout.write(json.dumps(
|
|
946
|
+
{"command": command, "ok": True, "result": result},
|
|
947
|
+
sort_keys=True, default=str,
|
|
948
|
+
) + "\n")
|
|
949
|
+
return
|
|
950
|
+
if command == "history":
|
|
951
|
+
executions = result["executions"]
|
|
952
|
+
stdout.write("--- history (%d) ---\n" % len(executions))
|
|
953
|
+
for item in executions:
|
|
954
|
+
stdout.write("cursor=%s %s %s ok=%s\n" % (
|
|
955
|
+
item.get("history_cursor"), item.get("execution_id"),
|
|
956
|
+
item.get("state"), item.get("ok"),
|
|
957
|
+
))
|
|
958
|
+
return
|
|
959
|
+
if command == "reset":
|
|
960
|
+
stdout.write("--- reset generation=%s ---\n"
|
|
961
|
+
% result["generation"])
|
|
962
|
+
return
|
|
963
|
+
stdout.write("--- cell %s cursor=%s ---\n" % (
|
|
964
|
+
result["execution_id"], result["history_cursor"]))
|
|
965
|
+
if result.get("stdout"):
|
|
966
|
+
stdout.write(result["stdout"])
|
|
967
|
+
if not result["stdout"].endswith("\n"):
|
|
968
|
+
stdout.write("\n")
|
|
969
|
+
if result.get("stderr"):
|
|
970
|
+
stdout.write("--- stderr ---\n")
|
|
971
|
+
stdout.write(result["stderr"])
|
|
972
|
+
if not result["stderr"].endswith("\n"):
|
|
973
|
+
stdout.write("\n")
|
|
974
|
+
script = result.get("cli_script", [])
|
|
975
|
+
stdout.write("--- ops (%d) ---\n" % len(script))
|
|
976
|
+
if script:
|
|
977
|
+
for line in script:
|
|
978
|
+
stdout.write(line + "\n")
|
|
979
|
+
else:
|
|
980
|
+
stdout.write("(none)\n")
|
|
981
|
+
for item in result.get("op_results", []):
|
|
982
|
+
if not item["ok"]:
|
|
983
|
+
stdout.write("op %s: error [%s] %s\n" % (
|
|
984
|
+
item["operation"], item["error"]["code"],
|
|
985
|
+
item["error"]["message"],
|
|
986
|
+
))
|
|
987
|
+
if result.get("state") == "not_run":
|
|
988
|
+
stdout.write("--- not run [%s] ---\n%s\n" % (
|
|
989
|
+
result.get("code"), result.get("reason")))
|
|
990
|
+
stdout.write("--- execution %s ok=%s ---\n" % (
|
|
991
|
+
result["state"], "true" if result["ok"] else "false"))
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _emit_error(command, exc, as_json, stdout, stderr):
|
|
995
|
+
payload = error_payload(exc)
|
|
996
|
+
if as_json:
|
|
997
|
+
stdout.write(json.dumps(
|
|
998
|
+
{"command": command, "ok": False, "error": payload},
|
|
999
|
+
sort_keys=True, default=str,
|
|
1000
|
+
) + "\n")
|
|
1001
|
+
else:
|
|
1002
|
+
line = "error [%s]: %s" % (payload["code"], payload["message"])
|
|
1003
|
+
if payload.get("fields"):
|
|
1004
|
+
line += " (fields: %s)" % ", ".join(payload["fields"])
|
|
1005
|
+
stderr.write(line + "\n")
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _do_submit(console, options, code, stdout, stderr):
|
|
1009
|
+
try:
|
|
1010
|
+
record = console.submit(code)
|
|
1011
|
+
except ExecutionError as exc:
|
|
1012
|
+
_emit_error("submit", exc, options["json"], stdout, stderr)
|
|
1013
|
+
return
|
|
1014
|
+
_emit_result("submit", record, options["json"], stdout)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _do_submit_file(console, options, path, stdout, stderr):
|
|
1018
|
+
try:
|
|
1019
|
+
record = console.submit_file(path)
|
|
1020
|
+
except ExecutionError as exc:
|
|
1021
|
+
_emit_error("submit-file", exc, options["json"], stdout, stderr)
|
|
1022
|
+
return
|
|
1023
|
+
_emit_result("submit-file", record, options["json"], stdout)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _do_history(console, options, stdout):
|
|
1027
|
+
_emit_result("history", {"executions": console.history()},
|
|
1028
|
+
options["json"], stdout)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _do_reset(console, options, stdout):
|
|
1032
|
+
_emit_result("reset", console.reset(), options["json"], stdout)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _run_session(console, options, stdin, stdout, stderr):
|
|
1036
|
+
interactive = stdin.isatty() if hasattr(stdin, "isatty") else False
|
|
1037
|
+
if interactive:
|
|
1038
|
+
stdout.write("console> ")
|
|
1039
|
+
stdout.flush()
|
|
1040
|
+
while True:
|
|
1041
|
+
line = stdin.readline()
|
|
1042
|
+
if line == "":
|
|
1043
|
+
break
|
|
1044
|
+
stripped = line.strip()
|
|
1045
|
+
if interactive:
|
|
1046
|
+
stdout.write("console> ")
|
|
1047
|
+
stdout.flush()
|
|
1048
|
+
if not stripped or stripped.startswith("#"):
|
|
1049
|
+
continue
|
|
1050
|
+
if stripped in ("exit", "quit"):
|
|
1051
|
+
break
|
|
1052
|
+
if stripped == "help":
|
|
1053
|
+
stdout.write(USAGE)
|
|
1054
|
+
continue
|
|
1055
|
+
if stripped == "submit":
|
|
1056
|
+
cell_lines = []
|
|
1057
|
+
while True:
|
|
1058
|
+
cell_line = stdin.readline()
|
|
1059
|
+
if cell_line == "":
|
|
1060
|
+
break
|
|
1061
|
+
if cell_line.strip() == ":end":
|
|
1062
|
+
break
|
|
1063
|
+
cell_lines.append(cell_line)
|
|
1064
|
+
eof = cell_line == ""
|
|
1065
|
+
code = "".join(cell_lines)
|
|
1066
|
+
if eof and not code.strip():
|
|
1067
|
+
break
|
|
1068
|
+
_do_submit(console, options, code, stdout, stderr)
|
|
1069
|
+
if eof:
|
|
1070
|
+
break
|
|
1071
|
+
continue
|
|
1072
|
+
if stripped == "submit-file" or stripped.startswith("submit-file "):
|
|
1073
|
+
_, _, path = stripped.partition(" ")
|
|
1074
|
+
path = path.strip()
|
|
1075
|
+
if not path:
|
|
1076
|
+
_emit_error("submit-file", UsageError(
|
|
1077
|
+
"invalid_command", "`submit-file` needs a PATH."),
|
|
1078
|
+
options["json"], stdout, stderr)
|
|
1079
|
+
continue
|
|
1080
|
+
_do_submit_file(console, options, path, stdout, stderr)
|
|
1081
|
+
continue
|
|
1082
|
+
if stripped == "history":
|
|
1083
|
+
_do_history(console, options, stdout)
|
|
1084
|
+
continue
|
|
1085
|
+
if stripped == "reset":
|
|
1086
|
+
_do_reset(console, options, stdout)
|
|
1087
|
+
continue
|
|
1088
|
+
_emit_error(stripped.split()[0], UsageError(
|
|
1089
|
+
"invalid_command",
|
|
1090
|
+
f"Unknown command: {stripped} (want submit, submit-file,"
|
|
1091
|
+
" history, reset, help, quit).",
|
|
1092
|
+
), options["json"], stdout, stderr)
|
|
1093
|
+
return EXIT_OK
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def main(argv=None, engine=None, cell_runner=None, stdin=None, stdout=None,
|
|
1097
|
+
stderr=None):
|
|
1098
|
+
"""Run the stdio console; return the process exit code (never raise).
|
|
1099
|
+
|
|
1100
|
+
Args:
|
|
1101
|
+
argv: Argument vector without the program name; defaults to
|
|
1102
|
+
sys.argv[1:].
|
|
1103
|
+
engine: Engine to run against; constructed from
|
|
1104
|
+
--project/--actor when omitted.
|
|
1105
|
+
cell_runner: Worker runner; constructed when omitted.
|
|
1106
|
+
stdin: Input stream for the interactive session.
|
|
1107
|
+
stdout: Stream for results and machine envelopes.
|
|
1108
|
+
stderr: Stream for human-readable errors plus usage.
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
Process exit code: 0 session-ok, 2 usage error.
|
|
1112
|
+
|
|
1113
|
+
Caution: Exit zero is not PASS — per-cell verdicts live in the
|
|
1114
|
+
persisted records; read them, never the exit code.
|
|
1115
|
+
"""
|
|
1116
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
1117
|
+
stdin = sys.stdin if stdin is None else stdin
|
|
1118
|
+
stdout = sys.stdout if stdout is None else stdout
|
|
1119
|
+
stderr = sys.stderr if stderr is None else stderr
|
|
1120
|
+
try:
|
|
1121
|
+
options, rest = _parse_argv(argv)
|
|
1122
|
+
except UsageError as exc:
|
|
1123
|
+
_emit_error(None, exc, False, stdout, stderr)
|
|
1124
|
+
return EXIT_USAGE
|
|
1125
|
+
if options["help"]:
|
|
1126
|
+
stdout.write(USAGE)
|
|
1127
|
+
return EXIT_OK
|
|
1128
|
+
if rest:
|
|
1129
|
+
_emit_error(None, UsageError(
|
|
1130
|
+
"invalid_command",
|
|
1131
|
+
f"Unknown command: {rest[0]} (the console is interactive).",
|
|
1132
|
+
), options["json"], stdout, stderr)
|
|
1133
|
+
return EXIT_USAGE
|
|
1134
|
+
if engine is None:
|
|
1135
|
+
engine = Engine(options["project"], actor=options["actor"])
|
|
1136
|
+
try:
|
|
1137
|
+
console = StdioConsole(
|
|
1138
|
+
engine, project_id=options["project"], actor=options["actor"],
|
|
1139
|
+
root=options["root"], cell_runner=cell_runner,
|
|
1140
|
+
)
|
|
1141
|
+
except ExecutionError as exc:
|
|
1142
|
+
_emit_error(None, exc, options["json"], stdout, stderr)
|
|
1143
|
+
return EXIT_USAGE
|
|
1144
|
+
try:
|
|
1145
|
+
return _run_session(console, options, stdin, stdout, stderr)
|
|
1146
|
+
finally:
|
|
1147
|
+
console.close()
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
if __name__ == "__main__":
|
|
1151
|
+
raise SystemExit(main())
|