pycarta-service-agent 0.2.0.dev1__py3-none-win_amd64.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.
- pycarta_service_agent/__init__.py +42 -0
- pycarta_service_agent/_bin/carta-service-agent.exe +0 -0
- pycarta_service_agent/_locate.py +81 -0
- pycarta_service_agent/command.py +459 -0
- pycarta_service_agent/config.py +151 -0
- pycarta_service_agent/errors.py +36 -0
- pycarta_service_agent/ipc/__init__.py +51 -0
- pycarta_service_agent/ipc/_client.py +392 -0
- pycarta_service_agent/ipc/_exceptions.py +96 -0
- pycarta_service_agent/ipc/_protocol.py +118 -0
- pycarta_service_agent/ipc/_supervisor.py +404 -0
- pycarta_service_agent/messages.py +87 -0
- pycarta_service_agent/py.typed +0 -0
- pycarta_service_agent-0.2.0.dev1.dist-info/METADATA +75 -0
- pycarta_service_agent-0.2.0.dev1.dist-info/RECORD +18 -0
- pycarta_service_agent-0.2.0.dev1.dist-info/WHEEL +5 -0
- pycarta_service_agent-0.2.0.dev1.dist-info/licenses/LICENSE.txt +21 -0
- pycarta_service_agent-0.2.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The carta-service-agent binary, plus the Python contract for talking to it.
|
|
2
|
+
|
|
3
|
+
This package delivers the platform-specific carta-service-agent executable and
|
|
4
|
+
everything describing the seam between it and the Python that drives it. Those
|
|
5
|
+
facts change when the Go code changes, so they ship in the same artifact as the
|
|
6
|
+
binary and cannot drift out of step with it.
|
|
7
|
+
|
|
8
|
+
Not here: the seam between Carta and service authors. The register decorator,
|
|
9
|
+
execution-event access, request routing, and the loop that drives user handlers
|
|
10
|
+
all belong to pycarta.services. That is what a service author imports.
|
|
11
|
+
|
|
12
|
+
find_binary is the only name exported at the top level. Everything else is in a
|
|
13
|
+
submodule, imported directly so using one does not drag in the others:
|
|
14
|
+
|
|
15
|
+
=============== =============================================================
|
|
16
|
+
Module Holds
|
|
17
|
+
=============== =============================================================
|
|
18
|
+
``command`` argv builders, environment variable names, exit codes
|
|
19
|
+
``config`` the carta.service.json shape
|
|
20
|
+
``errors`` the exception hierarchy
|
|
21
|
+
``ipc`` stdio JSON framing and the supervised subprocess
|
|
22
|
+
``messages`` IPC message type names and PROTOCOL_VERSION
|
|
23
|
+
=============== =============================================================
|
|
24
|
+
|
|
25
|
+
pycarta is the only intended consumer, but it is a real one. Treat a change to
|
|
26
|
+
any name these modules export as a breaking change.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
Running a fully enrolled service, one execution at a time::
|
|
30
|
+
|
|
31
|
+
from pycarta_service_agent import command, config, ipc
|
|
32
|
+
|
|
33
|
+
cfg = config.ServiceConfig.load(project_dir)
|
|
34
|
+
client = ipc.connect(
|
|
35
|
+
command.run_argv(cfg, once=True),
|
|
36
|
+
classify_exit=command.classify_exit,
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from ._locate import find_binary
|
|
41
|
+
|
|
42
|
+
__all__ = ["find_binary"]
|
|
Binary file
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Locating the carta-service-agent binary bundled in this wheel.
|
|
2
|
+
|
|
3
|
+
Also holds the CARTA_SERVICE_AGENT_BIN escape hatch, which points the package at
|
|
4
|
+
a binary elsewhere on disk. That is for developing this package: run a locally
|
|
5
|
+
built Go binary against the Python driving it without rebuilding a wheel first.
|
|
6
|
+
Not part of the contract with consumers, who should always get the binary their
|
|
7
|
+
wheel shipped.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import stat
|
|
15
|
+
from importlib import resources
|
|
16
|
+
|
|
17
|
+
_BIN_DIR = "_bin"
|
|
18
|
+
_BIN_NAME = (
|
|
19
|
+
"carta-service-agent.exe"
|
|
20
|
+
if platform.system() == "Windows"
|
|
21
|
+
else "carta-service-agent"
|
|
22
|
+
)
|
|
23
|
+
_PKG_NAME = "pycarta-service-agent"
|
|
24
|
+
|
|
25
|
+
# Development escape hatch, see the module docstring. Private on purpose. For
|
|
26
|
+
# developers of this package, not the consumers it exports to.
|
|
27
|
+
_ENV_OVERRIDE = "CARTA_SERVICE_AGENT_BIN"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_binary() -> str:
|
|
31
|
+
"""Absolute path to the carta-service-agent binary bundled in this wheel.
|
|
32
|
+
|
|
33
|
+
Each wheel carries exactly one binary, built for that wheel's platform tag,
|
|
34
|
+
so pip installs the one matching the host. Marked executable on first call if
|
|
35
|
+
it is not already, since a wheel does not reliably preserve the mode bit.
|
|
36
|
+
|
|
37
|
+
CARTA_SERVICE_AGENT_BIN, if set to an absolute path, replaces the bundled
|
|
38
|
+
binary. Escape hatch for developing this package against a locally built
|
|
39
|
+
binary. Not something a consumer should rely on. Validated the same way the
|
|
40
|
+
bundled path is, so a stale or misspelled value fails here with a useful
|
|
41
|
+
message instead of an opaque spawn error later.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
str: Absolute path to the executable, safe to pass as argv[0].
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
FileNotFoundError: CARTA_SERVICE_AGENT_BIN is set but names nothing, or
|
|
48
|
+
the package is installed with its bundled binary missing (broken or
|
|
49
|
+
hand-assembled install).
|
|
50
|
+
"""
|
|
51
|
+
override = os.environ.get(_ENV_OVERRIDE, "").strip()
|
|
52
|
+
if override:
|
|
53
|
+
if not os.path.isfile(override):
|
|
54
|
+
raise FileNotFoundError(
|
|
55
|
+
f"{_ENV_OVERRIDE} is set to {override!r}, which is not a file."
|
|
56
|
+
)
|
|
57
|
+
_ensure_executable(override)
|
|
58
|
+
return override
|
|
59
|
+
|
|
60
|
+
ref = resources.files(__package__).joinpath(_BIN_DIR, _BIN_NAME)
|
|
61
|
+
with resources.as_file(ref) as p:
|
|
62
|
+
path = os.fspath(p)
|
|
63
|
+
|
|
64
|
+
if not os.path.isfile(path):
|
|
65
|
+
raise FileNotFoundError(
|
|
66
|
+
f"{_PKG_NAME} is installed but its bundled binary is missing at {path!r}."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
_ensure_executable(path)
|
|
70
|
+
return path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _ensure_executable(path: str) -> None:
|
|
74
|
+
if os.name != "nt":
|
|
75
|
+
mode = os.stat(path).st_mode
|
|
76
|
+
expected = mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
77
|
+
if mode != expected:
|
|
78
|
+
try:
|
|
79
|
+
os.chmod(path, expected)
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Invoking the carta-service-agent binary: flags, env vars, exit codes.
|
|
2
|
+
|
|
3
|
+
The binary takes all of its config from flags and never reads
|
|
4
|
+
carta.service.json at runtime, so an adapter has to translate Python arguments
|
|
5
|
+
into an argv. The environment supplies credentials only, never config. Those two
|
|
6
|
+
contracts plus the exit-code contract are facts about the binary, so they live
|
|
7
|
+
here next to it rather than in the adapter.
|
|
8
|
+
|
|
9
|
+
The flag builders are pure. They return argv fragments and touch nothing.
|
|
10
|
+
run_argv and run_subcommand also resolve the binary's path, and run_subcommand
|
|
11
|
+
spawns it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
|
|
19
|
+
from .config import ServiceConfig, SUPPORTED_INVOKER
|
|
20
|
+
from .errors import CommandError
|
|
21
|
+
from .ipc import ExitAction
|
|
22
|
+
from ._locate import find_binary
|
|
23
|
+
|
|
24
|
+
# -- environment -------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
ENV_SESSION_TOKEN: str = "CARTA_SESSION_TOKEN"
|
|
27
|
+
"""Cognito ID token. Lets enroll and init skip the interactive browser login.
|
|
28
|
+
|
|
29
|
+
Read by enroll and init only. Required when --no-browser is set, since the PKCE
|
|
30
|
+
loopback flow needs a browser and there is not one.
|
|
31
|
+
|
|
32
|
+
Env var rather than a flag because command lines are visible in ps.
|
|
33
|
+
|
|
34
|
+
Same token the browser login would have produced. An adapter that already
|
|
35
|
+
authenticated the user can inject it here instead of prompting a second time.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
ENV_API_TOKEN: str = "CARTA_SERVICE_API_TOKEN"
|
|
39
|
+
"""Per-service API token, for a runner that did not enroll itself.
|
|
40
|
+
|
|
41
|
+
Read by run and export. Takes precedence over both the OS keychain and the
|
|
42
|
+
credentials fallback file. Set this with ENV_RUNNER_ID and run needs no local
|
|
43
|
+
credential storage at all, which is what makes a container or serverless
|
|
44
|
+
deployment work: no keychain available, and nothing should be writing credential
|
|
45
|
+
files.
|
|
46
|
+
|
|
47
|
+
"carta-service-agent export" prints the value to seed this.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
ENV_RUNNER_ID: str = "CARTA_SERVICE_RUNNER_ID"
|
|
51
|
+
"""Runner identity, for a runner that did not enroll itself.
|
|
52
|
+
|
|
53
|
+
Read by run and export, same precedence over the keychain and fallback file as
|
|
54
|
+
ENV_API_TOKEN. Normally enroll creates the runner ID and caches it in the
|
|
55
|
+
keychain. This is how a pre-provisioned deployment gets told which runner it is
|
|
56
|
+
instead.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
ENV_SMCP_JWT: str = "CARTA_SERVICE_SMCP_JWT"
|
|
60
|
+
"""Already-exchanged SMCP JWT, for a runner whose deployment shares one.
|
|
61
|
+
|
|
62
|
+
Read by run. Seeds the agent's session cache so no token exchange with the
|
|
63
|
+
agent gateway is needed while the token remains valid. For short-lived runners
|
|
64
|
+
(a Lambda woken per signal) that would otherwise each perform their own
|
|
65
|
+
exchange, the deployment exchanges once, stashes the JWT, and injects it here.
|
|
66
|
+
|
|
67
|
+
Supply a token whose remaining validity covers the whole run. The agent
|
|
68
|
+
validates expiry itself and falls back to a normal exchange (via ENV_API_TOKEN)
|
|
69
|
+
when the token is expired, expiring, or malformed.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# -- exit codes --------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
EXIT_STOP: int = 0
|
|
76
|
+
"""run shut down cleanly. Stdin closed, or a signal arrived.
|
|
77
|
+
|
|
78
|
+
Also what run returns after finishing its work under --once or
|
|
79
|
+
--max-executions, and when --exit-when-idle finds an empty queue. Do not
|
|
80
|
+
restart.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
EXIT_RESTART: int = 1
|
|
84
|
+
"""run hit a transient failure. Restart with backoff.
|
|
85
|
+
|
|
86
|
+
Reserved. run swallows loop errors into its own backoff and emits only
|
|
87
|
+
EXIT_STOP or EXIT_FAIL.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
EXIT_RESTART_NOW: int = 2
|
|
91
|
+
"""run asked to be restarted immediately, no backoff delay.
|
|
92
|
+
|
|
93
|
+
Reserved for a future config-reload or file-watch mode. Not currently emitted.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
EXIT_FAIL: int = 3
|
|
97
|
+
"""run failed deterministically. Do not restart.
|
|
98
|
+
|
|
99
|
+
Every setup failure lands here: flag parsing, missing or invalid required flags,
|
|
100
|
+
config validation, logger setup, credential lookup, gateway client, invoker
|
|
101
|
+
startup (including a failed hello handshake or a version mismatch). A restart
|
|
102
|
+
fails the same way.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def classify_exit(returncode: int) -> ExitAction:
|
|
107
|
+
"""Map a run exit code onto the action a supervisor should take.
|
|
108
|
+
|
|
109
|
+
Pass this to ipc.connect as classify_exit so the supervisor honors the
|
|
110
|
+
binary's contract instead of the generic "zero stops, anything else
|
|
111
|
+
restarts" default.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
returncode: Exit code from "carta-service-agent run".
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
ExitAction: STOP for EXIT_STOP, RESTART_NOW for EXIT_RESTART_NOW, FAIL
|
|
118
|
+
for EXIT_FAIL, RESTART for anything else. Codes the binary does not
|
|
119
|
+
define and signal-derived codes fall in that last bucket and are treated
|
|
120
|
+
as transient.
|
|
121
|
+
"""
|
|
122
|
+
if returncode == EXIT_STOP:
|
|
123
|
+
return ExitAction.STOP
|
|
124
|
+
if returncode == EXIT_RESTART_NOW:
|
|
125
|
+
return ExitAction.RESTART_NOW
|
|
126
|
+
if returncode == EXIT_FAIL:
|
|
127
|
+
return ExitAction.FAIL
|
|
128
|
+
return ExitAction.RESTART
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# -- flags -------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def url_flags(
|
|
135
|
+
carta_url: str = "",
|
|
136
|
+
control_plane_url: str = "",
|
|
137
|
+
gateway_url: str = "",
|
|
138
|
+
platform_env: str = "",
|
|
139
|
+
) -> list[str]:
|
|
140
|
+
"""Build the endpoint flags every subcommand accepts.
|
|
141
|
+
|
|
142
|
+
Empty values are omitted rather than passed as empty strings, which lets the
|
|
143
|
+
binary apply its own defaults. A blank URL gets derived from the platform
|
|
144
|
+
environment, and a blank platform environment means production.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
carta_url: Override for the Carta API base URL.
|
|
148
|
+
control_plane_url: Override for the control plane base URL.
|
|
149
|
+
gateway_url: Override for the agent gateway base URL.
|
|
150
|
+
platform_env: Platform environment to derive unset URLs from, either
|
|
151
|
+
"development" or "production". Not validated here. The binary
|
|
152
|
+
rejects an unrecognized value.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
list[str]: Flag/value pairs for the non-empty arguments, stable order.
|
|
156
|
+
Empty list when every argument is empty.
|
|
157
|
+
"""
|
|
158
|
+
flags: list[str] = []
|
|
159
|
+
for flag, value in (
|
|
160
|
+
("--carta-url", carta_url),
|
|
161
|
+
("--control-plane-url", control_plane_url),
|
|
162
|
+
("--gateway-url", gateway_url),
|
|
163
|
+
("--platform-env", platform_env),
|
|
164
|
+
):
|
|
165
|
+
if value:
|
|
166
|
+
flags.extend([flag, value])
|
|
167
|
+
return flags
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def enroll_flags(
|
|
171
|
+
carta_url: str = "",
|
|
172
|
+
control_plane_url: str = "",
|
|
173
|
+
gateway_url: str = "",
|
|
174
|
+
platform_env: str = "",
|
|
175
|
+
namespace: str = "",
|
|
176
|
+
service_name: str = "",
|
|
177
|
+
no_auth: bool = False,
|
|
178
|
+
no_browser: bool = False,
|
|
179
|
+
no_keychain: bool = False,
|
|
180
|
+
) -> list[str]:
|
|
181
|
+
"""Build the flags for "carta-service-agent enroll".
|
|
182
|
+
|
|
183
|
+
Enroll registers the service and this device with the platform. It resolves
|
|
184
|
+
or creates the runner, creates the service, links them, mints the API token,
|
|
185
|
+
and writes the resolved service_id back into carta.service.json.
|
|
186
|
+
|
|
187
|
+
--invoker is always included, since this package is the Python adapter.
|
|
188
|
+
|
|
189
|
+
namespace and service_name are required by the binary but optional here, so
|
|
190
|
+
a caller can build a partial argv. The binary names the missing flag.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
carta_url: Override for the Carta API base URL.
|
|
194
|
+
control_plane_url: Override for the control plane base URL.
|
|
195
|
+
gateway_url: Override for the agent gateway base URL.
|
|
196
|
+
platform_env: "development" or "production".
|
|
197
|
+
namespace: Carta namespace to enroll the service into.
|
|
198
|
+
service_name: Name for the service within the namespace.
|
|
199
|
+
no_auth: Skip authentication. Only valid when the platform is also
|
|
200
|
+
running without auth. Enroll does not currently honor it.
|
|
201
|
+
no_browser: Authenticate from ENV_SESSION_TOKEN only, never open a
|
|
202
|
+
browser. Fails fast if that variable is unset, malformed, or expired.
|
|
203
|
+
no_keychain: Skip the OS keychain, use the credentials fallback file. For
|
|
204
|
+
a host with no keychain, where every keychain call fails and logs a
|
|
205
|
+
warning first.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
list[str]: The flags, without the binary path or the "enroll" subcommand
|
|
209
|
+
name. Pass to run_subcommand.
|
|
210
|
+
"""
|
|
211
|
+
flags = ["--invoker", SUPPORTED_INVOKER]
|
|
212
|
+
flags += url_flags(carta_url, control_plane_url, gateway_url, platform_env)
|
|
213
|
+
if namespace:
|
|
214
|
+
flags += ["--namespace", namespace]
|
|
215
|
+
if service_name:
|
|
216
|
+
flags += ["--name", service_name]
|
|
217
|
+
if no_auth:
|
|
218
|
+
flags.append("--no-auth")
|
|
219
|
+
if no_browser:
|
|
220
|
+
flags.append("--no-browser")
|
|
221
|
+
if no_keychain:
|
|
222
|
+
flags.append("--no-keychain")
|
|
223
|
+
return flags
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def init_flags(
|
|
227
|
+
carta_url: str = "",
|
|
228
|
+
control_plane_url: str = "",
|
|
229
|
+
gateway_url: str = "",
|
|
230
|
+
platform_env: str = "",
|
|
231
|
+
no_browser: bool = False,
|
|
232
|
+
no_keychain: bool = False,
|
|
233
|
+
) -> list[str]:
|
|
234
|
+
"""Build the flags for "carta-service-agent init".
|
|
235
|
+
|
|
236
|
+
Init is interactive one-time project setup. It authenticates the user, lets
|
|
237
|
+
them pick or create a namespace, and writes carta.service.json into the
|
|
238
|
+
current working directory. It does not capture a service name. That arrives
|
|
239
|
+
at enroll time.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
carta_url: Override for the Carta API base URL.
|
|
243
|
+
control_plane_url: Override for the control plane base URL.
|
|
244
|
+
gateway_url: Override for the agent gateway base URL.
|
|
245
|
+
platform_env: "development" or "production".
|
|
246
|
+
no_browser: Authenticate from ENV_SESSION_TOKEN only, never open a
|
|
247
|
+
browser. Init is interactive in other respects, so this suppresses
|
|
248
|
+
the login prompt only.
|
|
249
|
+
no_keychain: Skip the OS keychain, use the credentials fallback file.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
list[str]: The flags, without the binary path or the "init" subcommand
|
|
253
|
+
name. Pass to run_subcommand.
|
|
254
|
+
"""
|
|
255
|
+
flags = url_flags(carta_url, control_plane_url, gateway_url, platform_env)
|
|
256
|
+
if no_browser:
|
|
257
|
+
flags.append("--no-browser")
|
|
258
|
+
if no_keychain:
|
|
259
|
+
flags.append("--no-keychain")
|
|
260
|
+
return flags
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def export_flags(
|
|
264
|
+
service_id: str,
|
|
265
|
+
namespace: str,
|
|
266
|
+
service_name: str,
|
|
267
|
+
carta_url: str = "",
|
|
268
|
+
control_plane_url: str = "",
|
|
269
|
+
gateway_url: str = "",
|
|
270
|
+
platform_env: str = "",
|
|
271
|
+
no_keychain: bool = False,
|
|
272
|
+
) -> list[str]:
|
|
273
|
+
"""Build the flags for "carta-service-agent export".
|
|
274
|
+
|
|
275
|
+
Export is read-only and contacts no platform service. It prints this
|
|
276
|
+
machine's credentials for one service as JSON on stdout, so a
|
|
277
|
+
pre-provisioned deployment can be seeded with ENV_API_TOKEN and
|
|
278
|
+
ENV_RUNNER_ID. It exists because an API token value cannot be recovered from
|
|
279
|
+
the platform after creation. The machine that enrolled the service holds the
|
|
280
|
+
only copy.
|
|
281
|
+
|
|
282
|
+
The output contains a live credential. Capture stdout rather than letting it
|
|
283
|
+
reach a terminal or a log.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
service_id: Platform service ID to export credentials for. This is the
|
|
287
|
+
keychain account holding the API token, so it has to match the
|
|
288
|
+
service that was enrolled.
|
|
289
|
+
namespace: Carta namespace owning the service.
|
|
290
|
+
service_name: Service name within the namespace.
|
|
291
|
+
carta_url: Override for the Carta API base URL.
|
|
292
|
+
control_plane_url: Override for the control plane base URL.
|
|
293
|
+
gateway_url: Override for the agent gateway base URL.
|
|
294
|
+
platform_env: "development" or "production". Decides which URLs appear
|
|
295
|
+
in the exported JSON.
|
|
296
|
+
no_keychain: Skip the OS keychain, read the credentials fallback file.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
list[str]: The flags, without the binary path or the "export" subcommand
|
|
300
|
+
name.
|
|
301
|
+
"""
|
|
302
|
+
flags = [
|
|
303
|
+
"--id",
|
|
304
|
+
service_id,
|
|
305
|
+
"--namespace",
|
|
306
|
+
namespace,
|
|
307
|
+
"--name",
|
|
308
|
+
service_name,
|
|
309
|
+
"--invoker",
|
|
310
|
+
SUPPORTED_INVOKER,
|
|
311
|
+
]
|
|
312
|
+
flags += url_flags(carta_url, control_plane_url, gateway_url, platform_env)
|
|
313
|
+
if no_keychain:
|
|
314
|
+
flags.append("--no-keychain")
|
|
315
|
+
return flags
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def run_argv(
|
|
319
|
+
cfg: ServiceConfig,
|
|
320
|
+
*,
|
|
321
|
+
exit_when_idle: bool = False,
|
|
322
|
+
log_format: str = "",
|
|
323
|
+
log_level: str = "",
|
|
324
|
+
max_executions: int = 0,
|
|
325
|
+
no_debug_logf: bool = False,
|
|
326
|
+
no_keychain: bool = False,
|
|
327
|
+
once: bool = False,
|
|
328
|
+
quiet: bool = False,
|
|
329
|
+
verbose: bool = False,
|
|
330
|
+
) -> list[str]:
|
|
331
|
+
"""Build the full argv for "carta-service-agent run", binary path included.
|
|
332
|
+
|
|
333
|
+
Unlike the other builders here, the result is ready to hand to ipc.connect.
|
|
334
|
+
--log-stderr is always appended: the binary's stdout is the IPC channel, so
|
|
335
|
+
log output there corrupts the message framing.
|
|
336
|
+
|
|
337
|
+
The defaults describe a long-lived agent holding a long poll, waiting for
|
|
338
|
+
work. once and exit_when_idle describe the opposite, a runner started per
|
|
339
|
+
wake-up that skips the long poll, drains what is queued, and exits.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
cfg: Service configuration. service_id, namespace, and service_name have
|
|
343
|
+
to be non-blank. The URL fields and platform_env are forwarded when
|
|
344
|
+
set, omitted when empty.
|
|
345
|
+
exit_when_idle: Exit as soon as the queue is empty.
|
|
346
|
+
log_format: Console stream format, "text" or "json". Empty uses the
|
|
347
|
+
default of "text". The debug log file is always JSON.
|
|
348
|
+
log_level: Console stream level, one of "debug", "info", "warn", or
|
|
349
|
+
"error". Empty uses the default of "info". Does not affect the
|
|
350
|
+
debug log file, which is always at debug.
|
|
351
|
+
max_executions: Stop after claiming this many executions. 0 means no
|
|
352
|
+
limit. Counted at claim time, not on success, so a service that
|
|
353
|
+
fails every execution still terminates. Negative values are rejected
|
|
354
|
+
by the binary.
|
|
355
|
+
no_debug_logf: Do not write the debug log file.
|
|
356
|
+
no_keychain: Skip the OS keychain, read credentials from the fallback
|
|
357
|
+
file. Unnecessary when ENV_API_TOKEN and ENV_RUNNER_ID are set,
|
|
358
|
+
since those bypass both.
|
|
359
|
+
once: Handle at most one execution and exit. This is the shorthand for
|
|
360
|
+
"--exit-when-idle --max-executions 1". Overrides max_executions.
|
|
361
|
+
quiet: Log the console stream at error level. Overrides log_level.
|
|
362
|
+
Incompatible with verbose.
|
|
363
|
+
verbose: Log the console stream at debug level. Overrides log_level.
|
|
364
|
+
Incompatible with quiet.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
list[str]: Full argv, starting with the absolute path to the binary.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ValueError: cfg.service_id, cfg.namespace, or cfg.service_name is empty
|
|
371
|
+
or whitespace.
|
|
372
|
+
FileNotFoundError: The binary could not be located.
|
|
373
|
+
"""
|
|
374
|
+
required = {
|
|
375
|
+
"service_id": cfg.service_id,
|
|
376
|
+
"namespace": cfg.namespace,
|
|
377
|
+
"service_name": cfg.service_name,
|
|
378
|
+
}
|
|
379
|
+
for name, value in required.items():
|
|
380
|
+
if not value or not value.strip():
|
|
381
|
+
raise ValueError(f"{name} is required to launch the agent")
|
|
382
|
+
argv = [
|
|
383
|
+
find_binary(),
|
|
384
|
+
"run",
|
|
385
|
+
"--id",
|
|
386
|
+
cfg.service_id,
|
|
387
|
+
"--namespace",
|
|
388
|
+
cfg.namespace,
|
|
389
|
+
"--name",
|
|
390
|
+
cfg.service_name,
|
|
391
|
+
"--invoker",
|
|
392
|
+
SUPPORTED_INVOKER,
|
|
393
|
+
]
|
|
394
|
+
argv += url_flags(
|
|
395
|
+
cfg.carta_url,
|
|
396
|
+
cfg.control_plane_url,
|
|
397
|
+
cfg.gateway_url,
|
|
398
|
+
cfg.platform_env,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if once:
|
|
402
|
+
argv.append("--once")
|
|
403
|
+
if exit_when_idle:
|
|
404
|
+
argv.append("--exit-when-idle")
|
|
405
|
+
if max_executions:
|
|
406
|
+
argv += ["--max-executions", str(max_executions)]
|
|
407
|
+
if no_keychain:
|
|
408
|
+
argv.append("--no-keychain")
|
|
409
|
+
|
|
410
|
+
if log_level:
|
|
411
|
+
argv += ["--log-level", log_level]
|
|
412
|
+
if log_format:
|
|
413
|
+
argv += ["--log-format", log_format]
|
|
414
|
+
if verbose:
|
|
415
|
+
argv.append("--verbose")
|
|
416
|
+
if quiet:
|
|
417
|
+
argv.append("--quiet")
|
|
418
|
+
if no_debug_logf:
|
|
419
|
+
argv.append("--no-debug-logf")
|
|
420
|
+
|
|
421
|
+
# log stderr required, the agent's stdout is the IPC channel
|
|
422
|
+
argv.append("--log-stderr")
|
|
423
|
+
return argv
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# -- invocation --------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def run_subcommand(
|
|
430
|
+
subcommand: str,
|
|
431
|
+
flags: list[str],
|
|
432
|
+
*,
|
|
433
|
+
env: dict[str, str] | None = None,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Run a subcommand to completion with the terminal's stdio.
|
|
436
|
+
|
|
437
|
+
The child inherits this process's stdin, stdout, and stderr, so its prompts
|
|
438
|
+
and diagnostics reach the user directly. Right for init and enroll, which are
|
|
439
|
+
interactive. Wrong for export, whose stdout is JSON worth capturing.
|
|
440
|
+
|
|
441
|
+
Do not use this for run. That subcommand's stdio is the IPC channel. Use
|
|
442
|
+
run_argv with ipc.connect instead.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
subcommand: Subcommand name, e.g. "enroll" or "init".
|
|
446
|
+
flags: Flags for it, usually from enroll_flags or init_flags.
|
|
447
|
+
env: Variables to merge onto this process's environment for the child.
|
|
448
|
+
The parent environment is otherwise inherited unchanged, so this is
|
|
449
|
+
additive. Use it to inject ENV_SESSION_TOKEN.
|
|
450
|
+
|
|
451
|
+
Raises:
|
|
452
|
+
CommandError: The subcommand exited non-zero. The child has already
|
|
453
|
+
written its diagnostics to the inherited stderr.
|
|
454
|
+
FileNotFoundError: The binary could not be located.
|
|
455
|
+
"""
|
|
456
|
+
proc_env = {**os.environ, **env} if env else None
|
|
457
|
+
result = subprocess.run([find_binary(), subcommand, *flags], env=proc_env)
|
|
458
|
+
if result.returncode != 0:
|
|
459
|
+
raise CommandError(f"{subcommand} failed (see output above)")
|