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
aisc/domain/data_root.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Data root domain contract (Stage 7, DATA-01..04, D7-01/02/05).
|
|
2
|
+
|
|
3
|
+
The single source of truth for the canonical Windows data layout:
|
|
4
|
+
``%LOCALAPPDATA%\\AISC\\data`` (see 02-domain-contract.md). Everything AISC
|
|
5
|
+
auto-generates — config, state, runtime, logs, cache, artifacts, diagnostics,
|
|
6
|
+
migrations — lives under one resolver-produced root; the workspace keeps only
|
|
7
|
+
user files.
|
|
8
|
+
|
|
9
|
+
This module is PURE (no env, no filesystem): canonicalization helpers, the
|
|
10
|
+
versioned workspace hash, the layout contract and the structured resolve
|
|
11
|
+
result. Env/platform selection and containment probing live in
|
|
12
|
+
``aisc.application.data_root.DataRootResolver``; the Rust mirror is
|
|
13
|
+
``workbench/src-tauri/src/data_root.rs`` (kept in sync via
|
|
14
|
+
``tests/fixtures/data-root/hash-vectors.json``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Dict
|
|
23
|
+
|
|
24
|
+
DATA_ROOT_PROTOCOL = "aisc.data-root/v1"
|
|
25
|
+
DATA_ROOT_SCHEMA_VERSION = 1
|
|
26
|
+
|
|
27
|
+
# Versioned workspace isolation hash (D7-02). ``sha256-v1:<64 hex>`` in JSON
|
|
28
|
+
# fields; the directory-name form swaps the colon for a dash (':' is illegal
|
|
29
|
+
# in Windows filenames).
|
|
30
|
+
WORKSPACE_HASH_ALGO = "sha256-v1"
|
|
31
|
+
|
|
32
|
+
# Shared-root layout (02-domain-contract.md "Canonical layout"). Order is the
|
|
33
|
+
# contract order; consumers must not invent siblings.
|
|
34
|
+
SHARED_SUBDIRS = ("config", "state", "workspaces", "artifacts", "cache", "diagnostics", "logs", "migrations")
|
|
35
|
+
|
|
36
|
+
# Per-workspace layout under workspaces/<hash>/.
|
|
37
|
+
WORKSPACE_SUBDIRS = ("claude", "codex", "cc-switch", "runtime", "logs")
|
|
38
|
+
|
|
39
|
+
# Locks live under state/ (contract: "locks/indexes"). Workspace-scoped
|
|
40
|
+
# locks share the dir with a hash prefix, keeping the contract layout intact.
|
|
41
|
+
LOCKS_SUBDIR = "state/locks"
|
|
42
|
+
|
|
43
|
+
# Stable error codes (exit mapping is the caller's job). Domain-owned so the
|
|
44
|
+
# application resolver and adapter writers share one spelling.
|
|
45
|
+
ERR_OVERRIDE_RELATIVE = "AISC_ERR_DATA_ROOT_OVERRIDE_RELATIVE"
|
|
46
|
+
ERR_REPARSE_POINT = "AISC_ERR_DATA_ROOT_REPARSE_POINT"
|
|
47
|
+
ERR_WORKSPACE_OVERLAP = "AISC_ERR_DATA_ROOT_WORKSPACE_OVERLAP"
|
|
48
|
+
ERR_LOCK_TIMEOUT = "AISC_ERR_DATA_ROOT_LOCK_TIMEOUT"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def strip_verbatim(path_str: str) -> str:
|
|
52
|
+
"""Drop Windows verbatim (``\\\\?\\``) prefixes so both languages hash the
|
|
53
|
+
same string (Python ``resolve()`` usually omits the prefix, Rust
|
|
54
|
+
``canonicalize()`` always adds it)."""
|
|
55
|
+
if path_str.startswith("\\\\?\\UNC\\"):
|
|
56
|
+
return "\\\\" + path_str[len("\\\\?\\UNC\\"):]
|
|
57
|
+
if path_str.startswith("\\\\?\\"):
|
|
58
|
+
return path_str[len("\\\\?\\"):]
|
|
59
|
+
return path_str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def canonical_workspace_path(workspace: Path) -> str:
|
|
63
|
+
"""Canonical absolute path string used as the hash input (non-strict:
|
|
64
|
+
a missing workspace resolves as-given, matching domain/artifacts.py)."""
|
|
65
|
+
return strip_verbatim(str(Path(workspace).resolve()))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def hash_canonical_path(canon: str) -> str:
|
|
69
|
+
"""Pure hash of an ALREADY-canonical path string → ``sha256-v1:<64 hex>``.
|
|
70
|
+
|
|
71
|
+
Full digest (not the 16-hex short form used by the Stage-3 artifact
|
|
72
|
+
registry): collision tests in 04-observability-testing rely on the whole
|
|
73
|
+
digest, and D7-02 asks for the versioned form.
|
|
74
|
+
"""
|
|
75
|
+
return WORKSPACE_HASH_ALGO + ":" + hashlib.sha256(canon.encode("utf-8")).hexdigest()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def workspace_hash_v1(workspace: Path) -> str:
|
|
79
|
+
"""Versioned hash of a workspace path (irreversible; never stores the raw
|
|
80
|
+
path in a directory name — D7-02)."""
|
|
81
|
+
return hash_canonical_path(canonical_workspace_path(workspace))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def workspace_dir_name(workspace_hash: str) -> str:
|
|
85
|
+
"""Windows-safe directory form: ``sha256-v1:<hex>`` → ``sha256-v1-<hex>``."""
|
|
86
|
+
return workspace_hash.replace(":", "-", 1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ResolvedDataRoot:
|
|
91
|
+
"""Structured resolver result (schema/versioned, 01-cross-stage-contracts
|
|
92
|
+
§1: callers never concatenate data-root paths themselves).
|
|
93
|
+
|
|
94
|
+
``workspace`` is deliberately NOT in ``to_dict()`` — doctor/diagnostics
|
|
95
|
+
expose the hash, not the raw workspace path, unless the user explicitly
|
|
96
|
+
exports it (04-observability-testing "证据与脱敏").
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
schema: str = DATA_ROOT_PROTOCOL
|
|
100
|
+
schema_version: int = DATA_ROOT_SCHEMA_VERSION
|
|
101
|
+
root: Path = Path()
|
|
102
|
+
origin: str = "default" # "default" | "env" (AISC_DATA_ROOT override)
|
|
103
|
+
workspace_hash: str = ""
|
|
104
|
+
writable: bool = False
|
|
105
|
+
# Relative-name → absolute-path maps (contract order).
|
|
106
|
+
shared_dirs: Dict[str, Path] = field(default_factory=dict)
|
|
107
|
+
workspace_dirs: Dict[str, Path] = field(default_factory=dict)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def workspace_dir(self) -> Path:
|
|
111
|
+
"""workspaces/<hash>/ — the per-workspace subtree root."""
|
|
112
|
+
return self.shared_dirs["workspaces"] / workspace_dir_name(self.workspace_hash)
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> Dict[str, str]:
|
|
115
|
+
return {
|
|
116
|
+
"schema": self.schema,
|
|
117
|
+
"schema_version": self.schema_version,
|
|
118
|
+
"root": str(self.root),
|
|
119
|
+
"origin": self.origin,
|
|
120
|
+
"workspace_hash": self.workspace_hash,
|
|
121
|
+
"writable": self.writable,
|
|
122
|
+
"shared_dirs": {k: str(v) for k, v in self.shared_dirs.items()},
|
|
123
|
+
"workspace_dirs": {k: str(v) for k, v in self.workspace_dirs.items()},
|
|
124
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Docker resource ownership contract (A0 / docker-ownership-foundation).
|
|
2
|
+
|
|
3
|
+
Single source of truth for the ownership labels shared by the
|
|
4
|
+
docker-resource-lifecycle and runtime-lifecycle-ux plans:
|
|
5
|
+
|
|
6
|
+
- docs/plans/docker-resource-lifecycle/02-domain-contract.md §1
|
|
7
|
+
- docs/plans/docker-resource-lifecycle/05-cross-plan-coordination.md §1
|
|
8
|
+
|
|
9
|
+
Containers and volumes carry ``io.aisc.*`` labels; workstation images carry
|
|
10
|
+
``org.aisc.*`` provenance labels (alongside the existing
|
|
11
|
+
``org.aisc.cc-switch.*`` build labels). Labels are the primary ownership
|
|
12
|
+
proof — name prefixes are legacy-compatibility evidence only and never the
|
|
13
|
+
sole mechanism for new resources.
|
|
14
|
+
|
|
15
|
+
Mirrored in Rust ``workbench/src-tauri/src/docker_ownership.rs`` and frozen
|
|
16
|
+
in ``tests/fixtures/docker-ownership/labels.json`` (the three-language
|
|
17
|
+
consistency gate for this stage; see the hash-vectors precedent in
|
|
18
|
+
``tests/fixtures/data-root/``).
|
|
19
|
+
|
|
20
|
+
Nothing here performs I/O and nothing here is secret-bearing: label keys,
|
|
21
|
+
kind/owner values and schema versions only. Docker argv assembly for
|
|
22
|
+
container/volume creation and the three-tier classification service
|
|
23
|
+
(owned / legacy_owned / unverified) live in application/adapters layers
|
|
24
|
+
(docker-resource-lifecycle stages A1-A2).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Dict, List, Mapping
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Frozen constants (mirrored in Rust docker_ownership.rs / the labels fixture)
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
#: Marker label proving AISC ownership. Value is always ``true``.
|
|
36
|
+
LABEL_MANAGED = "io.aisc.managed"
|
|
37
|
+
|
|
38
|
+
#: Resource kind. One of :data:`KIND_*`.
|
|
39
|
+
LABEL_KIND = "io.aisc.kind"
|
|
40
|
+
|
|
41
|
+
#: Creating component. One of :data:`OWNER_*`.
|
|
42
|
+
LABEL_OWNER = "io.aisc.owner"
|
|
43
|
+
|
|
44
|
+
#: Ownership label schema version (label value; Docker labels are strings).
|
|
45
|
+
LABEL_SCHEMA_VERSION = "io.aisc.schema-version"
|
|
46
|
+
|
|
47
|
+
#: Workspace hash scoping a resource to one workspace (containers, volumes).
|
|
48
|
+
LABEL_WORKSPACE_KEY = "io.aisc.workspace-key"
|
|
49
|
+
|
|
50
|
+
#: Runtime UUID — runtime containers only.
|
|
51
|
+
LABEL_RUNTIME_ID = "io.aisc.runtime-id"
|
|
52
|
+
|
|
53
|
+
#: Image provenance labels (``org.aisc.*`` namespace).
|
|
54
|
+
IMAGE_LABEL_MANAGED = "org.aisc.managed"
|
|
55
|
+
IMAGE_LABEL_KIND = "org.aisc.kind"
|
|
56
|
+
IMAGE_LABEL_SCHEMA_VERSION = "org.aisc.schema-version"
|
|
57
|
+
IMAGE_LABEL_SOURCE_VERSION = "org.aisc.source-version"
|
|
58
|
+
|
|
59
|
+
# --- label values -----------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
KIND_RUNTIME = "runtime"
|
|
62
|
+
KIND_ONE_SHOT = "one-shot"
|
|
63
|
+
KIND_TOOLCHAIN = "toolchain"
|
|
64
|
+
|
|
65
|
+
OWNER_WORKBENCH = "workbench"
|
|
66
|
+
OWNER_CLI = "cli"
|
|
67
|
+
|
|
68
|
+
#: The only image kind v1 defines (workstation images from ``aisc build``).
|
|
69
|
+
IMAGE_KIND_WORKSTATION = "workstation-image"
|
|
70
|
+
|
|
71
|
+
#: Value of ``io.aisc.schema-version`` / ``org.aisc.schema-version``.
|
|
72
|
+
OWNERSHIP_LABEL_SCHEMA_V1 = "1"
|
|
73
|
+
|
|
74
|
+
#: ``schema_version`` integer for the maintenance scan/cleanup JSON
|
|
75
|
+
#: envelopes (docker-resource-lifecycle stage B consumes this).
|
|
76
|
+
OWNERSHIP_SCHEMA_V1 = 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Pure label-set builders (consumed by the creation entry points, stage A2)
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def base_labels(kind: str, owner: str) -> Dict[str, str]:
|
|
84
|
+
"""Managed + kind + owner + schema-version — the minimum ownership set."""
|
|
85
|
+
return {
|
|
86
|
+
LABEL_MANAGED: "true",
|
|
87
|
+
LABEL_KIND: kind,
|
|
88
|
+
LABEL_OWNER: owner,
|
|
89
|
+
LABEL_SCHEMA_VERSION: OWNERSHIP_LABEL_SCHEMA_V1,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def runtime_labels(runtime_id: str, owner: str, workspace_key: str) -> Dict[str, str]:
|
|
94
|
+
"""Label set for a Workbench runtime container (``kind=runtime``)."""
|
|
95
|
+
labels = base_labels(KIND_RUNTIME, owner)
|
|
96
|
+
labels[LABEL_RUNTIME_ID] = runtime_id
|
|
97
|
+
labels[LABEL_WORKSPACE_KEY] = workspace_key
|
|
98
|
+
return labels
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def one_shot_labels(owner: str = OWNER_CLI) -> Dict[str, str]:
|
|
102
|
+
"""Label set for a CLI one-shot container (``aisc run``)."""
|
|
103
|
+
return base_labels(KIND_ONE_SHOT, owner)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def toolchain_volume_labels(workspace_key: str) -> Dict[str, str]:
|
|
107
|
+
"""Label set for a persistent project toolchain volume.
|
|
108
|
+
|
|
109
|
+
``kind=toolchain`` volumes are deleted only by the explicit workspace
|
|
110
|
+
runtime-data cleanup — never by generic Docker cleanup
|
|
111
|
+
(02-domain-contract.md §3.1).
|
|
112
|
+
"""
|
|
113
|
+
labels = base_labels(KIND_TOOLCHAIN, OWNER_WORKBENCH)
|
|
114
|
+
labels[LABEL_WORKSPACE_KEY] = workspace_key
|
|
115
|
+
return labels
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def image_labels(source_version: str) -> Dict[str, str]:
|
|
119
|
+
"""Provenance label set for workstation images built by ``aisc build``."""
|
|
120
|
+
return {
|
|
121
|
+
IMAGE_LABEL_MANAGED: "true",
|
|
122
|
+
IMAGE_LABEL_KIND: IMAGE_KIND_WORKSTATION,
|
|
123
|
+
IMAGE_LABEL_SCHEMA_VERSION: OWNERSHIP_LABEL_SCHEMA_V1,
|
|
124
|
+
IMAGE_LABEL_SOURCE_VERSION: source_version,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def label_args(labels: Mapping[str, str]) -> List[str]:
|
|
129
|
+
"""Flatten a label mapping to deterministic ``--label k=v`` argv tokens.
|
|
130
|
+
|
|
131
|
+
Sorted by key so generated argv (and its test snapshots) are stable
|
|
132
|
+
regardless of dict construction order.
|
|
133
|
+
"""
|
|
134
|
+
args: List[str] = []
|
|
135
|
+
for key in sorted(labels):
|
|
136
|
+
args.extend(("--label", f"{key}={labels[key]}"))
|
|
137
|
+
return args
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def managed_filter(kind: str = None) -> str:
|
|
141
|
+
"""``docker --filter`` value selecting AISC-managed resources.
|
|
142
|
+
|
|
143
|
+
Optionally narrowed to one kind — e.g. ``label=io.aisc.managed=true``
|
|
144
|
+
or ``label=io.aisc.kind=toolchain``.
|
|
145
|
+
"""
|
|
146
|
+
if kind is not None:
|
|
147
|
+
return f"label={LABEL_KIND}={kind}"
|
|
148
|
+
return f"label={LABEL_MANAGED}=true"
|
aisc/domain/gateway.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""DockerGateway domain models (Stage 4, DG-02).
|
|
2
|
+
|
|
3
|
+
Every gateway operation returns a structured result carrying the cross-cutting
|
|
4
|
+
facts the contract requires: ``operation_id``, ``backend``, observed state,
|
|
5
|
+
exit code, duration, stable error/cleanup status. ``stdout``/``stderr`` are
|
|
6
|
+
only populated when the caller explicitly requests capture and are bounded.
|
|
7
|
+
|
|
8
|
+
These are pure data models — no I/O, no docker-py import. The adapter
|
|
9
|
+
(``aisc.adapters.docker_gateway``) maps Engine/SDK/CLI results onto them so
|
|
10
|
+
application code never sees backend-specific shapes (D4-06).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Operation envelope
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class GatewayOperation:
|
|
25
|
+
"""Cross-cutting envelope for one gateway operation.
|
|
26
|
+
|
|
27
|
+
``backend`` is ``sdk`` | ``cli`` | ``auto`` (what actually ran); the
|
|
28
|
+
application must not branch on it — it exists for diagnostics and the
|
|
29
|
+
recording/fault-injection matrix (D4-07).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
operation_id: str = ""
|
|
33
|
+
backend: str = "auto"
|
|
34
|
+
exit_code: int = -1
|
|
35
|
+
duration_ms: int = 0
|
|
36
|
+
error_code: str = "" # stable: AISC_* or DockerErrorCode
|
|
37
|
+
error_message: str = ""
|
|
38
|
+
cleanup_status: str = "none" # none | ok | partial | failed
|
|
39
|
+
timed_out: bool = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class GatewayResult:
|
|
44
|
+
"""Base result: operation envelope + optional captured output."""
|
|
45
|
+
|
|
46
|
+
operation: GatewayOperation = field(default_factory=GatewayOperation)
|
|
47
|
+
stdout: str = ""
|
|
48
|
+
stderr: str = ""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def ok(self) -> bool:
|
|
52
|
+
return self.operation.exit_code == 0 and not self.operation.error_code
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def timed_out(self) -> bool:
|
|
56
|
+
return self.operation.timed_out
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def exit_code(self) -> int:
|
|
60
|
+
"""Convenience alias for ``operation.exit_code``."""
|
|
61
|
+
return self.operation.exit_code
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Query results
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class PreflightResult(GatewayResult):
|
|
70
|
+
"""Gateway preflight: daemon reachability + docker availability."""
|
|
71
|
+
|
|
72
|
+
available: bool = False
|
|
73
|
+
reason: str = "" # "ok" | "cli_not_found" | "daemon_unreachable" | "permission_denied"
|
|
74
|
+
docker_path: str = ""
|
|
75
|
+
docker_version: str = ""
|
|
76
|
+
engine_ok: bool = False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ImageInspectGatewayResult(GatewayResult):
|
|
81
|
+
"""Structured image inspection.
|
|
82
|
+
|
|
83
|
+
``status`` reuses :class:`aisc.domain.models.ImageInspectStatus` values
|
|
84
|
+
(exists | missing | docker_unavailable | permission_denied | timeout | error).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
status: str = "error"
|
|
88
|
+
image: str = ""
|
|
89
|
+
message: str = ""
|
|
90
|
+
detail: Dict[str, Any] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ContainerSummary:
|
|
95
|
+
"""One container row from ``list_containers``."""
|
|
96
|
+
|
|
97
|
+
id: str = ""
|
|
98
|
+
name: str = ""
|
|
99
|
+
image: str = ""
|
|
100
|
+
state: str = "" # running | exited | created | restarting | paused | dead
|
|
101
|
+
status: str = "" # human string e.g. "Up 2 hours" / "Exited (0) 1 minute ago"
|
|
102
|
+
labels: Dict[str, str] = field(default_factory=dict)
|
|
103
|
+
# Content-addressed image ID (sha256:...) — A0 docker-ownership-
|
|
104
|
+
# foundation. Empty when the backend cannot provide it cheaply
|
|
105
|
+
# (CLI `docker ps` only knows the image REF); callers needing the ID
|
|
106
|
+
# inspect the specific container instead of guessing from the ref.
|
|
107
|
+
image_id: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class ContainerListResult(GatewayResult):
|
|
112
|
+
"""Container listing with stable per-row summary."""
|
|
113
|
+
|
|
114
|
+
containers: List[ContainerSummary] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class ContainerInspectResult(GatewayResult):
|
|
119
|
+
"""Container inspection: id, name, state, image, labels, config."""
|
|
120
|
+
|
|
121
|
+
container_id: str = ""
|
|
122
|
+
name: str = ""
|
|
123
|
+
state: str = ""
|
|
124
|
+
image: str = ""
|
|
125
|
+
labels: Dict[str, str] = field(default_factory=dict)
|
|
126
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
127
|
+
# Content-addressed image ID (``.Image`` in docker inspect JSON) —
|
|
128
|
+
# distinct from ``image`` (the REF the container was created with).
|
|
129
|
+
image_id: str = ""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Lifecycle results
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class LifecycleResult(GatewayResult):
|
|
138
|
+
"""One lifecycle operation (start/stop/remove/wait/create)."""
|
|
139
|
+
|
|
140
|
+
target: str = "" # container name or id
|
|
141
|
+
observed_state: str = "" # e.g. running | stopped | removed | created | exited
|
|
142
|
+
container_id: str = ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Interactive / build results
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class InteractiveResult(GatewayResult):
|
|
151
|
+
"""Interactive exec session result.
|
|
152
|
+
|
|
153
|
+
``exit_code`` is the agent process exit code from ``exec_inspect``;
|
|
154
|
+
``session_id`` is the exec id for resize/cancel/wait.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
session_id: str = ""
|
|
158
|
+
resized: bool = False
|
|
159
|
+
waited: bool = False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class BuildResult(GatewayResult):
|
|
164
|
+
"""Image build result. Events are streamed via the caller's callback;
|
|
165
|
+
this carries the terminal exit code + image ref + duration."""
|
|
166
|
+
|
|
167
|
+
image_ref: str = ""
|
|
168
|
+
events_received: int = 0
|