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.
Files changed (92) hide show
  1. aisc/VERSION +1 -0
  2. aisc/__init__.py +49 -0
  3. aisc/__main__.py +6 -0
  4. aisc/adapters/__init__.py +1 -0
  5. aisc/adapters/config_reader.py +215 -0
  6. aisc/adapters/container_registry.py +718 -0
  7. aisc/adapters/data_root_store.py +296 -0
  8. aisc/adapters/docker_.py +1183 -0
  9. aisc/adapters/docker_gateway.py +1244 -0
  10. aisc/adapters/docker_sdk_backed.py +274 -0
  11. aisc/adapters/maintenance_lock.py +97 -0
  12. aisc/adapters/state_file.py +184 -0
  13. aisc/adapters/system.py +101 -0
  14. aisc/adapters/windows_config_reader.py +249 -0
  15. aisc/adapters/workspace_lease_store.py +189 -0
  16. aisc/application/__init__.py +1 -0
  17. aisc/application/artifact.py +265 -0
  18. aisc/application/bundle_fetch.py +836 -0
  19. aisc/application/cc_switch_provider.py +245 -0
  20. aisc/application/cc_switch_resolver.py +363 -0
  21. aisc/application/config_service.py +452 -0
  22. aisc/application/conversation.py +516 -0
  23. aisc/application/data_migration.py +440 -0
  24. aisc/application/data_root.py +275 -0
  25. aisc/application/docker_lifecycle.py +659 -0
  26. aisc/application/doctor.py +821 -0
  27. aisc/application/legacy_scan.py +252 -0
  28. aisc/application/network_subscription.py +642 -0
  29. aisc/application/profile_service.py +105 -0
  30. aisc/application/provider.py +92 -0
  31. aisc/application/repair.py +387 -0
  32. aisc/application/resources.py +270 -0
  33. aisc/application/runtime.py +1826 -0
  34. aisc/application/session.py +320 -0
  35. aisc/application/tool_versions.py +210 -0
  36. aisc/application/toolchain.py +91 -0
  37. aisc/application/update.py +286 -0
  38. aisc/application/usage.py +305 -0
  39. aisc/application/version.py +133 -0
  40. aisc/application/web_gateway.py +459 -0
  41. aisc/application/workspace_reconcile.py +318 -0
  42. aisc/applog.py +103 -0
  43. aisc/cli/__init__.py +1 -0
  44. aisc/cli/build_progress_parser.py +217 -0
  45. aisc/cli/commands/__init__.py +1 -0
  46. aisc/cli/commands/agents.py +117 -0
  47. aisc/cli/commands/artifact.py +210 -0
  48. aisc/cli/commands/build.py +566 -0
  49. aisc/cli/commands/bundle.py +107 -0
  50. aisc/cli/commands/cc_switch.py +158 -0
  51. aisc/cli/commands/config.py +104 -0
  52. aisc/cli/commands/container.py +787 -0
  53. aisc/cli/commands/conversation.py +72 -0
  54. aisc/cli/commands/data_root.py +84 -0
  55. aisc/cli/commands/logs.py +89 -0
  56. aisc/cli/commands/network.py +132 -0
  57. aisc/cli/commands/profile.py +52 -0
  58. aisc/cli/commands/provider.py +49 -0
  59. aisc/cli/commands/run.py +494 -0
  60. aisc/cli/commands/runs.py +125 -0
  61. aisc/cli/commands/runtime.py +450 -0
  62. aisc/cli/commands/serve.py +550 -0
  63. aisc/cli/commands/serve_fs.py +369 -0
  64. aisc/cli/commands/session.py +167 -0
  65. aisc/cli/commands/update.py +145 -0
  66. aisc/cli/commands/usage.py +74 -0
  67. aisc/cli/commands/wizard.py +130 -0
  68. aisc/cli/commands/workspaces.py +162 -0
  69. aisc/cli/help_i18n.py +385 -0
  70. aisc/cli/main.py +2926 -0
  71. aisc/cli/output.py +239 -0
  72. aisc/cli/tutorial.py +109 -0
  73. aisc/domain/__init__.py +1 -0
  74. aisc/domain/artifacts.py +279 -0
  75. aisc/domain/cc_switch_release.py +303 -0
  76. aisc/domain/config.py +64 -0
  77. aisc/domain/data_migration.py +259 -0
  78. aisc/domain/data_root.py +124 -0
  79. aisc/domain/docker_ownership.py +148 -0
  80. aisc/domain/gateway.py +168 -0
  81. aisc/domain/models.py +674 -0
  82. aisc/domain/toolchain.py +72 -0
  83. aisc/domain/web_services.py +355 -0
  84. aisc/domain/workspace_lease.py +139 -0
  85. aisc/schemas/__init__.py +1 -0
  86. aisc/schemas/config_schema.py +145 -0
  87. aisc_cli-0.1.0.dist-info/METADATA +420 -0
  88. aisc_cli-0.1.0.dist-info/RECORD +92 -0
  89. aisc_cli-0.1.0.dist-info/WHEEL +5 -0
  90. aisc_cli-0.1.0.dist-info/entry_points.txt +2 -0
  91. aisc_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  92. aisc_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -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