codespeak-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.
- codespeak_cli-0.2.0.dist-info/METADATA +11 -0
- codespeak_cli-0.2.0.dist-info/RECORD +19 -0
- codespeak_cli-0.2.0.dist-info/WHEEL +4 -0
- codespeak_cli-0.2.0.dist-info/entry_points.txt +3 -0
- console_client/__init__.py +1 -0
- console_client/auth/__init__.py +1 -0
- console_client/auth/auth_manager.py +156 -0
- console_client/auth/callback_server.py +170 -0
- console_client/auth/exceptions.py +83 -0
- console_client/auth/oauth_pkce.py +134 -0
- console_client/auth/token_storage.py +122 -0
- console_client/build_client.py +318 -0
- console_client/build_client_event_converter.py +156 -0
- console_client/client_feature_flags.py +23 -0
- console_client/console_client_logging.py +220 -0
- console_client/console_client_main.py +348 -0
- console_client/os_environment_servicer.py +676 -0
- console_client/sequence_reorder_buffer.py +93 -0
- console_client/version.py +10 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Token storage module for managing user authentication tokens."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
# Buffer time in seconds before actual expiry to trigger re-login
|
|
11
|
+
_TOKEN_EXPIRY_BUFFER_SECONDS = 30
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StoredToken(BaseModel):
|
|
15
|
+
"""Token data stored in the token file."""
|
|
16
|
+
|
|
17
|
+
access_token: str
|
|
18
|
+
expires_at: int | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_codespeak_home() -> Path:
|
|
22
|
+
"""Get the CodeSpeak home directory path (~/.codespeak)."""
|
|
23
|
+
return Path.home() / ".codespeak"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ensure_codespeak_home() -> Path:
|
|
27
|
+
"""
|
|
28
|
+
Ensure the CodeSpeak home directory exists.
|
|
29
|
+
|
|
30
|
+
Creates the directory with secure permissions (0700) if it doesn't exist.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Path to the CodeSpeak home directory.
|
|
34
|
+
"""
|
|
35
|
+
codespeak_home = get_codespeak_home()
|
|
36
|
+
if not codespeak_home.exists():
|
|
37
|
+
codespeak_home.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
38
|
+
return codespeak_home
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_token_path() -> Path:
|
|
42
|
+
"""Get the path to the token file (~/.codespeak/token.json)."""
|
|
43
|
+
return get_codespeak_home() / "token.json"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_token(token: str, expires_in: int = 0) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Save the authentication token and its expiration time.
|
|
49
|
+
|
|
50
|
+
Creates the CodeSpeak home directory if it doesn't exist.
|
|
51
|
+
Sets file permissions to 0600 (owner read/write only) for security.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
token: The authentication token to save.
|
|
55
|
+
expires_in: Token lifetime in seconds from now. If 0, no expiry is saved.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
OSError: If the token cannot be saved due to filesystem errors.
|
|
59
|
+
"""
|
|
60
|
+
ensure_codespeak_home()
|
|
61
|
+
token_path = get_token_path()
|
|
62
|
+
|
|
63
|
+
expires_at = int(time.time()) + expires_in if expires_in > 0 else None
|
|
64
|
+
stored = StoredToken(access_token=token, expires_at=expires_at)
|
|
65
|
+
|
|
66
|
+
token_path.write_text(stored.model_dump_json(), encoding="utf-8")
|
|
67
|
+
os.chmod(token_path, 0o600)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_stored_token() -> StoredToken | None:
|
|
71
|
+
"""Load the stored token data from the token file."""
|
|
72
|
+
token_path = get_token_path()
|
|
73
|
+
if not token_path.exists():
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(token_path.read_text(encoding="utf-8"))
|
|
78
|
+
return StoredToken.model_validate(data)
|
|
79
|
+
except (json.JSONDecodeError, ValueError):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_user_token() -> str | None:
|
|
84
|
+
"""
|
|
85
|
+
Load the authentication token from the token file.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The authentication token if it exists, None otherwise.
|
|
89
|
+
"""
|
|
90
|
+
stored = _load_stored_token()
|
|
91
|
+
return stored.access_token if stored else None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def delete_token() -> None:
|
|
95
|
+
"""
|
|
96
|
+
Delete the authentication token file.
|
|
97
|
+
|
|
98
|
+
Does nothing if the file doesn't exist.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
OSError: If the file exists but cannot be deleted.
|
|
102
|
+
"""
|
|
103
|
+
token_path = get_token_path()
|
|
104
|
+
if token_path.exists():
|
|
105
|
+
token_path.unlink()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_token_expired() -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Check if the stored token is expired or about to expire.
|
|
111
|
+
|
|
112
|
+
Returns True if the token is expired or will expire within the buffer period,
|
|
113
|
+
giving time for re-login before actual expiry.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if token is expired or will expire soon, False otherwise.
|
|
117
|
+
"""
|
|
118
|
+
stored = _load_stored_token()
|
|
119
|
+
if not stored or stored.expires_at is None:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
return time.time() >= stored.expires_at - _TOKEN_EXPIRY_BUFFER_SECONDS
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build client for connecting to remote CodeSpeak build server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import queue
|
|
8
|
+
import threading
|
|
9
|
+
from argparse import Namespace
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
import grpc
|
|
14
|
+
from api_stubs import os_environment_pb2
|
|
15
|
+
from api_stubs.build_server_pb2 import (
|
|
16
|
+
CancelBuildRequest,
|
|
17
|
+
ClientMessage,
|
|
18
|
+
ServerMessage,
|
|
19
|
+
StartBuildRequest,
|
|
20
|
+
)
|
|
21
|
+
from api_stubs.build_server_pb2_grpc import BuildServiceStub
|
|
22
|
+
from api_stubs.operation_logging import OsEnvRequestLogging
|
|
23
|
+
from api_stubs.os_environment_pb2 import OperationRequest, OperationResponse
|
|
24
|
+
from codespeak_shared import BuildResult
|
|
25
|
+
from codespeak_shared.build_insight.events import (
|
|
26
|
+
BuildInsightEvent,
|
|
27
|
+
ProgressItemCreateEvent,
|
|
28
|
+
ProgressItemUpdateEvent,
|
|
29
|
+
SpanCloseEvent,
|
|
30
|
+
SpanOpenEvent,
|
|
31
|
+
TestsRunEvent,
|
|
32
|
+
TextOutputEvent,
|
|
33
|
+
ToolCallEvent,
|
|
34
|
+
UserVisibleModelOutputEvent,
|
|
35
|
+
)
|
|
36
|
+
from codespeak_shared.build_insight.storage import BuildInsightStorage, BuildInsightStorageMode
|
|
37
|
+
from codespeak_shared.codespeak_project import FileBasedCodeSpeakProject
|
|
38
|
+
from codespeak_shared.env_allowlist import filter_env_vars
|
|
39
|
+
from codespeak_shared.exceptions import (
|
|
40
|
+
BuildStartFailedUserError,
|
|
41
|
+
CodespeakInternalError,
|
|
42
|
+
CodeSpeakServerUnavailableUserError,
|
|
43
|
+
)
|
|
44
|
+
from codespeak_shared.os_environment import OsEnvironment
|
|
45
|
+
from codespeak_shared.progress.rich_progress_output import RichBasedCliProgressRenderer
|
|
46
|
+
from codespeak_shared.protocol_version import CURRENT_VERSION as PROTOCOL_VERSION
|
|
47
|
+
from rich.console import Console
|
|
48
|
+
|
|
49
|
+
from console_client.auth.token_storage import load_user_token
|
|
50
|
+
from console_client.build_client_event_converter import (
|
|
51
|
+
convert_build_finished_proto_to_result,
|
|
52
|
+
convert_proto_to_build_insight_event,
|
|
53
|
+
)
|
|
54
|
+
from console_client.client_feature_flags import ConsoleClientFeatureFlags
|
|
55
|
+
from console_client.os_environment_servicer import OsEnvironmentServicer
|
|
56
|
+
from console_client.sequence_reorder_buffer import SequenceReorderBuffer
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_build_insight_event_summary(event: BuildInsightEvent) -> str:
|
|
60
|
+
if isinstance(event, ProgressItemCreateEvent):
|
|
61
|
+
return f"create '{event.title}' [{event.status.value}]"
|
|
62
|
+
elif isinstance(event, ProgressItemUpdateEvent):
|
|
63
|
+
status_text = f" ({event.status_text})" if event.status_text else ""
|
|
64
|
+
return f"update [{event.status.value}]{status_text}"
|
|
65
|
+
elif isinstance(event, ToolCallEvent):
|
|
66
|
+
return f"tool '{event.title}'"
|
|
67
|
+
elif isinstance(event, TextOutputEvent):
|
|
68
|
+
text_preview = event.text[:50] + "..." if len(event.text) > 50 else event.text
|
|
69
|
+
return f"text: {text_preview}"
|
|
70
|
+
elif isinstance(event, UserVisibleModelOutputEvent):
|
|
71
|
+
text_preview = event.text[:50] + "..." if len(event.text) > 50 else event.text
|
|
72
|
+
return f"output: {text_preview}"
|
|
73
|
+
elif isinstance(event, SpanOpenEvent):
|
|
74
|
+
return f"span_open '{event.title}'"
|
|
75
|
+
elif isinstance(event, SpanCloseEvent):
|
|
76
|
+
return f"span_close '{event.span_id[:8]}'"
|
|
77
|
+
elif isinstance(event, TestsRunEvent):
|
|
78
|
+
return "tests_run"
|
|
79
|
+
else:
|
|
80
|
+
return type(event).__name__
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _handle_os_env_request(
|
|
84
|
+
request: OperationRequest,
|
|
85
|
+
os_env: OsEnvironment,
|
|
86
|
+
logger: logging.Logger,
|
|
87
|
+
console: Console,
|
|
88
|
+
) -> OperationResponse:
|
|
89
|
+
"""Handle an OsEnvironment operation request from the server."""
|
|
90
|
+
try:
|
|
91
|
+
# Use OsEnvironmentServicer to handle the request
|
|
92
|
+
servicer = OsEnvironmentServicer(os_env, console)
|
|
93
|
+
|
|
94
|
+
# Handle the request synchronously
|
|
95
|
+
# The servicer's StreamOperations method processes requests one at a time
|
|
96
|
+
def request_iter() -> Iterator[OperationRequest]:
|
|
97
|
+
yield request
|
|
98
|
+
|
|
99
|
+
# Process the request and get the response
|
|
100
|
+
response_iterator = servicer.StreamOperations(request_iter(), None)
|
|
101
|
+
response: OperationResponse = next(response_iterator)
|
|
102
|
+
return response
|
|
103
|
+
except Exception as e: # noqa: BLE001 (os env handler catch-all block)
|
|
104
|
+
# Return a response with client_error set - server will check this field first
|
|
105
|
+
logger.exception(f"Error handling OsEnv request: {e}")
|
|
106
|
+
error_info = os_environment_pb2.ErrorInfo(
|
|
107
|
+
message=f"Client internal error: {type(e).__name__}: {e}",
|
|
108
|
+
type=os_environment_pb2.ErrorType.ERROR_TYPE_UNSPECIFIED,
|
|
109
|
+
)
|
|
110
|
+
return OperationResponse(request_id=request.request_id, client_internal_error=error_info)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_build_client(
|
|
114
|
+
args: Namespace,
|
|
115
|
+
local_project: FileBasedCodeSpeakProject,
|
|
116
|
+
client_feature_flags: ConsoleClientFeatureFlags,
|
|
117
|
+
console: Console,
|
|
118
|
+
env: dict[str, str],
|
|
119
|
+
) -> BuildResult:
|
|
120
|
+
server_host = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_BUILD_SERVER_HOST)
|
|
121
|
+
server_port = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_BUILD_SERVER_PORT)
|
|
122
|
+
use_secure = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_BUILD_SERVER_SECURE)
|
|
123
|
+
|
|
124
|
+
server_address = f"{server_host}:{server_port}"
|
|
125
|
+
console.print(f"Connecting to {server_address}...", highlight=False)
|
|
126
|
+
|
|
127
|
+
# Load authentication token (or send empty token if feature flag is disabled)
|
|
128
|
+
if client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_SEND_USER_TOKEN):
|
|
129
|
+
user_token = load_user_token() or ""
|
|
130
|
+
else:
|
|
131
|
+
user_token = ""
|
|
132
|
+
|
|
133
|
+
print_grpc_events = client_feature_flags.get_flag_value(ConsoleClientFeatureFlags.CONSOLE_CLIENT_PRINT_GRPC_EVENTS)
|
|
134
|
+
|
|
135
|
+
def print_grpc_event(message: str) -> None:
|
|
136
|
+
console.print(f"[Client] {message}", style="dim")
|
|
137
|
+
|
|
138
|
+
follower_build_insight_storage = BuildInsightStorage(mode=BuildInsightStorageMode.FOLLOWER)
|
|
139
|
+
cli_renderer = RichBasedCliProgressRenderer(
|
|
140
|
+
console, follower_build_insight_storage, interactive=(not args.no_interactive)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Create reorder buffer to handle out-of-order events
|
|
144
|
+
def publish_to_storage(event: BuildInsightEvent) -> None:
|
|
145
|
+
# Events come from the server with sequence numbers already assigned
|
|
146
|
+
follower_build_insight_storage.report_event(event)
|
|
147
|
+
|
|
148
|
+
build_insight_events_buffer = SequenceReorderBuffer[BuildInsightEvent](publish_to_storage)
|
|
149
|
+
|
|
150
|
+
# Connect to server using secure or insecure channel
|
|
151
|
+
if use_secure:
|
|
152
|
+
ssl_credentials = grpc.ssl_channel_credentials()
|
|
153
|
+
channel = grpc.secure_channel(server_address, ssl_credentials)
|
|
154
|
+
else:
|
|
155
|
+
channel = grpc.insecure_channel(server_address)
|
|
156
|
+
|
|
157
|
+
stub = cast(Any, BuildServiceStub(channel))
|
|
158
|
+
|
|
159
|
+
# Create request queue for bidirectional streaming
|
|
160
|
+
# Use None as sentinel to signal end of requests
|
|
161
|
+
request_queue: queue.Queue[ClientMessage | None] = queue.Queue()
|
|
162
|
+
|
|
163
|
+
# Create request iterator
|
|
164
|
+
def request_iterator() -> Iterator[ClientMessage]:
|
|
165
|
+
while True:
|
|
166
|
+
message = request_queue.get()
|
|
167
|
+
if message is None:
|
|
168
|
+
# Sentinel value - stop sending requests
|
|
169
|
+
break
|
|
170
|
+
yield message
|
|
171
|
+
|
|
172
|
+
# Start bidirectional stream
|
|
173
|
+
response_stream = stub.Build(request_iterator())
|
|
174
|
+
|
|
175
|
+
# Serialize args to JSON (convert Namespace to dict)
|
|
176
|
+
args_dict = vars(args)
|
|
177
|
+
args_json = json.dumps(args_dict)
|
|
178
|
+
|
|
179
|
+
# Send StartBuild message with settings, args, feature flags, and user token
|
|
180
|
+
# Convert feature flags to map<string, string> (protobuf requirement)
|
|
181
|
+
feature_flags_str = {k: str(v) for k, v in client_feature_flags.to_dict().items()}
|
|
182
|
+
start_request = ClientMessage(
|
|
183
|
+
start_build=StartBuildRequest(
|
|
184
|
+
project_root=str(local_project.project_root.get_underlying_path().resolve()),
|
|
185
|
+
settings_json=local_project.settings_json_str(),
|
|
186
|
+
args_json=args_json,
|
|
187
|
+
client_feature_flags=feature_flags_str,
|
|
188
|
+
env_vars=filter_env_vars(env),
|
|
189
|
+
user_token=user_token,
|
|
190
|
+
client_protocol_version=PROTOCOL_VERSION,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
if print_grpc_events:
|
|
194
|
+
print_grpc_event("Sending: start_build")
|
|
195
|
+
|
|
196
|
+
request_queue.put(start_request)
|
|
197
|
+
|
|
198
|
+
result: BuildResult | None = None
|
|
199
|
+
stop_event = threading.Event()
|
|
200
|
+
cancellation_requested = threading.Event()
|
|
201
|
+
|
|
202
|
+
logger = logging.getLogger(__name__)
|
|
203
|
+
|
|
204
|
+
# Process server messages in a separate thread to handle os_env_request
|
|
205
|
+
def process_server_messages() -> None:
|
|
206
|
+
nonlocal result
|
|
207
|
+
try:
|
|
208
|
+
for server_message in response_stream:
|
|
209
|
+
if stop_event.is_set():
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
server_message = cast(ServerMessage, server_message)
|
|
213
|
+
which = server_message.WhichOneof("message")
|
|
214
|
+
|
|
215
|
+
if print_grpc_events and which not in ("os_env_request", "build_insight_event"):
|
|
216
|
+
# Don't print generic "Received" for these since we print detailed "Handling" messages
|
|
217
|
+
print_grpc_event(f"Received: {which}")
|
|
218
|
+
|
|
219
|
+
if which == "build_started":
|
|
220
|
+
build_started = server_message.build_started
|
|
221
|
+
result_type = build_started.WhichOneof("result")
|
|
222
|
+
if result_type == "failure":
|
|
223
|
+
error = BuildStartFailedUserError(build_started.failure.message)
|
|
224
|
+
result = BuildResult.failed("build", error)
|
|
225
|
+
break
|
|
226
|
+
message = f"Remote build started (ID: {build_started.success.build_id})"
|
|
227
|
+
logger.info(message)
|
|
228
|
+
console.print(message, highlight=False)
|
|
229
|
+
|
|
230
|
+
elif which == "build_insight_event":
|
|
231
|
+
# Convert protobuf event back to Python and add to reorder buffer
|
|
232
|
+
# The buffer will handle out-of-order delivery and publish to storage in order
|
|
233
|
+
proto_event = server_message.build_insight_event
|
|
234
|
+
py_event = convert_proto_to_build_insight_event(proto_event)
|
|
235
|
+
if print_grpc_events:
|
|
236
|
+
summary = _get_build_insight_event_summary(py_event)
|
|
237
|
+
print_grpc_event(f"Handling build_insight_event: {summary}")
|
|
238
|
+
build_insight_events_buffer.add(py_event)
|
|
239
|
+
|
|
240
|
+
elif which == "os_env_request":
|
|
241
|
+
# Handle file operation request from server
|
|
242
|
+
os_request = cast(os_environment_pb2.OperationRequest, getattr(server_message, "os_env_request"))
|
|
243
|
+
if print_grpc_events:
|
|
244
|
+
request_description = OsEnvRequestLogging.format(os_request)
|
|
245
|
+
print_grpc_event(f"Handling OsEnv request: {request_description}")
|
|
246
|
+
os_response = _handle_os_env_request(os_request, local_project.os_env(), logger, console)
|
|
247
|
+
# Send response back to server (don't log responses)
|
|
248
|
+
client_msg = ClientMessage(os_env_response=os_response)
|
|
249
|
+
request_queue.put(client_msg)
|
|
250
|
+
|
|
251
|
+
elif which == "build_finished":
|
|
252
|
+
build_finished = server_message.build_finished
|
|
253
|
+
logger.info(f"Received build finished message: {build_finished}")
|
|
254
|
+
result = convert_build_finished_proto_to_result(build_finished)
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
except grpc.RpcError as e:
|
|
258
|
+
logger.exception(f"Grpc error: {e}")
|
|
259
|
+
|
|
260
|
+
if not stop_event.is_set():
|
|
261
|
+
# Check if this is a connection error
|
|
262
|
+
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
263
|
+
# Convert to user-friendly error
|
|
264
|
+
user_error = CodeSpeakServerUnavailableUserError(server_address)
|
|
265
|
+
result = BuildResult.failed("build", user_error)
|
|
266
|
+
else:
|
|
267
|
+
# Other gRPC errors
|
|
268
|
+
console.print(f"Error during build: {e}", style="bright_red")
|
|
269
|
+
result = BuildResult.failed("build", e)
|
|
270
|
+
except Exception as e: # noqa: BLE001
|
|
271
|
+
if not stop_event.is_set():
|
|
272
|
+
logger.exception(f"Error during build ({type(e).__name__}): {e}")
|
|
273
|
+
console.print(f"Error during build: {e}", style="bright_red")
|
|
274
|
+
result = BuildResult.failed("build", e)
|
|
275
|
+
|
|
276
|
+
# Start processing server messages in a thread
|
|
277
|
+
message_thread = threading.Thread(target=process_server_messages, daemon=False)
|
|
278
|
+
message_thread.start()
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
while True:
|
|
282
|
+
try:
|
|
283
|
+
# Wait for the thread to complete
|
|
284
|
+
message_thread.join(timeout=0.1)
|
|
285
|
+
if not message_thread.is_alive():
|
|
286
|
+
break
|
|
287
|
+
except KeyboardInterrupt:
|
|
288
|
+
# Do not interrupt client loop immediately.
|
|
289
|
+
# Instead, send build cancellation request to the server and so let it finish the loop
|
|
290
|
+
if not cancellation_requested.is_set():
|
|
291
|
+
cli_renderer.append_text("[yellow]Waiting for server to cancel build...[/yellow]")
|
|
292
|
+
cli_renderer.append_text("[yellow]Press Ctrl+C again to force exit[/yellow]")
|
|
293
|
+
cancellation_requested.set()
|
|
294
|
+
# Send CancelBuild request to server
|
|
295
|
+
cancel_request = ClientMessage(cancel_build=CancelBuildRequest())
|
|
296
|
+
if print_grpc_events:
|
|
297
|
+
print_grpc_event("Sending: cancel_build")
|
|
298
|
+
request_queue.put(cancel_request)
|
|
299
|
+
else:
|
|
300
|
+
# Second Ctrl+C - force exit without waiting for server
|
|
301
|
+
cli_renderer.append_text("[yellow]Force exiting...[/yellow]")
|
|
302
|
+
break
|
|
303
|
+
finally:
|
|
304
|
+
stop_event.set()
|
|
305
|
+
# Send sentinel to stop the request iterator cleanly
|
|
306
|
+
request_queue.put(None)
|
|
307
|
+
cli_renderer.finalize()
|
|
308
|
+
channel.close()
|
|
309
|
+
|
|
310
|
+
if result is None:
|
|
311
|
+
if cancellation_requested.is_set():
|
|
312
|
+
result = BuildResult.failed("build", KeyboardInterrupt())
|
|
313
|
+
else:
|
|
314
|
+
result = BuildResult.failed(
|
|
315
|
+
"build", CodespeakInternalError("Error: connection with server closed before obtaining build result")
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return result
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Converter from protobuf BuildInsightEvent messages to Python objects."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from api_stubs import build_server_pb2
|
|
6
|
+
from api_stubs.build_server_pb2 import BuildFinished, BuildResultType
|
|
7
|
+
from codespeak_shared import BuildResult
|
|
8
|
+
from codespeak_shared.build_insight.events import (
|
|
9
|
+
BuildInsightEvent,
|
|
10
|
+
ProgressItemCreateEvent,
|
|
11
|
+
ProgressItemStatus,
|
|
12
|
+
ProgressItemUpdateEvent,
|
|
13
|
+
SpanCloseEvent,
|
|
14
|
+
SpanOpenEvent,
|
|
15
|
+
StopInteractiveProgressEvent,
|
|
16
|
+
TestsRunEvent,
|
|
17
|
+
TextOutputEvent,
|
|
18
|
+
ToolCallEvent,
|
|
19
|
+
UserVisibleModelOutputEvent,
|
|
20
|
+
)
|
|
21
|
+
from codespeak_shared.exceptions import CodespeakInternalError, CodespeakUserError
|
|
22
|
+
from codespeak_shared.test_runner_types import TestResults
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _convert_progress_status_from_proto(status: int) -> ProgressItemStatus:
|
|
26
|
+
"""Convert protobuf enum to Python ProgressItemStatus."""
|
|
27
|
+
mapping: dict[int, ProgressItemStatus] = {
|
|
28
|
+
int(build_server_pb2.PROGRESS_ITEM_STATUS_PENDING): ProgressItemStatus.PENDING,
|
|
29
|
+
int(build_server_pb2.PROGRESS_ITEM_STATUS_IN_PROGRESS): ProgressItemStatus.IN_PROGRESS,
|
|
30
|
+
int(build_server_pb2.PROGRESS_ITEM_STATUS_SKIPPED): ProgressItemStatus.SKIPPED,
|
|
31
|
+
int(build_server_pb2.PROGRESS_ITEM_STATUS_DONE): ProgressItemStatus.DONE,
|
|
32
|
+
int(build_server_pb2.PROGRESS_ITEM_STATUS_FAILED): ProgressItemStatus.FAILED,
|
|
33
|
+
}
|
|
34
|
+
result = mapping.get(status)
|
|
35
|
+
return result if result is not None else ProgressItemStatus.PENDING
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def convert_proto_to_build_insight_event(proto_event: Any) -> BuildInsightEvent:
|
|
39
|
+
"""Convert a protobuf BuildInsightEvent to a Python BuildInsightEvent."""
|
|
40
|
+
which = proto_event.WhichOneof("event")
|
|
41
|
+
|
|
42
|
+
sequence_number = proto_event.sequence_number
|
|
43
|
+
|
|
44
|
+
if which == "progress_item_create":
|
|
45
|
+
create = proto_event.progress_item_create
|
|
46
|
+
return ProgressItemCreateEvent(
|
|
47
|
+
timestamp=proto_event.timestamp,
|
|
48
|
+
sequence_number=sequence_number,
|
|
49
|
+
progress_item_id=create.progress_item_id,
|
|
50
|
+
status=_convert_progress_status_from_proto(create.status),
|
|
51
|
+
title=create.title,
|
|
52
|
+
description=create.description if create.description else None,
|
|
53
|
+
parent_item_id=create.parent_item_id if create.parent_item_id else None,
|
|
54
|
+
status_text=create.status_text if create.status_text else None,
|
|
55
|
+
)
|
|
56
|
+
elif which == "progress_item_update":
|
|
57
|
+
update = proto_event.progress_item_update
|
|
58
|
+
return ProgressItemUpdateEvent(
|
|
59
|
+
timestamp=proto_event.timestamp,
|
|
60
|
+
sequence_number=sequence_number,
|
|
61
|
+
progress_item_id=update.progress_item_id,
|
|
62
|
+
status=_convert_progress_status_from_proto(update.status),
|
|
63
|
+
status_text=update.status_text if update.status_text else None,
|
|
64
|
+
)
|
|
65
|
+
elif which == "text_output":
|
|
66
|
+
text = proto_event.text_output
|
|
67
|
+
return TextOutputEvent(
|
|
68
|
+
timestamp=proto_event.timestamp,
|
|
69
|
+
sequence_number=sequence_number,
|
|
70
|
+
text=text.text,
|
|
71
|
+
parent_progress_item_id=text.parent_progress_item_id if text.parent_progress_item_id else None,
|
|
72
|
+
)
|
|
73
|
+
elif which == "tool_call":
|
|
74
|
+
tool = proto_event.tool_call
|
|
75
|
+
return ToolCallEvent(
|
|
76
|
+
timestamp=proto_event.timestamp,
|
|
77
|
+
sequence_number=sequence_number,
|
|
78
|
+
title=tool.title,
|
|
79
|
+
details=tool.details if tool.details else None,
|
|
80
|
+
parent_progress_item_id=tool.parent_progress_item_id if tool.parent_progress_item_id else None,
|
|
81
|
+
)
|
|
82
|
+
elif which == "user_visible_model_output":
|
|
83
|
+
model_out = proto_event.user_visible_model_output
|
|
84
|
+
return UserVisibleModelOutputEvent(
|
|
85
|
+
timestamp=proto_event.timestamp,
|
|
86
|
+
sequence_number=sequence_number,
|
|
87
|
+
text=model_out.text,
|
|
88
|
+
parent_progress_item_id=model_out.parent_progress_item_id if model_out.parent_progress_item_id else None,
|
|
89
|
+
)
|
|
90
|
+
elif which == "span_open":
|
|
91
|
+
span = proto_event.span_open
|
|
92
|
+
return SpanOpenEvent(
|
|
93
|
+
timestamp=proto_event.timestamp,
|
|
94
|
+
sequence_number=sequence_number,
|
|
95
|
+
span_id=span.span_id,
|
|
96
|
+
title=span.title,
|
|
97
|
+
description=span.description if span.description else None,
|
|
98
|
+
)
|
|
99
|
+
elif which == "span_close":
|
|
100
|
+
span = proto_event.span_close
|
|
101
|
+
return SpanCloseEvent(
|
|
102
|
+
timestamp=proto_event.timestamp,
|
|
103
|
+
sequence_number=sequence_number,
|
|
104
|
+
span_id=span.span_id,
|
|
105
|
+
)
|
|
106
|
+
elif which == "tests_run":
|
|
107
|
+
tests = proto_event.tests_run
|
|
108
|
+
test_results = None
|
|
109
|
+
if tests.test_results_json:
|
|
110
|
+
test_results = TestResults.model_validate_json(tests.test_results_json)
|
|
111
|
+
return TestsRunEvent(
|
|
112
|
+
timestamp=proto_event.timestamp,
|
|
113
|
+
sequence_number=sequence_number,
|
|
114
|
+
test_results=test_results,
|
|
115
|
+
)
|
|
116
|
+
elif which == "stop_interactive_progress":
|
|
117
|
+
return StopInteractiveProgressEvent(
|
|
118
|
+
timestamp=proto_event.timestamp,
|
|
119
|
+
sequence_number=sequence_number,
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(f"Unknown type {which} of event {proto_event}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def convert_build_finished_proto_to_result(build_finished: BuildFinished) -> BuildResult:
|
|
126
|
+
if build_finished.result_type == BuildResultType.BUILD_RESULT_SUCCEEDED:
|
|
127
|
+
result = BuildResult.succeeded(build_finished.command)
|
|
128
|
+
elif build_finished.result_type == BuildResultType.BUILD_RESULT_SKIPPED:
|
|
129
|
+
result = BuildResult(BuildResult.SKIPPED, build_finished.command)
|
|
130
|
+
elif build_finished.result_type == BuildResultType.BUILD_RESULT_CANCELLED:
|
|
131
|
+
result = BuildResult.failed(build_finished.command, KeyboardInterrupt())
|
|
132
|
+
elif build_finished.result_type == BuildResultType.BUILD_RESULT_FAILED:
|
|
133
|
+
if not build_finished.build_error:
|
|
134
|
+
raise ValueError(f"build_finished.build_error is missing in {build_finished}")
|
|
135
|
+
|
|
136
|
+
build_error = build_finished.build_error
|
|
137
|
+
|
|
138
|
+
if build_error.HasField("user_error"):
|
|
139
|
+
exception = CodespeakUserError(user_visible_message=build_error.user_error.user_visible_message)
|
|
140
|
+
elif build_error.HasField("internal_error"):
|
|
141
|
+
exception = CodespeakInternalError(internal_message=build_error.internal_error.internal_message)
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError(f"BuildError has neither user_error nor internal_error: {build_error}")
|
|
144
|
+
|
|
145
|
+
result = BuildResult.failed(build_finished.command, exception)
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(f"Unexpected build result type: {build_finished.result_type}")
|
|
148
|
+
|
|
149
|
+
# Set build statistics
|
|
150
|
+
if build_finished.build_stats:
|
|
151
|
+
stats = build_finished.build_stats
|
|
152
|
+
result.build_stats.cache_enabled = stats.cache_enabled
|
|
153
|
+
result.build_stats.cache_hits = stats.cache_hits
|
|
154
|
+
result.build_stats.cache_misses = stats.cache_misses
|
|
155
|
+
|
|
156
|
+
return result
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Console client feature flags."""
|
|
2
|
+
|
|
3
|
+
from codespeak_shared.shared_feature_flags import FeatureFlag, FeatureFlagsMeta, SharedFeatureFlags
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConsoleClientFeatureFlags(SharedFeatureFlags, metaclass=FeatureFlagsMeta):
|
|
7
|
+
CONSOLE_CLIENT_PRINT_GRPC_EVENTS = FeatureFlag(default_value=False)
|
|
8
|
+
|
|
9
|
+
CONSOLE_CLIENT_SEND_USER_TOKEN = FeatureFlag(default_value=True)
|
|
10
|
+
|
|
11
|
+
CONSOLE_CLIENT_BUILD_SERVER_HOST = FeatureFlag(default_value="build.codespeak.dev")
|
|
12
|
+
|
|
13
|
+
CONSOLE_CLIENT_BUILD_SERVER_PORT = FeatureFlag(default_value=50053)
|
|
14
|
+
|
|
15
|
+
CONSOLE_CLIENT_BUILD_SERVER_SECURE = FeatureFlag(default_value=True)
|
|
16
|
+
|
|
17
|
+
CONSOLE_CLIENT_AUTH0_DOMAIN = FeatureFlag[str](default_value="codespeak.us.auth0.com")
|
|
18
|
+
|
|
19
|
+
CONSOLE_CLIENT_AUTH0_CLIENT_ID = FeatureFlag[str](default_value="zVHCedn5lS2hl6hiVtQuTMm9Hg2ZHZk9")
|
|
20
|
+
|
|
21
|
+
CONSOLE_CLIENT_AUTH0_SCOPES = FeatureFlag[str](default_value="openid profile email")
|
|
22
|
+
|
|
23
|
+
CONSOLE_CLIENT_AUTH0_CALLBACK_TIMEOUT = FeatureFlag[int](default_value=300)
|