nightshift-sdk 0.4.2__tar.gz → 0.4.3__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.
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/PKG-INFO +2 -2
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/README.md +1 -1
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/administration/api-keys.mdx +2 -2
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/administration/backups.mdx +1 -1
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/cost-management.mdx +1 -1
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/introduction.mdx +3 -3
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/sdk/quickstart.mdx +3 -3
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/basic_agent.py +1 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/deploy.sh +4 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/production.sh +4 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/setup.sh +4 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/pyproject.toml +2 -1
- nightshift_sdk-0.4.3/src/nightshift/cli/commands/agents.py +101 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/deploy.py +10 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/events.py +10 -3
- nightshift_sdk-0.4.3/src/nightshift/protocol/events.py +56 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/registry.py +26 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/sdk/config.py +1 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/server.py +164 -20
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/task.py +12 -2
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/manager.py +53 -4
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_events.py +49 -2
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_registry.py +47 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/uv.lock +2 -2
- nightshift_sdk-0.4.2/src/nightshift/cli/commands/agents.py +0 -56
- nightshift_sdk-0.4.2/src/nightshift/protocol/events.py +0 -36
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/.claude/skills/opentui +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/.github/workflows/release.yml +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/.gitignore +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/CLAUDE.md +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/LICENSE +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/contributing/deploying-changes.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/contributing/dev-setup.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/architecture.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/provision.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/server.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/verify.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/docs.json +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/logo/kokapo.gif +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/logo/kokapo.svg +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/logo/nightshift-text.png +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/sdk/agent-config.mdx +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/code_reviewer.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/edit_agent.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/hooks_agent.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/mcp_agent.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/multi_agent.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/net_test.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/oai_agent.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/pyproject.toml +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/smoke_test.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/stateful_agent.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/test-workspace/hello.txt +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/web_researcher.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/bake-rootfs.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/.gitignore +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/cron.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/deploy.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/lambda/start.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/lambda/stop.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/teardown.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/test.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/upgrade.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/rootfs/build-rootfs.sh +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/rootfs/init +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/__main__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/agent/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/agent/__main__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/agent/entry.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/auth.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/api_key.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/login.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/logs.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/run.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/config.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/main.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/config.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/protocol/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/protocol/packaging.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/sdk/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/sdk/app.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/network.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/pool.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/rootfs.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/__init__.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_config.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_deploy_api.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_deploy_cli.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_protocol.py +0 -0
- {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_sdk.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nightshift-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Nightshift — autonomous agent orchestrator with Firecracker VMs
|
|
5
5
|
Project-URL: Homepage, https://nightshift.sh
|
|
6
6
|
Project-URL: Documentation, https://docs.nightshift.sh
|
|
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
|
|
57
57
|
# Nightshift
|
|
58
58
|
|
|
59
|
-
Nightshift runs AI agents in isolated [Firecracker](https://firecracker-microvm.github.io/) microVMs on bare-metal infrastructure.
|
|
59
|
+
Nightshift runs AI agents in isolated [Firecracker](https://firecracker-microvm.github.io/) microVMs on bare-metal infrastructure.
|
|
60
60
|
Each agent gets its own microVM with a dedicated filesystem, network, and resource limits; so agents can execute code, edit files, and make network calls without affecting the host or each other.
|
|
61
61
|
|
|
62
62
|
## Installation
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
# Nightshift
|
|
26
26
|
|
|
27
|
-
Nightshift runs AI agents in isolated [Firecracker](https://firecracker-microvm.github.io/) microVMs on bare-metal infrastructure.
|
|
27
|
+
Nightshift runs AI agents in isolated [Firecracker](https://firecracker-microvm.github.io/) microVMs on bare-metal infrastructure.
|
|
28
28
|
Each agent gets its own microVM with a dedicated filesystem, network, and resource limits; so agents can execute code, edit files, and make network calls without affecting the host or each other.
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
@@ -15,7 +15,7 @@ There are two kinds of keys:
|
|
|
15
15
|
|
|
16
16
|
## Bootstrap key
|
|
17
17
|
|
|
18
|
-
The bootstrap key is set via the `NIGHTSHIFT_API_KEY` environment variable. When the server starts, it hashes the value and stores it in the database. This is the only key that bypasses the API.
|
|
18
|
+
The bootstrap key is set via the `NIGHTSHIFT_API_KEY` environment variable. When the server starts, it hashes the value and stores it in the database. This is the only key that bypasses the API.
|
|
19
19
|
|
|
20
20
|
The operator generates the key themselves:
|
|
21
21
|
|
|
@@ -90,7 +90,7 @@ Use the hash prefix from `api-key list`. Provide more characters if the prefix i
|
|
|
90
90
|
|
|
91
91
|
## Multi-tenant isolation
|
|
92
92
|
|
|
93
|
-
Agents and runs are scoped by tenant ID. One tenant cannot see or run another's agents.
|
|
93
|
+
Agents and runs are scoped by tenant ID. One tenant cannot see or run another's agents.
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
nightshift api-key generate --tenant acme-corp --label "acme production"
|
|
@@ -28,7 +28,7 @@ Nightshift uses SQLite. The database is a single file at `/opt/nightshift/nights
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
<Warning>
|
|
31
|
-
Do NOT `cp` the file while the server is running,SQLite may have uncommitted WAL data. Always use `sqlite3 .backup` or stop the server first.
|
|
31
|
+
Do NOT `cp` the file while the server is running, SQLite may have uncommitted WAL data. Always use `sqlite3 .backup` or stop the server first.
|
|
32
32
|
</Warning>
|
|
33
33
|
</Tab>
|
|
34
34
|
<Tab title="Automated (cron)">
|
|
@@ -25,7 +25,7 @@ bash cron.sh
|
|
|
25
25
|
Creates `nightshift-lambda-role` with EC2 start/stop permissions scoped to instances tagged `nightshift-dev`.
|
|
26
26
|
</Step>
|
|
27
27
|
<Step title="Lambda functions">
|
|
28
|
-
Creates `nightshift-start` and `nightshift-stop`
|
|
28
|
+
Creates `nightshift-start` and `nightshift-stop`
|
|
29
29
|
</Step>
|
|
30
30
|
<Step title="Scheduler IAM role">
|
|
31
31
|
Creates `nightshift-scheduler-role` with `lambda:InvokeFunction` permission.
|
|
@@ -6,13 +6,13 @@ description: "Platform for scurely running agents in isolated sandboxes."
|
|
|
6
6
|
|
|
7
7
|
<img src="/logo/kokapo.gif" alt="Nightshift mascot" width="200" style={{ margin: "0 auto", display: "block" }} />
|
|
8
8
|
|
|
9
|
-
Nightshift runs Agents in microVMs using [Firecracker](https://firecracker-microvm.github.io/) for [hardware level isolation](https://linux-kvm.org/page/Main_Page).
|
|
9
|
+
Nightshift runs Agents in microVMs using [Firecracker](https://firecracker-microvm.github.io/) for [hardware level isolation](https://linux-kvm.org/page/Main_Page).
|
|
10
10
|
Each agent gets its own isolated VM with dedicated vCPUs, memory, filesystem, and network and streams results back via Server-Sent Events.
|
|
11
11
|
|
|
12
12
|
## What is Nightshift?
|
|
13
13
|
|
|
14
|
-
Nightshift is an open source platform for deploying and running Agents in secure, efficient, isolated environments.
|
|
15
|
-
You write an agent as a Python async generator, deploy it with a single CLI command, and run it on demand.
|
|
14
|
+
Nightshift is an open source platform for deploying and running Agents in secure, efficient, isolated environments.
|
|
15
|
+
You write an agent as a Python async generator, deploy it with a single CLI command, and run it on demand.
|
|
16
16
|
Each run spins up a dedicated Firecracker microVM with its own resources and network, executes your agent, and streams events back in real time.
|
|
17
17
|
|
|
18
18
|
A hosted platform is available at `api.nightshift.sh` or you can self-host on any linux machine with KVM support.
|
|
@@ -46,7 +46,7 @@ async def my_agent(prompt: str):
|
|
|
46
46
|
|
|
47
47
|
- The function must be `async def` and use `yield` (async generator)
|
|
48
48
|
- `prompt: str` is the only parameter it's what the user passes to your function at runtime
|
|
49
|
-
- Each `yield` sends an SSE event back to the caller
|
|
49
|
+
- Each `yield` sends an SSE event back to the caller
|
|
50
50
|
- Yielded values can be dicts, dataclasses, or Pydantic models; they're auto-serialized with a `type` field and `timestamp` added if missing
|
|
51
51
|
- Inside the VM, the workspace is always at `/workspace`
|
|
52
52
|
<Note>
|
|
@@ -58,7 +58,7 @@ async def my_agent(prompt: str):
|
|
|
58
58
|
```bash
|
|
59
59
|
nightshift login --url https://api.nightshift.sh --api-key ns_<key>
|
|
60
60
|
```
|
|
61
|
-
Saves credentials to `~/.nightshift/config.toml`.
|
|
61
|
+
Saves credentials to `~/.nightshift/config.toml`.
|
|
62
62
|
|
|
63
63
|
<Note>
|
|
64
64
|
If you're running a self-hosted platform, replace `api.nightshift.sh` with your server's hostname or `http://<IP>:3000`.
|
|
@@ -78,7 +78,7 @@ async def my_agent(prompt: str):
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
This will show the agents you have deployed in Nightshift.
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
</Step>
|
|
83
83
|
<Step title="Run an agent">
|
|
84
84
|
```bash
|
|
@@ -46,6 +46,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
46
46
|
esac
|
|
47
47
|
done
|
|
48
48
|
|
|
49
|
+
# Strip protocol prefix if provided (e.g. https://example.com -> example.com)
|
|
50
|
+
HOSTNAME="${HOSTNAME#https://}"
|
|
51
|
+
HOSTNAME="${HOSTNAME#http://}"
|
|
52
|
+
|
|
49
53
|
if [ -z "$HOSTNAME" ]; then
|
|
50
54
|
echo "Error: --hostname is required"
|
|
51
55
|
echo "Usage: $0 --hostname <FQDN> --api-key <key> [--region <region>] [--version <tag>] [--port <port>]"
|
|
@@ -33,6 +33,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
33
33
|
esac
|
|
34
34
|
done
|
|
35
35
|
|
|
36
|
+
# Strip protocol prefix if provided (e.g. https://example.com -> example.com)
|
|
37
|
+
HOSTNAME="${HOSTNAME#https://}"
|
|
38
|
+
HOSTNAME="${HOSTNAME#http://}"
|
|
39
|
+
|
|
36
40
|
if [ -z "$HOSTNAME" ]; then
|
|
37
41
|
echo "Error: --hostname is required"
|
|
38
42
|
echo "Usage: $0 --hostname <FQDN> --api-key <key> [--port <port>]"
|
|
@@ -51,6 +51,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
51
51
|
esac
|
|
52
52
|
done
|
|
53
53
|
|
|
54
|
+
# Strip protocol prefix if provided (e.g. https://example.com -> example.com)
|
|
55
|
+
PROD_HOSTNAME="${PROD_HOSTNAME#https://}"
|
|
56
|
+
PROD_HOSTNAME="${PROD_HOSTNAME#http://}"
|
|
57
|
+
|
|
54
58
|
if [ "$PRODUCTION" = true ] && [ -z "$PROD_HOSTNAME" ]; then
|
|
55
59
|
echo "Error: --production requires --hostname"
|
|
56
60
|
echo "Usage: $0 --production --hostname <FQDN> [--port <port>]"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nightshift-sdk"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.3"
|
|
4
4
|
description = "Nightshift — autonomous agent orchestrator with Firecracker VMs"
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
license = {text = "Apache-2.0"}
|
|
@@ -57,6 +57,7 @@ packages = ["src/nightshift"]
|
|
|
57
57
|
[tool.pytest.ini_options]
|
|
58
58
|
asyncio_mode = "auto"
|
|
59
59
|
testpaths = ["tests"]
|
|
60
|
+
pythonpath = ["src"]
|
|
60
61
|
|
|
61
62
|
[tool.ruff]
|
|
62
63
|
target-version = "py312"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""nightshift agents — list and manage deployed agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from nightshift.cli.config import get_auth_headers, get_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group(invoke_without_command=True)
|
|
12
|
+
@click.option("-v", "--verbose", is_flag=True, help="Show invoke/workspace URLs.")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def agents(ctx, verbose: bool) -> None:
|
|
15
|
+
"""List deployed agents. Use 'agents rm' to delete one."""
|
|
16
|
+
if ctx.invoked_subcommand is not None:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
url = get_url()
|
|
20
|
+
headers = get_auth_headers()
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
r = httpx.get(f"{url}/api/agents", headers=headers, timeout=30)
|
|
24
|
+
r.raise_for_status()
|
|
25
|
+
except httpx.HTTPStatusError as e:
|
|
26
|
+
raise click.ClickException(f"{e.response.status_code}: {e.response.text}")
|
|
27
|
+
except httpx.HTTPError as e:
|
|
28
|
+
raise click.ClickException(str(e))
|
|
29
|
+
|
|
30
|
+
agent_list = r.json()
|
|
31
|
+
if not agent_list:
|
|
32
|
+
click.echo("No agents deployed.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if verbose:
|
|
36
|
+
for i, a in enumerate(agent_list):
|
|
37
|
+
if i > 0:
|
|
38
|
+
click.echo()
|
|
39
|
+
click.echo(a["name"])
|
|
40
|
+
click.echo(f" Source: {a['source_filename']}")
|
|
41
|
+
click.echo(f" Invoke: {a['invoke_url']}")
|
|
42
|
+
if a.get("workspace_url"):
|
|
43
|
+
click.echo(f" Workspace: {a['workspace_url']}")
|
|
44
|
+
click.echo(f" Updated: {a['updated_at']}")
|
|
45
|
+
else:
|
|
46
|
+
click.echo(f"{'NAME':<20} {'SOURCE':<25} {'UPDATED':<25}")
|
|
47
|
+
click.echo("-" * 70)
|
|
48
|
+
for a in agent_list:
|
|
49
|
+
click.echo(f"{a['name']:<20} {a['source_filename']:<25} {a['updated_at']:<25}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@agents.command()
|
|
53
|
+
@click.argument("name")
|
|
54
|
+
def info(name: str) -> None:
|
|
55
|
+
"""Show details for a single agent."""
|
|
56
|
+
url = get_url()
|
|
57
|
+
headers = get_auth_headers()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
r = httpx.get(f"{url}/api/agents/{name}", headers=headers, timeout=30)
|
|
61
|
+
r.raise_for_status()
|
|
62
|
+
except httpx.HTTPStatusError as e:
|
|
63
|
+
raise click.ClickException(f"{e.response.status_code}: {e.response.text}")
|
|
64
|
+
except httpx.HTTPError as e:
|
|
65
|
+
raise click.ClickException(str(e))
|
|
66
|
+
|
|
67
|
+
a = r.json()
|
|
68
|
+
click.echo(a["name"])
|
|
69
|
+
click.echo(f" ID: {a['id']}")
|
|
70
|
+
click.echo(f" Source: {a['source_filename']}")
|
|
71
|
+
click.echo(f" Stateful: {a['stateful']}")
|
|
72
|
+
click.echo(f" Invoke: {a['invoke_url']}")
|
|
73
|
+
if a.get("workspace_url"):
|
|
74
|
+
click.echo(f" Workspace: {a['workspace_url']}")
|
|
75
|
+
|
|
76
|
+
cfg = a.get("config", {})
|
|
77
|
+
if cfg:
|
|
78
|
+
click.echo(f" vCPU: {cfg.get('vcpu_count')}")
|
|
79
|
+
click.echo(f" Memory: {cfg.get('mem_size_mib')} MiB")
|
|
80
|
+
click.echo(f" Timeout: {cfg.get('timeout_seconds')}s")
|
|
81
|
+
|
|
82
|
+
click.echo(f" Created: {a['created_at']}")
|
|
83
|
+
click.echo(f" Updated: {a['updated_at']}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@agents.command()
|
|
87
|
+
@click.argument("name")
|
|
88
|
+
def rm(name: str) -> None:
|
|
89
|
+
"""Delete a deployed agent."""
|
|
90
|
+
url = get_url()
|
|
91
|
+
headers = get_auth_headers()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
r = httpx.delete(f"{url}/api/agents/{name}", headers=headers, timeout=30)
|
|
95
|
+
r.raise_for_status()
|
|
96
|
+
except httpx.HTTPStatusError as e:
|
|
97
|
+
raise click.ClickException(f"{e.response.status_code}: {e.response.text}")
|
|
98
|
+
except httpx.HTTPError as e:
|
|
99
|
+
raise click.ClickException(str(e))
|
|
100
|
+
|
|
101
|
+
click.echo(f"Deleted agent: {name}")
|
|
@@ -96,6 +96,15 @@ def _discover_agents(file_path: str) -> dict:
|
|
|
96
96
|
raise click.ClickException(
|
|
97
97
|
f"Agent '{name}': workspace directory does not exist: {workspace}"
|
|
98
98
|
)
|
|
99
|
+
# Resolve forward_secrets from the deployer's local environment
|
|
100
|
+
secrets: dict[str, str] = {}
|
|
101
|
+
for key in agent.config.forward_secrets:
|
|
102
|
+
val = os.environ.get(key)
|
|
103
|
+
if val is None:
|
|
104
|
+
click.echo(f" Warning: forward_secrets key '{key}' not found in environment", err=True)
|
|
105
|
+
else:
|
|
106
|
+
secrets[key] = val
|
|
107
|
+
|
|
99
108
|
agents[name] = {
|
|
100
109
|
"function_name": agent.fn.__name__,
|
|
101
110
|
"workspace": workspace,
|
|
@@ -108,6 +117,7 @@ def _discover_agents(file_path: str) -> dict:
|
|
|
108
117
|
"env": agent.config.env,
|
|
109
118
|
"max_concurrent_vms": agent.config.max_concurrent_vms,
|
|
110
119
|
"stateful": agent.config.stateful,
|
|
120
|
+
"secrets": secrets,
|
|
111
121
|
},
|
|
112
122
|
}
|
|
113
123
|
return agents
|
|
@@ -11,7 +11,7 @@ import json
|
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
13
13
|
from dataclasses import asdict, dataclass, field
|
|
14
|
-
from typing import AsyncIterator, Literal, Union
|
|
14
|
+
from typing import AsyncIterator, Awaitable, Callable, Literal, Union
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -62,14 +62,17 @@ class EventBuffer:
|
|
|
62
62
|
(replay + live-tail) via an asyncio.Condition.
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
|
-
def __init__(self) -> None:
|
|
65
|
+
def __init__(self, persist: Callable[[str, str, dict], Awaitable[None]] | None = None) -> None:
|
|
66
66
|
self._runs: dict[str, list[tuple[str, dict]]] = {}
|
|
67
67
|
self._cond: asyncio.Condition = asyncio.Condition()
|
|
68
68
|
self._done: set[str] = set()
|
|
69
|
+
self._persist = persist
|
|
69
70
|
|
|
70
71
|
async def append(self, run_id: str, event_type: str, payload: dict) -> None:
|
|
71
72
|
"""Append an event to a run's buffer and notify waiters."""
|
|
72
73
|
self._runs.setdefault(run_id, []).append((event_type, payload))
|
|
74
|
+
if self._persist is not None:
|
|
75
|
+
await self._persist(run_id, event_type, payload)
|
|
73
76
|
async with self._cond:
|
|
74
77
|
self._cond.notify_all()
|
|
75
78
|
|
|
@@ -104,6 +107,10 @@ class EventBuffer:
|
|
|
104
107
|
self._runs.pop(run_id, None)
|
|
105
108
|
self._done.discard(run_id)
|
|
106
109
|
|
|
110
|
+
def has_run(self, run_id: str) -> bool:
|
|
111
|
+
"""Check whether a run is currently held in the in-memory buffer."""
|
|
112
|
+
return run_id in self._runs
|
|
113
|
+
|
|
107
114
|
# ── Convenience methods (used by task.py, vm/manager.py, agent/entry.py) ──
|
|
108
115
|
|
|
109
116
|
async def publish(self, run_id: str, event: NightshiftEvent) -> None:
|
|
@@ -123,7 +130,7 @@ class EventBuffer:
|
|
|
123
130
|
async for event_type, payload in self.stream(run_id):
|
|
124
131
|
yield {
|
|
125
132
|
"event": event_type,
|
|
126
|
-
"data": json.dumps({"type": event_type, **payload}),
|
|
133
|
+
"data": json.dumps({"type": event_type, **payload}, default=str),
|
|
127
134
|
}
|
|
128
135
|
if event_type in TERMINAL_EVENTS:
|
|
129
136
|
return
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Serialize agent messages to JSON dicts for SSE transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import asdict, is_dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_json_safe(obj: Any) -> Any:
|
|
12
|
+
"""Recursively convert non-JSON-serializable objects in a data structure."""
|
|
13
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
14
|
+
return obj
|
|
15
|
+
if isinstance(obj, dict):
|
|
16
|
+
return {k: _make_json_safe(v) for k, v in obj.items()}
|
|
17
|
+
if isinstance(obj, (list, tuple)):
|
|
18
|
+
return [_make_json_safe(item) for item in obj]
|
|
19
|
+
if hasattr(obj, "model_dump_json"):
|
|
20
|
+
return json.loads(obj.model_dump_json())
|
|
21
|
+
if hasattr(obj, "model_dump"):
|
|
22
|
+
return obj.model_dump(mode="json")
|
|
23
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
24
|
+
return _make_json_safe(asdict(obj))
|
|
25
|
+
if hasattr(obj, "__dict__"):
|
|
26
|
+
return _make_json_safe(vars(obj))
|
|
27
|
+
return str(obj)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def serialize_message(message: Any) -> dict:
|
|
31
|
+
"""Convert an agent message to a JSON-serializable dict.
|
|
32
|
+
|
|
33
|
+
Handles:
|
|
34
|
+
- dicts (passed through with defaults)
|
|
35
|
+
- dataclasses (converted via asdict, with nested pydantic models resolved)
|
|
36
|
+
- pydantic models (converted via model_dump_json for full JSON safety)
|
|
37
|
+
- objects with a __dict__ attribute
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(message, dict):
|
|
40
|
+
data = dict(message)
|
|
41
|
+
elif is_dataclass(message) and not isinstance(message, type):
|
|
42
|
+
data = _make_json_safe(asdict(message))
|
|
43
|
+
elif hasattr(message, "model_dump_json"):
|
|
44
|
+
data = json.loads(message.model_dump_json())
|
|
45
|
+
elif hasattr(message, "__dict__"):
|
|
46
|
+
data = _make_json_safe(vars(message))
|
|
47
|
+
else:
|
|
48
|
+
data = {"data": str(message)}
|
|
49
|
+
|
|
50
|
+
if "type" not in data:
|
|
51
|
+
data["type"] = type(message).__name__
|
|
52
|
+
|
|
53
|
+
if "timestamp" not in data:
|
|
54
|
+
data["timestamp"] = time.time()
|
|
55
|
+
|
|
56
|
+
return data
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import time
|
|
6
7
|
import uuid
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from datetime import datetime, timezone
|
|
@@ -86,6 +87,15 @@ class AgentRegistry:
|
|
|
86
87
|
label TEXT DEFAULT '',
|
|
87
88
|
created_at TEXT NOT NULL
|
|
88
89
|
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS run_events (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
run_id TEXT NOT NULL,
|
|
94
|
+
event_type TEXT NOT NULL,
|
|
95
|
+
payload_json TEXT NOT NULL,
|
|
96
|
+
created_at REAL NOT NULL
|
|
97
|
+
);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_run_events_run_id ON run_events(run_id);
|
|
89
99
|
"""
|
|
90
100
|
)
|
|
91
101
|
await self._db.commit()
|
|
@@ -247,6 +257,22 @@ class AgentRegistry:
|
|
|
247
257
|
status=r[4], created_at=r[5], completed_at=r[6], error=r[7],
|
|
248
258
|
)
|
|
249
259
|
|
|
260
|
+
# ── Run Events ────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async def save_event(self, run_id: str, event_type: str, payload: dict) -> None:
|
|
263
|
+
await self.db.execute(
|
|
264
|
+
"INSERT INTO run_events (run_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?)",
|
|
265
|
+
(run_id, event_type, json.dumps(payload, default=str), time.time()),
|
|
266
|
+
)
|
|
267
|
+
await self.db.commit()
|
|
268
|
+
|
|
269
|
+
async def get_run_events(self, run_id: str) -> list[tuple[str, dict]]:
|
|
270
|
+
rows = await self.db.execute_fetchall(
|
|
271
|
+
"SELECT event_type, payload_json FROM run_events WHERE run_id = ? ORDER BY id",
|
|
272
|
+
(run_id,),
|
|
273
|
+
)
|
|
274
|
+
return [(row[0], json.loads(row[1])) for row in rows]
|
|
275
|
+
|
|
250
276
|
# ── API Keys ──────────────────────────────────────────────────
|
|
251
277
|
|
|
252
278
|
async def store_api_key(self, key_hash: str, tenant_id: str, label: str = "") -> None:
|
|
@@ -12,6 +12,7 @@ class AgentConfig:
|
|
|
12
12
|
mem_size_mib: int = 2048
|
|
13
13
|
timeout_seconds: int = 1800
|
|
14
14
|
forward_env: list[str] = field(default_factory=list)
|
|
15
|
+
forward_secrets: list[str] = field(default_factory=list)
|
|
15
16
|
env: dict[str, str] = field(default_factory=dict)
|
|
16
17
|
max_concurrent_vms: int = 0 # 0 = use platform default; >0 = explicit limit
|
|
17
18
|
stateful: bool = False # True = single VM, serial, workspace extracted on teardown
|