rlmness-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rlmagent/__init__.py +1 -0
- rlmagent/__main__.py +8 -0
- rlmagent/acp/__init__.py +1 -0
- rlmagent/acp/runtime.py +71 -0
- rlmagent/acp/server.py +598 -0
- rlmagent/acp/updates.py +128 -0
- rlmagent/agentnet.py +237 -0
- rlmagent/agents/__init__.py +1 -0
- rlmagent/agents/approval.py +90 -0
- rlmagent/agents/bridge.py +109 -0
- rlmagent/agents/budget.py +212 -0
- rlmagent/agents/child_handles.py +46 -0
- rlmagent/agents/child_mailbox.py +30 -0
- rlmagent/agents/kernel_api.py +156 -0
- rlmagent/agents/output.py +34 -0
- rlmagent/agents/patch.py +107 -0
- rlmagent/agents/prompts.py +156 -0
- rlmagent/agents/trace.py +290 -0
- rlmagent/agents/tree.py +682 -0
- rlmagent/agents/wasm_api.py +105 -0
- rlmagent/agents/workspace.py +357 -0
- rlmagent/autopilot.py +447 -0
- rlmagent/backend.py +946 -0
- rlmagent/backend_context.py +74 -0
- rlmagent/cli/__init__.py +5 -0
- rlmagent/cli/config_cmd.py +177 -0
- rlmagent/cli/health.py +156 -0
- rlmagent/cli/main.py +1194 -0
- rlmagent/cli/update.py +188 -0
- rlmagent/clipboard.py +63 -0
- rlmagent/config/__init__.py +37 -0
- rlmagent/config/config.py +297 -0
- rlmagent/config/loader.py +119 -0
- rlmagent/config/prompts.py +124 -0
- rlmagent/config/secure_input.py +213 -0
- rlmagent/config/store.py +275 -0
- rlmagent/context/__init__.py +47 -0
- rlmagent/context/budget.py +557 -0
- rlmagent/contract.py +311 -0
- rlmagent/contract_run.py +232 -0
- rlmagent/conversation.py +2547 -0
- rlmagent/cron.py +77 -0
- rlmagent/directives.py +893 -0
- rlmagent/discovery.py +356 -0
- rlmagent/execution_log.py +66 -0
- rlmagent/factory.py +189 -0
- rlmagent/factory_graph.py +413 -0
- rlmagent/harness/__init__.py +63 -0
- rlmagent/harness/contracts/__init__.py +1 -0
- rlmagent/harness/contracts/stream.py +129 -0
- rlmagent/harness/contracts/tooling.py +127 -0
- rlmagent/harness/contracts/transcript/__init__.py +68 -0
- rlmagent/harness/contracts/transcript/_base.py +29 -0
- rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
- rlmagent/harness/contracts/transcript/entries.py +180 -0
- rlmagent/harness/contracts/transcript/segments.py +64 -0
- rlmagent/harness/contracts/values.py +6 -0
- rlmagent/harness/driver.py +397 -0
- rlmagent/harness/engine.py +489 -0
- rlmagent/harness/mending.py +153 -0
- rlmagent/harness/provider/__init__.py +1 -0
- rlmagent/harness/provider/base.py +27 -0
- rlmagent/harness/provider/wire.py +189 -0
- rlmagent/harness/py.typed +0 -0
- rlmagent/harness/session/__init__.py +5 -0
- rlmagent/harness/session/index.py +218 -0
- rlmagent/harness/session/lease.py +66 -0
- rlmagent/harness/session/records.py +118 -0
- rlmagent/harness/session/replay.py +165 -0
- rlmagent/harness/session/store.py +155 -0
- rlmagent/harness/session/summary.py +46 -0
- rlmagent/hints.py +47 -0
- rlmagent/history.py +92 -0
- rlmagent/hooks/__init__.py +1 -0
- rlmagent/hooks/hooks.py +1 -0
- rlmagent/instructions.py +157 -0
- rlmagent/job_runner.py +117 -0
- rlmagent/jobs.py +365 -0
- rlmagent/kept_agents.py +397 -0
- rlmagent/kernel/__init__.py +5 -0
- rlmagent/kernel/session.py +205 -0
- rlmagent/kernel/wasm.py +169 -0
- rlmagent/logs.py +208 -0
- rlmagent/managed_release.py +368 -0
- rlmagent/mcp.py +411 -0
- rlmagent/mcp_oauth.py +213 -0
- rlmagent/model/__init__.py +29 -0
- rlmagent/model/_claude/__init__.py +1 -0
- rlmagent/model/_claude/courier.py +117 -0
- rlmagent/model/_claude/emitter.py +378 -0
- rlmagent/model/_oai/__init__.py +1 -0
- rlmagent/model/_oai/helpers.py +256 -0
- rlmagent/model/_oai/normalize.py +263 -0
- rlmagent/model/_oai/parsers.py +317 -0
- rlmagent/model/_oai/payloads.py +226 -0
- rlmagent/model/_oai/transport.py +127 -0
- rlmagent/model/claude.py +552 -0
- rlmagent/model/copilot.py +142 -0
- rlmagent/model/correlation.py +21 -0
- rlmagent/model/deadline.py +19 -0
- rlmagent/model/discovery.py +181 -0
- rlmagent/model/external/__init__.py +11 -0
- rlmagent/model/external/acp.py +202 -0
- rlmagent/model/external/acp_install.py +109 -0
- rlmagent/model/external/cli.py +248 -0
- rlmagent/model/external/textcalls.py +57 -0
- rlmagent/model/failover.py +129 -0
- rlmagent/model/google.py +199 -0
- rlmagent/model/limits.py +46 -0
- rlmagent/model/oai_compatible.py +414 -0
- rlmagent/model/preferences.py +56 -0
- rlmagent/model/py.typed +0 -0
- rlmagent/model/registered_oauth.py +195 -0
- rlmagent/model/scripted.py +55 -0
- rlmagent/model/settings.py +271 -0
- rlmagent/model/subscription.py +398 -0
- rlmagent/model/token_bounds.py +15 -0
- rlmagent/model/transport/__init__.py +1 -0
- rlmagent/model/transport/backoff.py +87 -0
- rlmagent/model/transport/client.py +87 -0
- rlmagent/model/transport/faults.py +65 -0
- rlmagent/model/transport/responses_socket.py +223 -0
- rlmagent/model/vertex.py +93 -0
- rlmagent/network.py +123 -0
- rlmagent/packages.py +246 -0
- rlmagent/planning.py +84 -0
- rlmagent/playbook/__init__.py +1 -0
- rlmagent/playbook/apply.py +106 -0
- rlmagent/playbook/digest.py +63 -0
- rlmagent/playbook/improver.py +272 -0
- rlmagent/playbook/plan.py +99 -0
- rlmagent/playbook/store.py +67 -0
- rlmagent/plugins.py +327 -0
- rlmagent/policy.py +105 -0
- rlmagent/prompt_refs.py +57 -0
- rlmagent/prompts.py +331 -0
- rlmagent/prune.py +60 -0
- rlmagent/py.typed +0 -0
- rlmagent/reasoning.py +135 -0
- rlmagent/refinement/__init__.py +1 -0
- rlmagent/refinement/controller.py +448 -0
- rlmagent/refinement/engine.py +220 -0
- rlmagent/refinement/evidence.py +131 -0
- rlmagent/refinement/scoped.py +90 -0
- rlmagent/refinement/store.py +193 -0
- rlmagent/refresh.py +202 -0
- rlmagent/routing.py +52 -0
- rlmagent/runtime.py +575 -0
- rlmagent/safety.py +267 -0
- rlmagent/session_html.py +57 -0
- rlmagent/session_import.py +52 -0
- rlmagent/session_share.py +55 -0
- rlmagent/sessions.py +249 -0
- rlmagent/shell_sandbox.py +88 -0
- rlmagent/skillset.py +320 -0
- rlmagent/steering.py +148 -0
- rlmagent/stops.py +62 -0
- rlmagent/subscriptions.py +219 -0
- rlmagent/tasks.py +98 -0
- rlmagent/toolhost.py +310 -0
- rlmagent/tools/__init__.py +56 -0
- rlmagent/tools/_shared.py +210 -0
- rlmagent/tools/files.py +462 -0
- rlmagent/tools/kernel.py +93 -0
- rlmagent/tools/patch_tool.py +68 -0
- rlmagent/tools/search.py +188 -0
- rlmagent/trust.py +383 -0
- rlmagent/tui/__init__.py +1 -0
- rlmagent/tui/adapter.py +506 -0
- rlmagent/tui/app.py +711 -0
- rlmagent/tui/approval_screen.py +57 -0
- rlmagent/tui/choice_screen.py +47 -0
- rlmagent/tui/completions.py +116 -0
- rlmagent/tui/components.py +25 -0
- rlmagent/tui/composer.py +40 -0
- rlmagent/tui/factory_view.py +86 -0
- rlmagent/tui/inspect.py +193 -0
- rlmagent/tui/messages.py +215 -0
- rlmagent/tui/model_screen.py +307 -0
- rlmagent/tui/prompt.py +98 -0
- rlmagent/tui/question_screen.py +52 -0
- rlmagent/tui/replay.py +141 -0
- rlmagent/tui/rlm_agent.tcss +405 -0
- rlmagent/tui/search_box.py +91 -0
- rlmagent/tui/sidebar.py +182 -0
- rlmagent/tui/status.py +49 -0
- rlmagent/tui/steps.py +283 -0
- rlmagent/tui/suggest.py +778 -0
- rlmagent/tui/transcript.py +172 -0
- rlmagent/tui/trust_dialog.py +58 -0
- rlmagent/tui/worker_attach.py +141 -0
- rlmagent/ui/__init__.py +1 -0
- rlmagent/ui/render.py +211 -0
- rlmagent/undo.py +109 -0
- rlmagent/utils/__init__.py +1 -0
- rlmagent/websearch.py +75 -0
- rlmagent/worker.py +1275 -0
- rlmagent/worker_service.py +203 -0
- rlmness_cli-0.2.0.dist-info/METADATA +882 -0
- rlmness_cli-0.2.0.dist-info/RECORD +203 -0
- rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
- rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
- rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Verified wheel bundles activated through an atomic installation pointer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
import venv
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
from zipfile import ZipFile
|
|
16
|
+
|
|
17
|
+
from rlmagent.discovery import _resolve_home
|
|
18
|
+
from rlmagent.refinement.store import _lock
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def root():
|
|
22
|
+
return _resolve_home() / "updates" / "managed"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read(path):
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _write(path, data):
|
|
33
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
temporary = path.with_suffix(f".{uuid4().hex}.tmp")
|
|
35
|
+
try:
|
|
36
|
+
temporary.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
37
|
+
temporary.replace(path)
|
|
38
|
+
finally:
|
|
39
|
+
temporary.unlink(missing_ok=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def python_for(record):
|
|
43
|
+
identity = record["slot"]
|
|
44
|
+
if not re.fullmatch(r"[a-f0-9]{32}", identity):
|
|
45
|
+
raise ValueError("Invalid managed installation identity")
|
|
46
|
+
directory = (root() / "slots" / identity).resolve()
|
|
47
|
+
if not directory.is_relative_to((root() / "slots").resolve()):
|
|
48
|
+
raise ValueError("Managed installation escapes its store")
|
|
49
|
+
return directory / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def manifest(path):
|
|
53
|
+
data = _read(path)
|
|
54
|
+
if (
|
|
55
|
+
not isinstance(data, dict)
|
|
56
|
+
or set(data) - {"format", "version", "channel", "artifacts", "extras"}
|
|
57
|
+
or not {"format", "version", "channel", "artifacts"}.issubset(data)
|
|
58
|
+
or data["format"] != "rlmagent.release/1"
|
|
59
|
+
or data["channel"] not in ("stable", "preview", "nightly")
|
|
60
|
+
or not isinstance(data["version"], str)
|
|
61
|
+
or re.fullmatch(r"[0-9][A-Za-z0-9.!+_-]*", data["version"]) is None
|
|
62
|
+
or not isinstance(data["artifacts"], list)
|
|
63
|
+
or not 1 <= len(data["artifacts"]) <= 256
|
|
64
|
+
):
|
|
65
|
+
raise ValueError("Invalid release manifest")
|
|
66
|
+
extras = data.setdefault("extras", [])
|
|
67
|
+
if (
|
|
68
|
+
not isinstance(extras, list)
|
|
69
|
+
or not all(
|
|
70
|
+
extra in ("subscription", "copilot", "mcp-keyring", "vertex") for extra in extras
|
|
71
|
+
)
|
|
72
|
+
or len(set(extras)) != len(extras)
|
|
73
|
+
):
|
|
74
|
+
raise ValueError("Invalid release extras")
|
|
75
|
+
names = set()
|
|
76
|
+
for artifact in data["artifacts"]:
|
|
77
|
+
if (
|
|
78
|
+
not isinstance(artifact, dict)
|
|
79
|
+
or set(artifact) != {"filename", "sha256"}
|
|
80
|
+
or not isinstance(artifact["filename"], str)
|
|
81
|
+
or re.fullmatch(r"[A-Za-z0-9_.+-]+\.whl", artifact["filename"]) is None
|
|
82
|
+
or artifact["filename"] in names
|
|
83
|
+
or not isinstance(artifact["sha256"], str)
|
|
84
|
+
or re.fullmatch(r"[a-f0-9]{64}", artifact["sha256"]) is None
|
|
85
|
+
):
|
|
86
|
+
raise ValueError("Invalid or duplicate wheel artifact")
|
|
87
|
+
names.add(artifact["filename"])
|
|
88
|
+
return data
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _stage(data, source, destination):
|
|
92
|
+
destination.mkdir(parents=True)
|
|
93
|
+
source = source.resolve()
|
|
94
|
+
for artifact in data["artifacts"]:
|
|
95
|
+
wheel = (source / artifact["filename"]).resolve()
|
|
96
|
+
if (
|
|
97
|
+
not wheel.is_relative_to(source)
|
|
98
|
+
or not wheel.is_file()
|
|
99
|
+
or wheel.stat().st_size > 512 * 1024 * 1024
|
|
100
|
+
):
|
|
101
|
+
raise ValueError("Wheel is absent, too large, or outside the bundle")
|
|
102
|
+
target = destination / artifact["filename"]
|
|
103
|
+
shutil.copyfile(wheel, target)
|
|
104
|
+
with target.open("rb") as stream:
|
|
105
|
+
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
|
106
|
+
if digest != artifact["sha256"]:
|
|
107
|
+
raise ValueError(f"Wheel checksum mismatch: {artifact['filename']}")
|
|
108
|
+
with ZipFile(target) as archive:
|
|
109
|
+
metadata = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")]
|
|
110
|
+
if len(metadata) != 1:
|
|
111
|
+
raise ValueError("Wheel needs exactly one package metadata record")
|
|
112
|
+
if archive.getinfo(metadata[0]).file_size > 1024 * 1024:
|
|
113
|
+
raise ValueError("Wheel metadata exceeds the limit")
|
|
114
|
+
for line in archive.read(metadata[0]).decode().splitlines():
|
|
115
|
+
if line.lower().startswith("requires-dist:") and "@" in line:
|
|
116
|
+
raise ValueError("Offline bundles must not depend on direct network URLs")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _health(record):
|
|
120
|
+
python = python_for(record)
|
|
121
|
+
script = (
|
|
122
|
+
"import importlib.metadata as m; import rlmagent.cli.main; "
|
|
123
|
+
"import rlmagent.tui.app; import rlmagent.model.oai_compatible; import rlmness; "
|
|
124
|
+
"assert m.version('rlmness-cli') == " + repr(record["version"])
|
|
125
|
+
)
|
|
126
|
+
if "subscription" in record.get("extras", []):
|
|
127
|
+
script += "; import jwt, keyring, websockets"
|
|
128
|
+
if "copilot" in record.get("extras", []):
|
|
129
|
+
script += "; import copilot"
|
|
130
|
+
env = {
|
|
131
|
+
key: value for key, value in os.environ.items() if key not in ("PYTHONPATH", "PYTHONHOME")
|
|
132
|
+
}
|
|
133
|
+
reply = subprocess.run(
|
|
134
|
+
[str(python), "-I", "-c", script],
|
|
135
|
+
cwd=python.parent.parent,
|
|
136
|
+
env=env,
|
|
137
|
+
check=False,
|
|
138
|
+
stdout=subprocess.DEVNULL,
|
|
139
|
+
stderr=subprocess.DEVNULL,
|
|
140
|
+
timeout=30,
|
|
141
|
+
)
|
|
142
|
+
if reply.returncode:
|
|
143
|
+
raise ValueError("Replacement installation failed its health check")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _recover():
|
|
147
|
+
from rlmagent import worker
|
|
148
|
+
|
|
149
|
+
journal = _read(root() / "transaction.json")
|
|
150
|
+
if journal and journal["phase"] == "activating":
|
|
151
|
+
previous = journal.get("previous")
|
|
152
|
+
if previous is not None:
|
|
153
|
+
_write(root() / "current.json", previous)
|
|
154
|
+
else:
|
|
155
|
+
(root() / "current.json").unlink(missing_ok=True)
|
|
156
|
+
if journal.get("workers"):
|
|
157
|
+
names = {state["name"] for state in journal["workers"]}
|
|
158
|
+
_drain([state for state in _fleet() if state["name"] in names], 30)
|
|
159
|
+
if previous is not None:
|
|
160
|
+
_restart(journal["workers"], previous)
|
|
161
|
+
_write(root() / "transaction.json", {**journal, "phase": "recovered"})
|
|
162
|
+
if journal:
|
|
163
|
+
pause = worker.root() / "admission-paused"
|
|
164
|
+
if pause.exists() and pause.read_text() == journal["candidate"]["slot"]:
|
|
165
|
+
pause.unlink()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _fleet():
|
|
169
|
+
from rlmagent import worker
|
|
170
|
+
|
|
171
|
+
if not worker.root().exists():
|
|
172
|
+
return []
|
|
173
|
+
states = []
|
|
174
|
+
for path in worker.root().iterdir():
|
|
175
|
+
if not path.is_dir() or not worker._NAME.fullmatch(path.name):
|
|
176
|
+
continue
|
|
177
|
+
state = worker.state_of(path.name)
|
|
178
|
+
if state["alive"] or worker._supervised(state):
|
|
179
|
+
states.append(state)
|
|
180
|
+
return states
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _drain(states, timeout):
|
|
184
|
+
from rlmagent import worker
|
|
185
|
+
|
|
186
|
+
for state in states:
|
|
187
|
+
(worker.folder(state["name"]) / "stop").touch()
|
|
188
|
+
deadline = time.monotonic() + timeout
|
|
189
|
+
names = {state["name"] for state in states}
|
|
190
|
+
while any(state["name"] in names for state in _fleet()):
|
|
191
|
+
if time.monotonic() >= deadline:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"Worker drain timed out; active installation was preserved; no workers were killed"
|
|
194
|
+
)
|
|
195
|
+
time.sleep(0.2)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _restart(states, record):
|
|
199
|
+
from rlmagent import worker
|
|
200
|
+
|
|
201
|
+
for state in states:
|
|
202
|
+
launch = _read(worker.folder(state["name"]) / "launch.json")
|
|
203
|
+
if not launch:
|
|
204
|
+
raise ValueError("Worker has no saved launch settings")
|
|
205
|
+
args = list(launch["argv"])
|
|
206
|
+
if "--resume" in args:
|
|
207
|
+
index = args.index("--resume")
|
|
208
|
+
del args[index : index + 2]
|
|
209
|
+
if state.get("session"):
|
|
210
|
+
args += ["--resume", state["session"]]
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
[
|
|
213
|
+
str(python_for(record)),
|
|
214
|
+
"-m",
|
|
215
|
+
"rlmagent.cli.main",
|
|
216
|
+
"worker",
|
|
217
|
+
"start",
|
|
218
|
+
state["name"],
|
|
219
|
+
*args,
|
|
220
|
+
],
|
|
221
|
+
cwd=launch["cwd"],
|
|
222
|
+
env={
|
|
223
|
+
key: value
|
|
224
|
+
for key, value in os.environ.items()
|
|
225
|
+
if key not in ("PYTHONPATH", "PYTHONHOME")
|
|
226
|
+
},
|
|
227
|
+
check=False,
|
|
228
|
+
timeout=200,
|
|
229
|
+
)
|
|
230
|
+
if result.returncode:
|
|
231
|
+
raise ValueError(f"Worker {state['name']} could not restart; inspect its saved log")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def install(path: Path, *, drain=False, wait=60, restart=False):
|
|
235
|
+
if wait < 0 or wait > 3600:
|
|
236
|
+
raise ValueError("Drain wait must be between 0 and 3600 seconds")
|
|
237
|
+
with _lock(root() / "install.lock"):
|
|
238
|
+
_recover()
|
|
239
|
+
data = manifest(path)
|
|
240
|
+
previous = _read(root() / "current.json")
|
|
241
|
+
states = _fleet()
|
|
242
|
+
if states and not drain:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"Workers are running; use --drain to finish their current work before activation"
|
|
245
|
+
)
|
|
246
|
+
if restart:
|
|
247
|
+
from rlmagent import worker
|
|
248
|
+
|
|
249
|
+
if any(_read(worker.folder(state["name"]) / "launch.json") is None for state in states):
|
|
250
|
+
raise ValueError(
|
|
251
|
+
"A worker lacks saved launch settings; restart cannot be guaranteed"
|
|
252
|
+
)
|
|
253
|
+
candidate = {
|
|
254
|
+
"slot": uuid4().hex,
|
|
255
|
+
"version": data["version"],
|
|
256
|
+
"channel": data["channel"],
|
|
257
|
+
"extras": data.get("extras", []),
|
|
258
|
+
}
|
|
259
|
+
incoming = root() / "bundles" / candidate["slot"]
|
|
260
|
+
_stage(data, path.parent, incoming)
|
|
261
|
+
python = python_for(candidate)
|
|
262
|
+
venv.EnvBuilder(with_pip=True).create(python.parent.parent)
|
|
263
|
+
requirement = "rlmness-cli" + (
|
|
264
|
+
"[" + ",".join(candidate["extras"]) + "]" if candidate["extras"] else ""
|
|
265
|
+
)
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
[
|
|
268
|
+
str(python),
|
|
269
|
+
"-I",
|
|
270
|
+
"-m",
|
|
271
|
+
"pip",
|
|
272
|
+
"install",
|
|
273
|
+
"--no-index",
|
|
274
|
+
"--find-links",
|
|
275
|
+
str(incoming),
|
|
276
|
+
f"{requirement}=={data['version']}",
|
|
277
|
+
],
|
|
278
|
+
check=False,
|
|
279
|
+
timeout=300,
|
|
280
|
+
)
|
|
281
|
+
if result.returncode:
|
|
282
|
+
raise ValueError("Bundle installation failed; active installation was preserved")
|
|
283
|
+
_health(candidate)
|
|
284
|
+
if states:
|
|
285
|
+
_drain(states, wait)
|
|
286
|
+
journal = {
|
|
287
|
+
"phase": "activating",
|
|
288
|
+
"candidate": candidate,
|
|
289
|
+
"previous": previous,
|
|
290
|
+
"workers": states if restart else [],
|
|
291
|
+
}
|
|
292
|
+
_write(root() / "transaction.json", journal)
|
|
293
|
+
try:
|
|
294
|
+
if restart and states:
|
|
295
|
+
from rlmagent import worker
|
|
296
|
+
|
|
297
|
+
(worker.root() / "admission-paused").write_text(candidate["slot"])
|
|
298
|
+
_write(root() / "previous.json", previous)
|
|
299
|
+
_write(root() / "current.json", candidate)
|
|
300
|
+
_health(candidate)
|
|
301
|
+
if restart:
|
|
302
|
+
_restart(states, candidate)
|
|
303
|
+
_write(root() / "transaction.json", {**journal, "phase": "complete"})
|
|
304
|
+
_recover()
|
|
305
|
+
except BaseException:
|
|
306
|
+
_recover()
|
|
307
|
+
raise
|
|
308
|
+
return candidate
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def rollback(*, drain=False, wait=60, restart=False):
|
|
312
|
+
with _lock(root() / "install.lock"):
|
|
313
|
+
_recover()
|
|
314
|
+
previous = _read(root() / "previous.json")
|
|
315
|
+
if previous is None:
|
|
316
|
+
raise ValueError("No prior managed installation is available")
|
|
317
|
+
_health(previous)
|
|
318
|
+
states = _fleet()
|
|
319
|
+
if states and not drain:
|
|
320
|
+
raise ValueError("Workers are running; use --drain before rollback")
|
|
321
|
+
if restart:
|
|
322
|
+
from rlmagent import worker
|
|
323
|
+
|
|
324
|
+
if any(_read(worker.folder(state["name"]) / "launch.json") is None for state in states):
|
|
325
|
+
raise ValueError(
|
|
326
|
+
"A worker lacks saved launch settings; restart cannot be guaranteed"
|
|
327
|
+
)
|
|
328
|
+
if states:
|
|
329
|
+
_drain(states, wait)
|
|
330
|
+
current = _read(root() / "current.json")
|
|
331
|
+
journal = {
|
|
332
|
+
"phase": "activating",
|
|
333
|
+
"candidate": previous,
|
|
334
|
+
"previous": current,
|
|
335
|
+
"workers": states if restart else [],
|
|
336
|
+
}
|
|
337
|
+
_write(root() / "transaction.json", journal)
|
|
338
|
+
try:
|
|
339
|
+
if restart and states:
|
|
340
|
+
from rlmagent import worker
|
|
341
|
+
|
|
342
|
+
(worker.root() / "admission-paused").write_text(previous["slot"])
|
|
343
|
+
_write(root() / "current.json", previous)
|
|
344
|
+
if restart:
|
|
345
|
+
_restart(states, previous)
|
|
346
|
+
_write(root() / "previous.json", current)
|
|
347
|
+
_write(root() / "transaction.json", {**journal, "phase": "complete"})
|
|
348
|
+
_recover()
|
|
349
|
+
except BaseException:
|
|
350
|
+
_recover()
|
|
351
|
+
raise
|
|
352
|
+
return previous
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def run(args):
|
|
356
|
+
with _lock(root() / "install.lock"):
|
|
357
|
+
_recover()
|
|
358
|
+
record = _read(root() / "current.json")
|
|
359
|
+
if record is None:
|
|
360
|
+
raise ValueError("No managed installation is active")
|
|
361
|
+
python = python_for(record)
|
|
362
|
+
_health(record)
|
|
363
|
+
env = {
|
|
364
|
+
key: value for key, value in os.environ.items() if key not in ("PYTHONPATH", "PYTHONHOME")
|
|
365
|
+
}
|
|
366
|
+
return subprocess.run(
|
|
367
|
+
[str(python), "-m", "rlmagent.cli.main", *args], env=env, check=False
|
|
368
|
+
).returncode
|