aisc-cli 0.1.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.
- aisc/VERSION +1 -0
- aisc/__init__.py +49 -0
- aisc/__main__.py +6 -0
- aisc/adapters/__init__.py +1 -0
- aisc/adapters/config_reader.py +215 -0
- aisc/adapters/container_registry.py +718 -0
- aisc/adapters/data_root_store.py +296 -0
- aisc/adapters/docker_.py +1183 -0
- aisc/adapters/docker_gateway.py +1244 -0
- aisc/adapters/docker_sdk_backed.py +274 -0
- aisc/adapters/maintenance_lock.py +97 -0
- aisc/adapters/state_file.py +184 -0
- aisc/adapters/system.py +101 -0
- aisc/adapters/windows_config_reader.py +249 -0
- aisc/adapters/workspace_lease_store.py +189 -0
- aisc/application/__init__.py +1 -0
- aisc/application/artifact.py +265 -0
- aisc/application/bundle_fetch.py +836 -0
- aisc/application/cc_switch_provider.py +245 -0
- aisc/application/cc_switch_resolver.py +363 -0
- aisc/application/config_service.py +452 -0
- aisc/application/conversation.py +516 -0
- aisc/application/data_migration.py +440 -0
- aisc/application/data_root.py +275 -0
- aisc/application/docker_lifecycle.py +659 -0
- aisc/application/doctor.py +821 -0
- aisc/application/legacy_scan.py +252 -0
- aisc/application/network_subscription.py +642 -0
- aisc/application/profile_service.py +105 -0
- aisc/application/provider.py +92 -0
- aisc/application/repair.py +387 -0
- aisc/application/resources.py +270 -0
- aisc/application/runtime.py +1826 -0
- aisc/application/session.py +320 -0
- aisc/application/tool_versions.py +210 -0
- aisc/application/toolchain.py +91 -0
- aisc/application/update.py +286 -0
- aisc/application/usage.py +305 -0
- aisc/application/version.py +133 -0
- aisc/application/web_gateway.py +459 -0
- aisc/application/workspace_reconcile.py +318 -0
- aisc/applog.py +103 -0
- aisc/cli/__init__.py +1 -0
- aisc/cli/build_progress_parser.py +217 -0
- aisc/cli/commands/__init__.py +1 -0
- aisc/cli/commands/agents.py +117 -0
- aisc/cli/commands/artifact.py +210 -0
- aisc/cli/commands/build.py +566 -0
- aisc/cli/commands/bundle.py +107 -0
- aisc/cli/commands/cc_switch.py +158 -0
- aisc/cli/commands/config.py +104 -0
- aisc/cli/commands/container.py +787 -0
- aisc/cli/commands/conversation.py +72 -0
- aisc/cli/commands/data_root.py +84 -0
- aisc/cli/commands/logs.py +89 -0
- aisc/cli/commands/network.py +132 -0
- aisc/cli/commands/profile.py +52 -0
- aisc/cli/commands/provider.py +49 -0
- aisc/cli/commands/run.py +494 -0
- aisc/cli/commands/runs.py +125 -0
- aisc/cli/commands/runtime.py +450 -0
- aisc/cli/commands/serve.py +550 -0
- aisc/cli/commands/serve_fs.py +369 -0
- aisc/cli/commands/session.py +167 -0
- aisc/cli/commands/update.py +145 -0
- aisc/cli/commands/usage.py +74 -0
- aisc/cli/commands/wizard.py +130 -0
- aisc/cli/commands/workspaces.py +162 -0
- aisc/cli/help_i18n.py +385 -0
- aisc/cli/main.py +2926 -0
- aisc/cli/output.py +239 -0
- aisc/cli/tutorial.py +109 -0
- aisc/domain/__init__.py +1 -0
- aisc/domain/artifacts.py +279 -0
- aisc/domain/cc_switch_release.py +303 -0
- aisc/domain/config.py +64 -0
- aisc/domain/data_migration.py +259 -0
- aisc/domain/data_root.py +124 -0
- aisc/domain/docker_ownership.py +148 -0
- aisc/domain/gateway.py +168 -0
- aisc/domain/models.py +674 -0
- aisc/domain/toolchain.py +72 -0
- aisc/domain/web_services.py +355 -0
- aisc/domain/workspace_lease.py +139 -0
- aisc/schemas/__init__.py +1 -0
- aisc/schemas/config_schema.py +145 -0
- aisc_cli-0.1.0.dist-info/METADATA +420 -0
- aisc_cli-0.1.0.dist-info/RECORD +92 -0
- aisc_cli-0.1.0.dist-info/WHEEL +5 -0
- aisc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- aisc_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- aisc_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Runtime commands implementation.
|
|
2
|
+
|
|
3
|
+
Implements aisc runtime subcommands: preflight, start, stop, list, inspect, etc.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from aisc.adapters.docker_ import DockerExecutor
|
|
13
|
+
from aisc.adapters.docker_sdk_backed import default_executor
|
|
14
|
+
from aisc.domain.models import CliError, RuntimeErrorCode, RuntimeSnapshot, RuntimeExitCode
|
|
15
|
+
from aisc.application.runtime import (
|
|
16
|
+
iso_now,
|
|
17
|
+
compute_config_fingerprint,
|
|
18
|
+
validate_uuid_v4,
|
|
19
|
+
preflight_runtime,
|
|
20
|
+
start_runtime,
|
|
21
|
+
list_runtimes,
|
|
22
|
+
inspect_runtime,
|
|
23
|
+
stop_runtime,
|
|
24
|
+
restart_runtime,
|
|
25
|
+
remove_runtime,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_runtime_preflight(
|
|
30
|
+
runtime_id: str,
|
|
31
|
+
workspace: Optional[str] = None,
|
|
32
|
+
image: str = "super-claude:latest",
|
|
33
|
+
network: str = "direct",
|
|
34
|
+
scope: str = "project",
|
|
35
|
+
owner: str = "workbench",
|
|
36
|
+
format: str = "text",
|
|
37
|
+
executor: Optional[DockerExecutor] = None,
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""Perform read-only preflight checks for runtime startup.
|
|
40
|
+
|
|
41
|
+
Per docs/gui-planning/05-cli-gui-contract.md §5.1.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
runtime_id: UUID v4 provided by Workbench
|
|
45
|
+
workspace: Workspace path (defaults to cwd)
|
|
46
|
+
image: Docker image name
|
|
47
|
+
network: Network mode (direct/proxy)
|
|
48
|
+
scope: Scope mode (project/temporary)
|
|
49
|
+
owner: Owner identifier
|
|
50
|
+
format: Output format (text/json)
|
|
51
|
+
executor: Docker executor (for testing)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dict with preflight result payload
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Validate runtime_id
|
|
58
|
+
if not validate_uuid_v4(runtime_id):
|
|
59
|
+
return {
|
|
60
|
+
"error": {
|
|
61
|
+
"code": RuntimeErrorCode.INVALID_RUNTIME_ID,
|
|
62
|
+
"message": f"Invalid runtime ID format (must be UUID v4): {runtime_id}",
|
|
63
|
+
},
|
|
64
|
+
"exit_code": RuntimeExitCode.INVALID_RUNTIME_ID,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Validate scope
|
|
68
|
+
if scope not in ("project", "temporary"):
|
|
69
|
+
return {
|
|
70
|
+
"error": {
|
|
71
|
+
"code": RuntimeErrorCode.SCOPE_INVALID,
|
|
72
|
+
"message": f"Invalid scope (must be project|temporary): {scope}",
|
|
73
|
+
},
|
|
74
|
+
"exit_code": RuntimeExitCode.USAGE_ERROR,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Resolve workspace
|
|
78
|
+
ws_path = Path(workspace).resolve() if workspace else Path.cwd()
|
|
79
|
+
|
|
80
|
+
# PERF P4 (D-13): SDK-backed default executor (AISC_DOCKER_EXECUTOR=cli rollback)
|
|
81
|
+
exec_ = executor or default_executor()
|
|
82
|
+
|
|
83
|
+
# Execute preflight (Stage 7: registry root = data-root state dir)
|
|
84
|
+
from aisc.application.data_root import workspace_state_dir
|
|
85
|
+
|
|
86
|
+
registry_root = workspace_state_dir(ws_path)
|
|
87
|
+
result = preflight_runtime(
|
|
88
|
+
runtime_id=runtime_id,
|
|
89
|
+
workspace=str(ws_path),
|
|
90
|
+
image=image,
|
|
91
|
+
network=network,
|
|
92
|
+
scope=scope,
|
|
93
|
+
owner=owner,
|
|
94
|
+
executor=exec_,
|
|
95
|
+
registry_root=registry_root,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Convert to dict for JSON output
|
|
99
|
+
return {
|
|
100
|
+
"spec": result.spec,
|
|
101
|
+
"checks": [
|
|
102
|
+
{
|
|
103
|
+
"id": c.id,
|
|
104
|
+
"status": c.status,
|
|
105
|
+
"error_code": c.error_code,
|
|
106
|
+
"detail": c.detail,
|
|
107
|
+
}
|
|
108
|
+
for c in result.checks
|
|
109
|
+
],
|
|
110
|
+
"can_start": result.can_start,
|
|
111
|
+
"recommended_action": result.recommended_action,
|
|
112
|
+
"matching_runtime_id": result.matching_runtime_id,
|
|
113
|
+
"conflicts": result.conflicts,
|
|
114
|
+
"observed_at": result.observed_at,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Runtime lifecycle commands (§5.2-5.5)
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def _resolve_workspace_and_registry(workspace: Optional[str]) -> tuple:
|
|
123
|
+
"""Return (canonical_workspace_str, registry_root_path).
|
|
124
|
+
|
|
125
|
+
Stage 7: registry root is the data-root state dir (DATA-01); legacy
|
|
126
|
+
``<workspace>/.aisc`` state is adopted on first use.
|
|
127
|
+
"""
|
|
128
|
+
from aisc.application.data_root import workspace_state_dir
|
|
129
|
+
|
|
130
|
+
ws_path = Path(workspace).resolve() if workspace else Path.cwd()
|
|
131
|
+
return str(ws_path), workspace_state_dir(ws_path)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_runtime_start(
|
|
135
|
+
runtime_id: str,
|
|
136
|
+
workspace: Optional[str] = None,
|
|
137
|
+
image: str = "super-claude:latest",
|
|
138
|
+
network: str = "direct",
|
|
139
|
+
scope: str = "project",
|
|
140
|
+
owner: str = "workbench",
|
|
141
|
+
proxy_config: Optional[str] = None,
|
|
142
|
+
host_mcp_url: Optional[str] = None,
|
|
143
|
+
max_memory: Optional[str] = None,
|
|
144
|
+
max_cpus: Optional[float] = None,
|
|
145
|
+
executor: Optional[DockerExecutor] = None,
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""Execute ``aisc runtime start`` per contract §5.2."""
|
|
148
|
+
exec_ = executor or default_executor()
|
|
149
|
+
ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
150
|
+
result = start_runtime(
|
|
151
|
+
runtime_id=runtime_id,
|
|
152
|
+
workspace=ws,
|
|
153
|
+
image=image,
|
|
154
|
+
network=network,
|
|
155
|
+
scope=scope,
|
|
156
|
+
owner=owner,
|
|
157
|
+
executor=exec_,
|
|
158
|
+
registry_root=reg_root,
|
|
159
|
+
proxy_config=proxy_config,
|
|
160
|
+
host_mcp_url=host_mcp_url,
|
|
161
|
+
max_memory=max_memory,
|
|
162
|
+
max_cpus=max_cpus,
|
|
163
|
+
)
|
|
164
|
+
return result.to_dict()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_runtime_list(
|
|
168
|
+
workspace: Optional[str] = None,
|
|
169
|
+
owner: Optional[str] = None,
|
|
170
|
+
executor: Optional[DockerExecutor] = None,
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""Execute ``aisc runtime list`` per contract §5.3."""
|
|
173
|
+
exec_ = executor or default_executor()
|
|
174
|
+
ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
175
|
+
snapshots = list_runtimes(executor=exec_, registry_root=reg_root, owner=owner, workspace=ws)
|
|
176
|
+
return {"runtimes": [s.to_dict() for s in snapshots], "observed_at": iso_now()}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_runtime_inspect(
|
|
180
|
+
runtime_id: str,
|
|
181
|
+
workspace: Optional[str] = None,
|
|
182
|
+
executor: Optional[DockerExecutor] = None,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""Execute ``aisc runtime inspect`` per contract §5.4."""
|
|
185
|
+
exec_ = executor or default_executor()
|
|
186
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
187
|
+
snapshot = inspect_runtime(runtime_id=runtime_id, executor=exec_, registry_root=reg_root)
|
|
188
|
+
return snapshot.to_dict()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def cmd_runtime_status(
|
|
192
|
+
runtime_id: str,
|
|
193
|
+
workspace: Optional[str] = None,
|
|
194
|
+
executor: Optional[DockerExecutor] = None,
|
|
195
|
+
) -> Dict[str, Any]:
|
|
196
|
+
"""Execute ``aisc runtime status`` (PERF P1, contract §5.4b): inspect +
|
|
197
|
+
services merged in ONE invocation — the 5s poll tick used to pay the
|
|
198
|
+
~750ms sidecar spawn twice. ``snapshot`` is authoritative; ``services``
|
|
199
|
+
is best-effort under a 3s deadline (timeout/failure -> None)."""
|
|
200
|
+
from aisc.application.runtime import runtime_status
|
|
201
|
+
|
|
202
|
+
exec_ = executor or default_executor()
|
|
203
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
204
|
+
return runtime_status(
|
|
205
|
+
runtime_id=runtime_id, executor=exec_, registry_root=reg_root
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_runtime_stop(
|
|
210
|
+
runtime_id: str,
|
|
211
|
+
workspace: Optional[str] = None,
|
|
212
|
+
executor: Optional[DockerExecutor] = None,
|
|
213
|
+
grace_seconds: int = 10,
|
|
214
|
+
) -> Dict[str, Any]:
|
|
215
|
+
"""Execute ``aisc runtime stop`` per contract §5.5.
|
|
216
|
+
|
|
217
|
+
``--grace`` is validated here (command layer): out-of-range values are a
|
|
218
|
+
usage error and must never reach Docker (05 §3.1)."""
|
|
219
|
+
if not isinstance(grace_seconds, int) or not 1 <= grace_seconds <= 600:
|
|
220
|
+
raise CliError(
|
|
221
|
+
message=f"Invalid --grace {grace_seconds!r}: must be an integer in 1..600",
|
|
222
|
+
exit_code=2,
|
|
223
|
+
error_code="AISC_ERR_USAGE",
|
|
224
|
+
)
|
|
225
|
+
exec_ = executor or default_executor()
|
|
226
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
227
|
+
snapshot = stop_runtime(
|
|
228
|
+
runtime_id=runtime_id, executor=exec_, registry_root=reg_root,
|
|
229
|
+
grace_seconds=grace_seconds,
|
|
230
|
+
)
|
|
231
|
+
return snapshot.to_dict()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def cmd_runtime_restart(
|
|
235
|
+
runtime_id: str,
|
|
236
|
+
workspace: Optional[str] = None,
|
|
237
|
+
executor: Optional[DockerExecutor] = None,
|
|
238
|
+
) -> Dict[str, Any]:
|
|
239
|
+
"""Execute ``aisc runtime restart`` per contract §5.5."""
|
|
240
|
+
exec_ = executor or default_executor()
|
|
241
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
242
|
+
snapshot = restart_runtime(runtime_id=runtime_id, executor=exec_, registry_root=reg_root)
|
|
243
|
+
return snapshot.to_dict()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def cmd_runtime_remove(
|
|
247
|
+
runtime_id: str,
|
|
248
|
+
workspace: Optional[str] = None,
|
|
249
|
+
force: bool = False,
|
|
250
|
+
executor: Optional[DockerExecutor] = None,
|
|
251
|
+
) -> Dict[str, Any]:
|
|
252
|
+
"""Execute ``aisc runtime remove`` per contract §5.5."""
|
|
253
|
+
exec_ = executor or default_executor()
|
|
254
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
255
|
+
snapshot = remove_runtime(
|
|
256
|
+
runtime_id=runtime_id, executor=exec_, registry_root=reg_root, force=force
|
|
257
|
+
)
|
|
258
|
+
return snapshot.to_dict()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# svc-2: `aisc runtime services` (aisc.runtime-services/v1)
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def cmd_runtime_services(
|
|
266
|
+
runtime_id: str,
|
|
267
|
+
workspace: Optional[str] = None,
|
|
268
|
+
executor: Optional[DockerExecutor] = None,
|
|
269
|
+
) -> Dict[str, Any]:
|
|
270
|
+
"""Execute ``aisc runtime services`` — gateway info + registered services."""
|
|
271
|
+
from aisc.application.web_gateway import runtime_services
|
|
272
|
+
|
|
273
|
+
exec_ = executor or default_executor()
|
|
274
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
275
|
+
return runtime_services(
|
|
276
|
+
runtime_id=runtime_id, executor=exec_, registry_root=reg_root
|
|
277
|
+
).to_dict()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def cmd_runtime_services_expose(
|
|
281
|
+
runtime_id: str,
|
|
282
|
+
port: str,
|
|
283
|
+
name: str = "",
|
|
284
|
+
workspace: Optional[str] = None,
|
|
285
|
+
executor: Optional[DockerExecutor] = None,
|
|
286
|
+
) -> Dict[str, Any]:
|
|
287
|
+
"""Execute ``aisc runtime services expose`` (ops/test entry; agents use
|
|
288
|
+
the in-container helper — both write the same manifest)."""
|
|
289
|
+
from aisc.application.web_gateway import expose_runtime_service
|
|
290
|
+
|
|
291
|
+
exec_ = executor or default_executor()
|
|
292
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
293
|
+
return expose_runtime_service(
|
|
294
|
+
runtime_id=runtime_id, port_text=port, name=name,
|
|
295
|
+
executor=exec_, registry_root=reg_root,
|
|
296
|
+
).to_dict()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cmd_runtime_services_unexpose(
|
|
300
|
+
runtime_id: str,
|
|
301
|
+
port: str,
|
|
302
|
+
workspace: Optional[str] = None,
|
|
303
|
+
executor: Optional[DockerExecutor] = None,
|
|
304
|
+
) -> Dict[str, Any]:
|
|
305
|
+
"""Execute ``aisc runtime services unexpose``."""
|
|
306
|
+
from aisc.application.web_gateway import unexpose_runtime_service
|
|
307
|
+
|
|
308
|
+
exec_ = executor or default_executor()
|
|
309
|
+
_ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
310
|
+
return unexpose_runtime_service(
|
|
311
|
+
runtime_id=runtime_id, port_text=port,
|
|
312
|
+
executor=exec_, registry_root=reg_root,
|
|
313
|
+
).to_dict()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def cmd_runtime_reconcile(
|
|
317
|
+
workspace: Optional[str] = None,
|
|
318
|
+
instance_id: str = "",
|
|
319
|
+
workspace_key: Optional[str] = None,
|
|
320
|
+
executor: Optional[DockerExecutor] = None,
|
|
321
|
+
) -> Dict[str, Any]:
|
|
322
|
+
"""Execute ``aisc runtime reconcile`` (runtime-lifecycle-ux 02 §3).
|
|
323
|
+
|
|
324
|
+
One idempotent classification+cleanup pass. The command succeeds
|
|
325
|
+
(exit 0) even when ``can_proceed`` is false — a blocked classification
|
|
326
|
+
is a valid answer, not a transport error.
|
|
327
|
+
"""
|
|
328
|
+
from aisc.application.workspace_reconcile import reconcile_workspace
|
|
329
|
+
|
|
330
|
+
exec_ = executor or default_executor()
|
|
331
|
+
ws, reg_root = _resolve_workspace_and_registry(workspace)
|
|
332
|
+
payload = reconcile_workspace(ws, instance_id, exec_, registry_root=reg_root)
|
|
333
|
+
if workspace_key and payload.get("workspace_key") != workspace_key:
|
|
334
|
+
raise CliError(
|
|
335
|
+
message=(
|
|
336
|
+
"workspace key mismatch: caller-supplied key does not match "
|
|
337
|
+
"the canonical workspace (wrong workspace path?)"
|
|
338
|
+
),
|
|
339
|
+
exit_code=RuntimeExitCode.USAGE_ERROR,
|
|
340
|
+
error_code=RuntimeErrorCode.WORKSPACE_INVALID,
|
|
341
|
+
data={"expected": workspace_key, "actual": payload.get("workspace_key")},
|
|
342
|
+
)
|
|
343
|
+
return payload
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def cmd_runtime_lease(
|
|
347
|
+
action: str,
|
|
348
|
+
workspace: Optional[str] = None,
|
|
349
|
+
instance_id: str = "",
|
|
350
|
+
lease_id: Optional[str] = None,
|
|
351
|
+
) -> Dict[str, Any]:
|
|
352
|
+
"""Execute ``aisc runtime lease {claim,heartbeat,release,inspect}``.
|
|
353
|
+
|
|
354
|
+
The lease rides the data-root layout (02 §2.3); the Rust heartbeat
|
|
355
|
+
task (D-RUNTIME-12) calls ``heartbeat`` through this command on its
|
|
356
|
+
interval. ``claim`` conflicts raise AISC_ERR_ACTIVE_WORKSPACE_LEASE;
|
|
357
|
+
``heartbeat`` after a takeover raises AISC_ERR_RUNTIME_LEASE_CONFLICT.
|
|
358
|
+
"""
|
|
359
|
+
import uuid as _uuid
|
|
360
|
+
|
|
361
|
+
from aisc.adapters.data_root_store import DataRootStore
|
|
362
|
+
from aisc.adapters.workspace_lease_store import WorkspaceLeaseStore
|
|
363
|
+
from aisc.application.data_root import DataRootResolver
|
|
364
|
+
from aisc.application.runtime import workspace_key_for
|
|
365
|
+
|
|
366
|
+
ws = str(Path(workspace or os.getcwd()).resolve())
|
|
367
|
+
resolved = DataRootResolver().resolve(Path(ws))
|
|
368
|
+
store = DataRootStore(resolved)
|
|
369
|
+
store.prepare()
|
|
370
|
+
leases = WorkspaceLeaseStore(store, workspace_key_for(ws))
|
|
371
|
+
|
|
372
|
+
if action == "inspect":
|
|
373
|
+
lease = leases.inspect()
|
|
374
|
+
payload = lease.to_dict() if lease else {"lease": None}
|
|
375
|
+
return {"action": "inspect", "workspace_key": workspace_key_for(ws), **payload}
|
|
376
|
+
if action == "claim":
|
|
377
|
+
if not instance_id:
|
|
378
|
+
raise CliError(
|
|
379
|
+
message="lease claim requires --instance-id",
|
|
380
|
+
exit_code=RuntimeExitCode.USAGE_ERROR,
|
|
381
|
+
error_code=RuntimeErrorCode.RUNTIME_LEASE_CONFLICT,
|
|
382
|
+
)
|
|
383
|
+
lease, outcome = leases.claim(instance_id, lease_id=lease_id)
|
|
384
|
+
return {
|
|
385
|
+
"action": "claim", "outcome": outcome,
|
|
386
|
+
"workspace_key": workspace_key_for(ws), **lease.to_dict(),
|
|
387
|
+
}
|
|
388
|
+
if action == "heartbeat":
|
|
389
|
+
lease = leases.heartbeat(instance_id, lease_id or "")
|
|
390
|
+
return {
|
|
391
|
+
"action": "heartbeat",
|
|
392
|
+
"workspace_key": workspace_key_for(ws),
|
|
393
|
+
**(lease.to_dict() if lease else {"lease": None}),
|
|
394
|
+
}
|
|
395
|
+
if action == "release":
|
|
396
|
+
released = leases.release(instance_id, lease_id or "")
|
|
397
|
+
return {
|
|
398
|
+
"action": "release", "released": released,
|
|
399
|
+
"workspace_key": workspace_key_for(ws),
|
|
400
|
+
}
|
|
401
|
+
raise CliError(
|
|
402
|
+
message=f"unknown lease action: {action}",
|
|
403
|
+
exit_code=RuntimeExitCode.USAGE_ERROR,
|
|
404
|
+
error_code=RuntimeErrorCode.RUNTIME_LEASE_CONFLICT,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def print_runtime_text(subcommand: str, data: Any, errors: list) -> None:
|
|
409
|
+
"""Minimal human-readable output for ``aisc runtime`` text mode."""
|
|
410
|
+
if errors:
|
|
411
|
+
for e in errors:
|
|
412
|
+
print(f"Error: {e.get('message', '')}")
|
|
413
|
+
return
|
|
414
|
+
if not isinstance(data, dict):
|
|
415
|
+
return
|
|
416
|
+
if subcommand == "preflight":
|
|
417
|
+
print(f"can_start: {data.get('can_start')}")
|
|
418
|
+
print(f"recommended_action: {data.get('recommended_action')}")
|
|
419
|
+
for c in data.get("checks", []):
|
|
420
|
+
print(f" {c.get('id')}: {c.get('status')}")
|
|
421
|
+
elif subcommand == "start":
|
|
422
|
+
cfg = data.get("config", {})
|
|
423
|
+
print(f"runtime {data.get('runtime_id', '')}")
|
|
424
|
+
print(f" container: {data.get('container_name', '')} ({data.get('container_id', '')})")
|
|
425
|
+
print(f" state: {data.get('state', '')} ready: {data.get('ready', '')} "
|
|
426
|
+
f"reused: {data.get('reused', '')}")
|
|
427
|
+
print(f" image: {cfg.get('image', '')} network: {cfg.get('network', '')} "
|
|
428
|
+
f"scope: {cfg.get('scope', '')}")
|
|
429
|
+
elif subcommand == "list":
|
|
430
|
+
runtimes = data.get("runtimes", [])
|
|
431
|
+
if not runtimes:
|
|
432
|
+
print("(no runtimes)")
|
|
433
|
+
for r in runtimes:
|
|
434
|
+
print(f"{r.get('runtime_id', '')} {r.get('container_name', '')} "
|
|
435
|
+
f"state={r.get('state', '')} registry={r.get('registry_state', '')}")
|
|
436
|
+
elif subcommand in ("inspect", "stop", "restart", "remove"):
|
|
437
|
+
print(f"runtime {data.get('runtime_id', '')}")
|
|
438
|
+
print(f" container: {data.get('container_name', '')}")
|
|
439
|
+
print(f" state: {data.get('state', '')} registry: {data.get('registry_state', '')}")
|
|
440
|
+
elif subcommand == "services":
|
|
441
|
+
gateway = data.get("gateway", {})
|
|
442
|
+
print(f"gateway: {gateway.get('state', '')} "
|
|
443
|
+
f"host: {gateway.get('host', '')}:{gateway.get('host_port', '')}")
|
|
444
|
+
if gateway.get("reason"):
|
|
445
|
+
print(f" reason: {gateway.get('reason')}")
|
|
446
|
+
services = data.get("services", [])
|
|
447
|
+
if not services:
|
|
448
|
+
print("(no services registered; in-container: aisc-web-expose <port>)")
|
|
449
|
+
for s in services:
|
|
450
|
+
print(f" {s.get('port')} {s.get('name') or '-'} {s.get('url')}")
|