levain-runtime 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/.gitignore +7 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/PKG-INFO +1 -1
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/chisel.py +10 -1
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/cli.py +78 -1
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/platform_api.py +29 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/pyproject.toml +1 -1
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/LICENSE +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/README.md +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/__init__.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/__main__.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/config.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/heartbeat.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/process.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/protocol.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/sandbox/node_runner.py.vendored +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/sandbox/run_logging.py.vendored +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/server.py +0 -0
- {levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/sessions.py +0 -0
|
@@ -18,6 +18,10 @@ build/
|
|
|
18
18
|
!**/platform_agents/build/
|
|
19
19
|
!**/src/build/
|
|
20
20
|
|
|
21
|
+
# Maven build output
|
|
22
|
+
services/flink/target/
|
|
23
|
+
services/flink/dependency-reduced-pom.xml
|
|
24
|
+
|
|
21
25
|
# Binaries
|
|
22
26
|
bin/
|
|
23
27
|
|
|
@@ -136,3 +140,6 @@ assets/video/*/renders/
|
|
|
136
140
|
# www/public/videos/, the composition that made them does not.
|
|
137
141
|
assets/video/updates-*/
|
|
138
142
|
assets/video/*/snapshots/
|
|
143
|
+
|
|
144
|
+
# Maven build output (Flink job); the image build compiles from source.
|
|
145
|
+
services/flink/target/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: levain-runtime
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Self-hosted runtime for Levain agents: run an agent's nodes on your own machine or container while the platform drives the recipe
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -90,6 +90,15 @@ def server_url(gateway_url: str) -> str:
|
|
|
90
90
|
return gateway_url
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def gateway(creds: Credentials) -> str:
|
|
94
|
+
"""The gateway to dial: the platform's, unless this host overrides it.
|
|
95
|
+
|
|
96
|
+
A runner on the platform cluster's own node dials the gateway Service
|
|
97
|
+
directly instead of looping out through the public edge and back.
|
|
98
|
+
"""
|
|
99
|
+
return os.environ.get("LEVAIN_RUNTIME_GATEWAY_URL") or creds.gateway_url
|
|
100
|
+
|
|
101
|
+
|
|
93
102
|
def client_argv(binary: Path, creds: Credentials) -> list[str]:
|
|
94
103
|
remotes = [f"R:{creds.reverse_port}:127.0.0.1:{RUNNER_HTTP_PORT}"]
|
|
95
104
|
remotes += [f"{f.local_port}:{f.remote}" for f in creds.forwards]
|
|
@@ -102,7 +111,7 @@ def client_argv(binary: Path, creds: Credentials) -> list[str]:
|
|
|
102
111
|
# default lets the reconnect wait grow to five minutes.
|
|
103
112
|
"--max-retry-interval",
|
|
104
113
|
"30s",
|
|
105
|
-
server_url(creds
|
|
114
|
+
server_url(gateway(creds)),
|
|
106
115
|
*remotes,
|
|
107
116
|
]
|
|
108
117
|
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import shlex
|
|
10
10
|
import subprocess
|
|
11
11
|
import sys
|
|
12
|
+
import time
|
|
12
13
|
from importlib import resources
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Annotated
|
|
@@ -283,6 +284,82 @@ def register(
|
|
|
283
284
|
print("next: levain-runtime up")
|
|
284
285
|
|
|
285
286
|
|
|
287
|
+
def _detach_and_send(
|
|
288
|
+
root: Path,
|
|
289
|
+
creds: Credentials,
|
|
290
|
+
event_type: str,
|
|
291
|
+
payload: object,
|
|
292
|
+
agent: str | None,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Deliver an event from a detached child; the caller returns now.
|
|
295
|
+
|
|
296
|
+
A hook must never wait on the network, so the parent forks and
|
|
297
|
+
exits while the child drops the inherited stdio (a caller reading
|
|
298
|
+
our pipes would otherwise block on the child) and posts the event.
|
|
299
|
+
Failures land in ``emit.log`` under the runtime home.
|
|
300
|
+
"""
|
|
301
|
+
if os.fork() > 0:
|
|
302
|
+
return
|
|
303
|
+
os.setsid()
|
|
304
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
305
|
+
for fd in (0, 1, 2):
|
|
306
|
+
os.dup2(devnull, fd)
|
|
307
|
+
try:
|
|
308
|
+
platform_api.post_event(creds, event_type, payload, agent)
|
|
309
|
+
except Exception as exc: # nothing above us to handle it
|
|
310
|
+
with (root / "emit.log").open("a") as fh:
|
|
311
|
+
fh.write(
|
|
312
|
+
f"{time.strftime('%Y-%m-%dT%H:%M:%S%z')} {event_type}: {exc}\n"
|
|
313
|
+
)
|
|
314
|
+
finally:
|
|
315
|
+
os._exit(0)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@app.command()
|
|
319
|
+
def emit(
|
|
320
|
+
event_type: Annotated[
|
|
321
|
+
str,
|
|
322
|
+
typer.Argument(help="Event name, e.g. runtime.claude_code.stop"),
|
|
323
|
+
],
|
|
324
|
+
agent: Annotated[
|
|
325
|
+
str | None,
|
|
326
|
+
typer.Option(help="Only dispatch this agent (slug)"),
|
|
327
|
+
] = None,
|
|
328
|
+
payload: Annotated[
|
|
329
|
+
str | None,
|
|
330
|
+
typer.Option(help="JSON payload, or - to read it from stdin"),
|
|
331
|
+
] = None,
|
|
332
|
+
wait: Annotated[
|
|
333
|
+
bool,
|
|
334
|
+
typer.Option(
|
|
335
|
+
"--wait", help="Send in the foreground and print the result"
|
|
336
|
+
),
|
|
337
|
+
] = False,
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Raise an event for agents subscribed to it on this runtime.
|
|
340
|
+
|
|
341
|
+
Reads the payload, then returns immediately while a detached child
|
|
342
|
+
delivers it — safe to call from hooks. Use --wait to send in the
|
|
343
|
+
foreground and see the platform's response.
|
|
344
|
+
"""
|
|
345
|
+
config = _load_config()
|
|
346
|
+
creds = config.credentials
|
|
347
|
+
if creds is None:
|
|
348
|
+
raise typer.BadParameter(
|
|
349
|
+
"no credentials; run `levain-runtime connect`"
|
|
350
|
+
)
|
|
351
|
+
raw = sys.stdin.read() if payload == "-" else payload
|
|
352
|
+
try:
|
|
353
|
+
parsed = json.loads(raw) if raw and raw.strip() else None
|
|
354
|
+
except json.JSONDecodeError as exc:
|
|
355
|
+
raise typer.BadParameter(f"payload is not valid JSON: {exc}") from None
|
|
356
|
+
if wait or not hasattr(os, "fork"):
|
|
357
|
+
result = platform_api.post_event(creds, event_type, parsed, agent)
|
|
358
|
+
print(json.dumps(result))
|
|
359
|
+
return
|
|
360
|
+
_detach_and_send(config.root, creds, event_type, parsed, agent)
|
|
361
|
+
|
|
362
|
+
|
|
286
363
|
@app.command()
|
|
287
364
|
def up() -> None:
|
|
288
365
|
"""Start the node server, the tunnel and the heartbeat."""
|
|
@@ -305,7 +382,7 @@ def status() -> None:
|
|
|
305
382
|
)
|
|
306
383
|
if config.credentials:
|
|
307
384
|
print(f"runtime: {config.credentials.runtime_id}")
|
|
308
|
-
print(f"gateway: {config.credentials
|
|
385
|
+
print(f"gateway: {chisel.gateway(config.credentials)}")
|
|
309
386
|
else:
|
|
310
387
|
print("runtime: not connected")
|
|
311
388
|
if config.mode == "direct":
|
|
@@ -50,6 +50,35 @@ def refresh_connection(creds: Credentials) -> bool:
|
|
|
50
50
|
return True
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def post_event(
|
|
54
|
+
creds: Credentials,
|
|
55
|
+
event_type: str,
|
|
56
|
+
payload: Any,
|
|
57
|
+
agent: str | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""``POST /api/v1/runtimes/{id}/events``; returns the response body.
|
|
60
|
+
|
|
61
|
+
Raises ``httpx.HTTPStatusError`` with the API's message on refusal.
|
|
62
|
+
"""
|
|
63
|
+
body: dict[str, Any] = {"event_type": event_type, "payload": payload}
|
|
64
|
+
if agent is not None:
|
|
65
|
+
body["agent"] = agent
|
|
66
|
+
with _client() as client:
|
|
67
|
+
resp = client.post(
|
|
68
|
+
f"{creds.api_url}/api/v1/runtimes/{creds.runtime_id}/events",
|
|
69
|
+
json=body,
|
|
70
|
+
headers={"Authorization": f"Bearer {creds.secret}"},
|
|
71
|
+
)
|
|
72
|
+
if resp.status_code >= 400:
|
|
73
|
+
raise httpx.HTTPStatusError(
|
|
74
|
+
_error_message(resp), request=resp.request, response=resp
|
|
75
|
+
)
|
|
76
|
+
data = resp.json()
|
|
77
|
+
if not isinstance(data, dict):
|
|
78
|
+
raise ValueError("unexpected response from the platform")
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
|
|
53
82
|
def register_runtime(
|
|
54
83
|
api_url: str, api_key: str, body: dict[str, Any]
|
|
55
84
|
) -> dict[str, Any]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "levain-runtime"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
description = "Self-hosted runtime for Levain agents: run an agent's nodes on your own machine or container while the platform drives the recipe"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/sandbox/node_runner.py.vendored
RENAMED
|
File without changes
|
{levain_runtime-0.1.2 → levain_runtime-0.1.4}/levain_runtime/sandbox/run_logging.py.vendored
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|